Skip to content

Commit

Permalink
comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ivojawer committed Nov 12, 2023
1 parent f25675b commit 54a5bfa
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 82 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/runtimeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`))
Expand Down
22 changes: 15 additions & 7 deletions src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -471,6 +479,12 @@ export function Module<S extends Mixable<Node>>(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')
Expand Down Expand Up @@ -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))
}
}


Expand Down Expand Up @@ -750,7 +758,7 @@ export class Super extends Expression(Node) {

export class New extends Expression(Node) {
get kind(): 'New' { return 'New' }
readonly instantiated!: Reference<Class>
readonly instantiated!: Reference<Class | Mixin>
readonly args!: List<NamedArgument>

constructor({ args = [], ...payload }: Payload<New, 'instantiated'>) {
Expand Down
27 changes: 16 additions & 11 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -38,7 +38,7 @@ const error = (code: string) => (...safewords: string[]) => {
const skippable = (...breakpoints: Parser<any>[]): Parser<any> => lazy(() =>
alt(
skippableContext,
comment,
comment('start'),
notFollowedBy(alt(key('}'), newline, ...breakpoints)).then(any),
)
)
Expand Down Expand Up @@ -80,11 +80,16 @@ const key = <T extends string>(str: T): Parser<T> => (
: 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<Annotation> = lazy(() =>
string('@').then(obj({
name,
Expand All @@ -100,15 +105,15 @@ export const annotation: Parser<Annotation> = lazy(() =>

const node = <N extends Node, P>(constructor: new (payload: P) => N) => (parser: () => Parser<P>): Parser<N> =>
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<PackageNode> => lazy(() =>
obj({
fileName: of(fileName),
Expand Down
56 changes: 47 additions & 9 deletions src/printer/print.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -45,7 +45,8 @@ function print(node: Node, { maxWidth, indentation, abbreviateAssignments }: Pri
type Formatter<T extends Node> = (node: T) => IDoc
type FormatterWithContext<T extends Node> = (context: PrintContext) => Formatter<T>
const format: FormatterWithContext<Node> = 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)),
Expand Down Expand Up @@ -74,6 +75,8 @@ const format: FormatterWithContext<Node> = context => node => {
when(Import)(formatImport),
when(Super)(formatSuper(context)),
)

return enclose(metadata, formattedNode)
}

const formatPackage: FormatterWithContext<Package> = context => node => {
Expand Down Expand Up @@ -102,7 +105,7 @@ const formatMethod: FormatterWithContext<Method> = context => node => {
KEYWORDS.METHOD,
WS,
node.name,
enclosedList(context.nest)(parens, node.parameters.map(formatWithContext)),
enclosedListOfNodes(context)(parens, node.parameters),
]

if(node.isNative()){
Expand Down Expand Up @@ -222,7 +225,7 @@ function isInlineBody(aBody: Body): aBody is Body & { sentences: [Expression] }

const formatNew: FormatterWithContext<New> = context => node => {
const args =
enclosedList(context.nest)(parens, node.args.map(format(context)))
enclosedListOfNodes(context)(parens, node.args)
return [
KEYWORDS.NEW,
WS,
Expand Down Expand Up @@ -316,7 +319,7 @@ const formatParameterizedType: FormatterWithContext<ParameterizedType> =
context => node => [
node.reference.name,
notEmpty(node.args) ?
[WS, enclosedList(context.nest)(parens, node.args.map(format(context)))] :
[WS, enclosedListOfNodes(context)(parens, node.args)] :
[],
]

Expand Down Expand Up @@ -427,7 +430,7 @@ const formatSentences = (context: PrintContext) => (sentences: List<Sentence>, s
return [formatted, formatSentenceInBody(context)(!shouldShortenReturn ? sentence : sentence.value, previousSentence)]
}, [])

const formatArguments = (context: PrintContext) => (args: List<Expression>): IDoc => enclosedList(context.nest)(parens, args.map(format(context)))
const formatArguments = (context: PrintContext) => (args: List<Expression>): IDoc => enclosedListOfNodes(context)(parens, args)

const formatSentenceInBody = (context: PrintContext) => (sentence: Sentence, previousSentence: Sentence | undefined): IDoc => {
const distanceFromLastSentence = (sentence.sourceMap && (!previousSentence || previousSentence.sourceMap) ?
Expand All @@ -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<Field | Method | Test>): IDoc => {
Expand Down Expand Up @@ -487,4 +490,39 @@ const prefixOperatorByMessage: Record<Name, Name> = {
'negate': '!',
'invert': '-',
'plus': '+',
}
}

// metadata
const splitMetadata = (context: PrintContext, metadata: List<Annotation>): [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<Node>): IDoc =>
enclosedList(context.nest)(
enclosers,
nodes.map(format(context)),
nodes.some(aNode => aNode.metadata.some(entry => entry.name ==='comment'))
)
9 changes: 6 additions & 3 deletions src/printer/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}
Expand All @@ -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

Expand Down
29 changes: 12 additions & 17 deletions test/assertions.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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))
})

Expand All @@ -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))
})
}
Expand Down
17 changes: 15 additions & 2 deletions test/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand Down
Loading

0 comments on commit 54a5bfa

Please sign in to comment.