diff --git a/package.json b/package.json index 47a8e60f..bc0ecbea 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:validations": "mocha --parallel -r ts-node/register/transpile-only test/validator.test.ts", "test:wtest": "mocha --delay -t 10000 -r ts-node/register/transpile-only test/wtest.ts", "test:printer": "mocha --parallel -r ts-node/register/transpile-only test/printer.test.ts", + "test:parser": "mocha --parallel -r ts-node/register/transpile-only test/parser.test.ts", "prepublishOnly": "npm run build && npm test", "postpublish": "git tag v$npm_package_version && git push --tags", "prepack": "npm run build" diff --git a/src/interpreter/runtimeModel.ts b/src/interpreter/runtimeModel.ts index 640d7c7f..8d7245ba 100644 --- a/src/interpreter/runtimeModel.ts +++ b/src/interpreter/runtimeModel.ts @@ -489,7 +489,7 @@ export class Evaluation { const target = node.instantiated.target ?? raise(new Error(`Could not resolve reference to instantiated module ${node.instantiated.name}`)) const name = node.instantiated.name - + if (!target.is(Class)) raise(new Error(`${name} is not a class, you cannot generate instances of a ${target?.kind}`)) if (target.isAbstract) raise(new Error(`${name} is an abstract class, you cannot generate instances`)) diff --git a/src/model.ts b/src/model.ts index d22d606d..9c1090e3 100644 --- a/src/model.ts +++ b/src/model.ts @@ -54,6 +54,14 @@ export class Annotation { } } +export class Comment { + readonly text: string + + constructor(name: Name){ + this.text = name + } +} + export type Code = string export type Level = 'warning' | 'error' @@ -471,6 +479,12 @@ export function Module>(supertype: S) { return undefined } + @cached + get isAbstract(): boolean { + const abstractMethods = this.hierarchy.flatMap(module => module.methods.filter(method => method.isAbstract())) + return abstractMethods.some(method => !this.lookupMethod(method.name, method.parameters.length)) + } + @cached defaultValueFor(field: Field): Expression { if(!this.allFields.includes(field)) throw new Error('Field does not belong to the module') @@ -508,12 +522,6 @@ export class Class extends Module(Node) { return this === objectClass ? undefined : objectClass } } - - @cached - get isAbstract(): boolean { - const abstractMethods = this.hierarchy.flatMap(module => module.methods.filter(method => method.isAbstract())) - return abstractMethods.some(method => !this.lookupMethod(method.name, method.parameters.length)) - } } @@ -750,7 +758,7 @@ export class Super extends Expression(Node) { export class New extends Expression(Node) { get kind(): 'New' { return 'New' } - readonly instantiated!: Reference + readonly instantiated!: Reference readonly args!: List constructor({ args = [], ...payload }: Payload) { diff --git a/src/parser.ts b/src/parser.ts index dbad567f..c3c7e2e1 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -1,8 +1,8 @@ -import Parsimmon, { alt as alt_parser, index, lazy, makeSuccess, notFollowedBy, of, Parser, regex, seq, seqObj, string, whitespace, any, Index, newline } from 'parsimmon' +import Parsimmon, { Index, Parser, alt as alt_parser, any, index, lazy, makeSuccess, newline, notFollowedBy, of, regex, seq, seqObj, string, whitespace } from 'parsimmon' import unraw from 'unraw' -import { BaseProblem, SourceIndex, Assignment as AssignmentNode, Body as BodyNode, Catch as CatchNode, Class as ClassNode, Describe as DescribeNode, Entity as EntityNode, Expression as ExpressionNode, Field as FieldNode, If as IfNode, Import as ImportNode, Literal as LiteralNode, Method as MethodNode, Mixin as MixinNode, Name, NamedArgument as NamedArgumentNode, New as NewNode, Node, Package as PackageNode, Parameter as ParameterNode, Program as ProgramNode, Reference as ReferenceNode, Return as ReturnNode, Self as SelfNode, Send as SendNode, Sentence as SentenceNode, Singleton as SingletonNode, Super as SuperNode, Test as TestNode, Throw as ThrowNode, Try as TryNode, Variable as VariableNode, SourceMap, Closure as ClosureNode, ParameterizedType as ParameterizedTypeNode, Level, LiteralValue, Annotation } from './model' -import { List, mapObject, discriminate, is } from './extensions' import { ASSIGNATION_OPERATORS, INFIX_OPERATORS, PREFIX_OPERATORS } from './constants' +import { List, discriminate, is, mapObject } from './extensions' +import { Annotation, Assignment as AssignmentNode, BaseProblem, Body as BodyNode, Catch as CatchNode, Class as ClassNode, Closure as ClosureNode, Describe as DescribeNode, Entity as EntityNode, Expression as ExpressionNode, Field as FieldNode, If as IfNode, Import as ImportNode, Level, Literal as LiteralNode, LiteralValue, Method as MethodNode, Mixin as MixinNode, Name, NamedArgument as NamedArgumentNode, New as NewNode, Node, Package as PackageNode, Parameter as ParameterNode, ParameterizedType as ParameterizedTypeNode, Program as ProgramNode, Reference as ReferenceNode, Return as ReturnNode, Self as SelfNode, Send as SendNode, Sentence as SentenceNode, Singleton as SingletonNode, SourceIndex, SourceMap, Super as SuperNode, Test as TestNode, Throw as ThrowNode, Try as TryNode, Variable as VariableNode } from './model' // TODO: Use description in lazy() for better errors // TODO: Support FQReferences to singletons as expressions @@ -38,7 +38,7 @@ const error = (code: string) => (...safewords: string[]) => { const skippable = (...breakpoints: Parser[]): Parser => lazy(() => alt( skippableContext, - comment, + comment('start'), notFollowedBy(alt(key('}'), newline, ...breakpoints)).then(any), ) ) @@ -80,11 +80,16 @@ const key = (str: T): Parser => ( : string(str) ).trim(_) -const comment = lazy('comment', () => regex(/\/\*(.|[\r\n])*?\*\/|\/\/.*/)) -const _ = optional(comment.or(whitespace).atLeast(1)) +const _ = optional(whitespace.atLeast(1)) const __ = optional(key(';').or(_)) +const comment = (position: 'start'|'end') => lazy('comment', () => regex(/\/\*(.|[\r\n])*?\*\/|\/\/.*/)).map(text => new Annotation('comment', { text, position })) +const endComment = alt( + optional(_).then(comment('end')), // same-line comment + comment('end').sepBy(_) // after-line comments +) + export const annotation: Parser = lazy(() => string('@').then(obj({ name, @@ -100,15 +105,15 @@ export const annotation: Parser = lazy(() => const node = (constructor: new (payload: P) => N) => (parser: () => Parser

): Parser => seq( - annotation.sepBy(_).wrap(_, _), + alt(annotation, comment('start')).sepBy(_).wrap(_, _), index, lazy(parser), - index - ).map(([metadata, start, payload, end]) => - new constructor({ metadata, sourceMap: buildSourceMap(start, end), ...payload }) + endComment, + index, + ).map(([metadata, start, payload, comment, end]) => + new constructor({ metadata: metadata.concat(comment), sourceMap: buildSourceMap(start, end), ...payload }) ) - export const File = (fileName: string): Parser => lazy(() => obj({ fileName: of(fileName), diff --git a/src/printer/print.ts b/src/printer/print.ts index 31542f81..a50ed6c5 100644 --- a/src/printer/print.ts +++ b/src/printer/print.ts @@ -1,8 +1,8 @@ import { IDoc, align, append, choice, enclose, intersperse, lineBreak, lineBreaks, hang as nativeHang, indent as nativeIndent, nest as nativeNest, parens, prepend, render, softBreak, softLine } from 'prettier-printer' import { KEYWORDS, LIST_MODULE, PREFIX_OPERATORS, SET_MODULE } from '../constants' import { List, isEmpty, match, notEmpty, when } from '../extensions' -import { Assignment, Body, Catch, Class, Describe, Expression, Field, If, Import, Literal, Method, Mixin, Name, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Sentence, Singleton, Super, Test, Throw, Try, Variable } from '../model' -import { DocTransformer, WS, body, enclosedList, infixOperators, listEnclosers, listed, setEnclosers, stringify } from './utils' +import { Annotation, Assignment, Body, Catch, Class, Describe, Expression, Field, If, Import, Literal, Method, Mixin, Name, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Sentence, Singleton, Super, Test, Throw, Try, Variable } from '../model' +import { DocTransformer, WS, body, defaultToEmpty, enclosedList, infixOperators, listEnclosers, listed, setEnclosers, stringify } from './utils' type PrintSettings = { maxWidth: number, @@ -45,7 +45,8 @@ function print(node: Node, { maxWidth, indentation, abbreviateAssignments }: Pri type Formatter = (node: T) => IDoc type FormatterWithContext = (context: PrintContext) => Formatter const format: FormatterWithContext = context => node => { - return match(node)( + const metadata: [IDoc, IDoc] = splitMetadata(context, node.metadata) + const formattedNode: IDoc = match(node)( when(Package)(formatPackage(context)), when(Program)(formatProgram(context)), when(Assignment)(formatAssignment(context)), @@ -74,6 +75,8 @@ const format: FormatterWithContext = context => node => { when(Import)(formatImport), when(Super)(formatSuper(context)), ) + + return enclose(metadata, formattedNode) } const formatPackage: FormatterWithContext = context => node => { @@ -102,7 +105,7 @@ const formatMethod: FormatterWithContext = context => node => { KEYWORDS.METHOD, WS, node.name, - enclosedList(context.nest)(parens, node.parameters.map(formatWithContext)), + enclosedListOfNodes(context)(parens, node.parameters), ] if(node.isNative()){ @@ -222,7 +225,7 @@ function isInlineBody(aBody: Body): aBody is Body & { sentences: [Expression] } const formatNew: FormatterWithContext = context => node => { const args = - enclosedList(context.nest)(parens, node.args.map(format(context))) + enclosedListOfNodes(context)(parens, node.args) return [ KEYWORDS.NEW, WS, @@ -316,7 +319,7 @@ const formatParameterizedType: FormatterWithContext = context => node => [ node.reference.name, notEmpty(node.args) ? - [WS, enclosedList(context.nest)(parens, node.args.map(format(context)))] : + [WS, enclosedListOfNodes(context)(parens, node.args)] : [], ] @@ -427,7 +430,7 @@ const formatSentences = (context: PrintContext) => (sentences: List, s return [formatted, formatSentenceInBody(context)(!shouldShortenReturn ? sentence : sentence.value, previousSentence)] }, []) -const formatArguments = (context: PrintContext) => (args: List): IDoc => enclosedList(context.nest)(parens, args.map(format(context))) +const formatArguments = (context: PrintContext) => (args: List): IDoc => enclosedListOfNodes(context)(parens, args) const formatSentenceInBody = (context: PrintContext) => (sentence: Sentence, previousSentence: Sentence | undefined): IDoc => { const distanceFromLastSentence = (sentence.sourceMap && (!previousSentence || previousSentence.sourceMap) ? @@ -452,7 +455,7 @@ const formatAssign = (context: PrintContext, ignoreNull = false) => (name: strin ] const formatCollection = (context: PrintContext) => (values: Expression[], enclosers: [IDoc, IDoc]) => { - return enclosedList(context.nest)(enclosers, values.map(format(context))) + return enclosedListOfNodes(context)(enclosers, values) } const formatModuleMembers = (context: PrintContext) => (members: List): IDoc => { @@ -487,4 +490,39 @@ const prefixOperatorByMessage: Record = { 'negate': '!', 'invert': '-', 'plus': '+', -} \ No newline at end of file +} + +// metadata +const splitMetadata = (context: PrintContext, metadata: List): [IDoc, IDoc] => { + const withSplittedMultilineComments = metadata.map(annotation => annotation.name === 'comment' && (annotation.args.get('text')! as string).includes('\n') ? + (annotation.args.get('text')! as string).split('\n').map(commentSection => new Annotation('comment', { text: commentSection.trimStart(), position:annotation.args.get('position')! } )) : + annotation + ).flat() + + const prevMetadata = withSplittedMultilineComments.filter(metadata => !isComment(metadata) || metadata.args.get('position') === 'start') + const afterMetadata = withSplittedMultilineComments.filter(metadata => metadata.args.get('position') === 'end') + const metadataBefore = defaultToEmpty(notEmpty(prevMetadata), [intersperse(lineBreak, prevMetadata.map(formatAnnotation(context))), lineBreak]) + const metadataAfter = defaultToEmpty(notEmpty(afterMetadata), [softLine, intersperse(lineBreak, afterMetadata.map(formatAnnotation(context)))]) + + return [metadataBefore, metadataAfter] +} + +const formatAnnotation = (context: PrintContext) => (annotation: Annotation): IDoc => { + if(annotation.name === 'comment') return annotation.args.get('text')! as string + return ['@', annotation.name, enclosedList(context.nest)(parens, [...annotation.args.entries()].map( + ([name, value]) => intersperse(WS, [name, '=', format(context)(new Literal({ value }))]) + ))] +} + +function isComment(annotation: Annotation): annotation is Annotation & {name: 'comment'} { + return annotation.name === 'comment' +} + + +//lists +const enclosedListOfNodes = (context: PrintContext) => (enclosers: [IDoc, IDoc], nodes: List): IDoc => + enclosedList(context.nest)( + enclosers, + nodes.map(format(context)), + nodes.some(aNode => aNode.metadata.some(entry => entry.name ==='comment')) + ) \ No newline at end of file diff --git a/src/printer/utils.ts b/src/printer/utils.ts index 062b25d5..50fefd0d 100644 --- a/src/printer/utils.ts +++ b/src/printer/utils.ts @@ -19,13 +19,14 @@ export const body = (nest: DocTransformer) => (content: IDoc): IDoc => encloseIn */ export const listed = (contents: IDoc[], separator: IDoc = ','): IDoc => intersperse([separator, softLine], contents) -export const enclosedList = (nest: DocTransformer) => (enclosers: [IDoc, IDoc], content: IDoc[], separator: IDoc = ','): IDoc => { +export const enclosedList = (nest: DocTransformer) => (enclosers: [IDoc, IDoc], content: IDoc[], forceSpread = false, separator: IDoc = ','): IDoc => { if(content.length === 0) return enclose(enclosers, '') + const narrowFormat = encloseIndented(['', ''], intersperse([separator, lineBreak], content), nest) return enclose( enclosers, - choice( + forceSpread ? narrowFormat : choice( intersperse([separator, WS], content), - encloseIndented(['', ''], intersperse([separator, lineBreak], content), nest) + narrowFormat ) ) } @@ -35,6 +36,8 @@ export const encloseIndented = (enclosers: [IDoc, IDoc], content: IDoc, nest: Do export const stringify = enclose(dquotes) +export const defaultToEmpty = (condition: boolean, doc: IDoc): IDoc => condition ? doc : [] + export const prefixIfNotEmpty = (prefix: IDoc) => (docs: IDocArray): IDoc => docs.length === 0 ? prepend(prefix, docs) : docs diff --git a/test/assertions.ts b/test/assertions.ts index b4343701..c0443524 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -1,16 +1,14 @@ -import { formatError, Parser } from 'parsimmon' -import link from '../src/linker' -import { Name, Node, Package, Reference, Environment as EnvironmentType, Environment } from '../src/model' -import { List } from '../src/extensions' -import { Validation } from '../src/validator' -import { File, ParseError } from '../src/parser' -import globby from 'globby' +import dedent from 'dedent' import { promises } from 'fs' -import { buildEnvironment as buildEnv, print, WRE } from '../src' +import globby from 'globby' +import { formatError, Parser } from 'parsimmon' import { join } from 'path' -import validate from '../src/validator' -import dedent from 'dedent' -import { fromJSON } from '../src/jsonUtils' +import { buildEnvironment as buildEnv, print } from '../src' +import { List } from '../src/extensions' +import link from '../src/linker' +import { Environment, Environment as EnvironmentType, Name, Node, Package, Reference } from '../src/model' +import { ParseError } from '../src/parser' +import validate, { Validation } from '../src/validator' const { readFile } = promises @@ -97,7 +95,6 @@ export const parserAssertions: Chai.ChaiPlugin = (chai, utils) => { new Assertion(expectedProblems).to.deep.contain.all.members(actualProblems, 'Unexpected problem found') new Assertion(actualProblems).to.deep.contain.all.members(expectedProblems, 'Expected problem not found') - new Assertion(plucked(this._obj)).to.deep.equal(plucked(expected)) }) @@ -122,12 +119,10 @@ export const printerAssertions: Chai.ChaiPlugin = (chai) => { const { Assertion } = chai Assertion.addMethod('formattedTo', function (expected: string) { - const fileName = 'formatted' - const parsed = File(fileName).parse(this._obj) - if(!parsed.status) throw new Error('Failed to parse code') - const environment = link([parsed.value], fromJSON(WRE)) + const name = 'formatted' + const environment = buildEnv([{ name, content: this._obj }]) const printerConfig = { maxWidth: 80, indentation: { size: 2, useSpaces: true }, abbreviateAssignments: true } - const formatted = print(environment.getNodeByFQN(fileName), printerConfig) + const formatted = print(environment.getNodeByFQN(name), printerConfig) new Assertion(formatted).to.equal(dedent(expected)) }) } diff --git a/test/parser.test.ts b/test/parser.test.ts index 5e05ca7d..20f04a27 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -14,16 +14,29 @@ describe('Wollok parser', () => { describe('Comments', () => { const parser = parse.Import + it('line comments should be parsed as metadata', () => { + `//some comment + import p`.should.be.parsedBy(parser).into(new Import({ + entity: new Reference({ name: 'p' }), + metadata: [new Annotation('comment', { text: 'some comment' })], + })) + .and.be.tracedTo(21, 29) + .and.have.nested.property('entity').tracedTo(28, 29) + }) + it('multiline comments should be ignored in between tokens', () => { `/*some comment*/import /* some - comment */ p`.should.be.parsedBy(parser).into(new Import({ entity: new Reference({ name: 'p' }) })) + comment */ p`.should.be.parsedBy(parser).into(new Import({ + entity: new Reference({ name: 'p', metadata: [new Annotation('comment', { text: '/* some\n comment */' })] }), + metadata: [new Annotation('comment', { text: '/*some comment*/' })], + })) .and.be.tracedTo(16, 49) .and.have.nested.property('entity').tracedTo(48, 49) }) it('line comments should be ignored at the end of line', () => { `import //some comment - p`.should.be.parsedBy(parser).into(new Import({ entity: new Reference({ name: 'p' }) })) + p`.should.be.parsedBy(parser).into(new Import({ entity: new Reference({ name: 'p', metadata: [new Annotation('comment', { text: 'some asa comment' })] }) })) .and.be.tracedTo(0, 29) .and.have.nested.property('entity').tracedTo(28, 29) }) diff --git a/test/printer.test.ts b/test/printer.test.ts index e11caa49..fd58e5de 100644 --- a/test/printer.test.ts +++ b/test/printer.test.ts @@ -109,6 +109,85 @@ describe('Wollok Printer', () => { }`) }) }) + + describe('Comments', () => { + it('single line comment', () => { + `program prueba { + // comentario + const a = 1 // other comment but this comment is actually very veeeeeeeeeeeeery veeeeeeeeeery long + const b = 2 + // last comentario + }`.should.be.formattedTo(` + program prueba { + // comentario + const a = 1 + // other comment but this comment is actually very veeeeeeeeeeeeery veeeeeeeeeery long + const b = 2 // last comentario + }`) + }) + + it('comments on send', () => { + `program prueba { + // ok + 5.even() // ok + }`.should.be.formattedTo(` + program prueba { + // ok + 5.even() // ok + }`) + }) + + it('many comments', () => { + `program prueba { + // comentario + // comentario + const a = 1 + }`.should.be.formattedTo(` + program prueba { + // comentario + // comentario + const a = 1 + }`) + }) + + //ToDo smarter trimming + it('multi line comments', () => { + `program prueba { + /* comentario + comentario */ + const a = 1 + }`.should.be.formattedTo(` + program prueba { + /* comentario + comentario */ + const a = 1 + }`) + }) + + it('side comment', () => { + `program prueba { + const a = 1 // comentario + }`.should.be.formattedTo(` + program prueba { + const a = 1 // comentario + }`) + }) + + xit('comment on a list', () => { + `program prueba { + const a = [1 + ,2//comment on a lista + ,3] + }`.should.be.formattedTo(` + program prueba { + const a = [ + 1, + 2, //comment on a lista + 3 + ] + }`) + }) + }) }) describe('Object', () => { it('testBasicObjectDefinition', () => { @@ -1428,17 +1507,14 @@ describe('Wollok Printer', () => { }`) }) - //ToDo comments - xit('testAnotherInitializeWithComplexDefinition', () => { + it('testAnotherInitializeWithComplexDefinition', () => { ` describe "testDeMusicGuide" { - // musicos var soledad var kike var lucia var joaquin - // canciones const cisne = new Cancion(titulo = "Cisne", minutos = 312, letra ="Hoy el viento se abrio quedo vacio el aire una vez mas y el manantial broto y nadie esta aqui y puedo ver que solo estallan las hojas al brillar") const laFamilia = new Cancion(titulo = "La Familia", minutos=264, letra = "Quiero brindar por mi gente sencilla, por el amor brindo por la familia") const almaDeDiamante = new Cancion(titulo @@ -1455,15 +1531,12 @@ describe('Wollok Printer', () => { = laFamilia.duracion(), letra =laFamilia.letra()) const mashupAlmaCrisantemo = new Mashup(titulo = "nombre", minutos = "duracion", letra = "letra", temas = [ almaDeDiamante, crisantemo ]) - // albumes const paraLosArboles = new Album(titulo = "Para los arboles", fecha = new Date(day = 31, month = 3, year = 2003), editados = 50000, vendidos = 49000).agregarCancion(cisne).agregarCancion(almaDeDiamante) const justCrisantemo = new Album(titulo = "Just Crisantemo", fecha = new Date(day=05, month=12, year=2007), editados = 28000, vendidos=27500).agregarCancion(crisantemo) const especialLaFamilia = new Album(titulo = "Especial La Familia", fecha = new Date(day = 17, month = 06, year = 1992), editados = 100000, vendidos = 89000).agregarCancion(laFamilia) const laSole = new Album(titulo = "La Sole", fecha = new Date(day = 04, month = 02, year = 2005), editados = 200000, vendidos = 130000).agregarCancion(eres).agregarCancion(corazonAmericano) - // presentaciones var presentacionEnLuna var presentacionEnTrastienda - // guitarras const fender = new Guitarra() const gibson = new Gibson() method @@ -1484,15 +1557,12 @@ describe('Wollok Printer', () => { test "fake" { assert.that(true) } } - `.should.be.formattedTo(` + `.should.be.formattedTo(` describe "testDeMusicGuide" { - - // musicos var soledad var kike var lucia var joaquin - // canciones const cisne = new Cancion( titulo = "Cisne", minutos = 312, @@ -1539,7 +1609,6 @@ describe('Wollok Printer', () => { letra = "letra", temas = [almaDeDiamante, crisantemo] ) - // albumes const paraLosArboles = new Album( titulo = "Para los arboles", fecha = new Date(day = 31, month = 3, year = 2003), @@ -1564,10 +1633,8 @@ describe('Wollok Printer', () => { editados = 200000, vendidos = 130000 ).agregarCancion(eres).agregarCancion(corazonAmericano) - // presentaciones var presentacionEnLuna var presentacionEnTrastienda - // guitarras const fender = new Guitarra() const gibson = new Gibson() @@ -1609,8 +1676,6 @@ describe('Wollok Printer', () => { assert.that(true) } }`) - - }) it('testDescribeWithMethodDefinition', () => { @@ -1979,22 +2044,21 @@ describe('Wollok Printer', () => { it('program_maxOneLineBreakBetweenLines', () => { `program p { - const a = 10 - const b = 0 - - - - const c = a + b - }`.should.be.formattedTo( ` - program p { - const a = 10 - const b = 0 - - - - const c = a + b - } - `) + const a = 10 + const b = 0 + + + + const c = a + b + }`.should.be.formattedTo( ` + program p { + const a = 10 + const b = 0 + + + + const c = a + b + }`) }) it('basicTryCatch', () => {