From 3fd9211dd1cd93374e79d3b09d22e635bc3dde1b Mon Sep 17 00:00:00 2001 From: ivojawer Date: Mon, 6 Nov 2023 07:52:18 -0300 Subject: [PATCH] multiline enclosed list + override method --- src/printer/print.ts | 137 +++++++++++++--------- src/printer/utils.ts | 23 ++-- test/assertions.ts | 10 +- test/printer.test.ts | 262 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 66 deletions(-) diff --git a/src/printer/print.ts b/src/printer/print.ts index 27211af4..932ed4b4 100644 --- a/src/printer/print.ts +++ b/src/printer/print.ts @@ -1,8 +1,8 @@ -import { IDoc, append, braces, choice, enclose, intersperse, lineBreak, lineBreaks, indent as nativeIndent, parens, render, softBreak } from 'prettier-printer' -import { KEYWORDS, LIST_MODULE, OBJECT_MODULE, SET_MODULE } from '../constants' +import { IDoc, append, braces, choice, enclose, intersperse, lineBreak, lineBreaks, indent as nativeIndent, nest as nativeNest, parens, render, softBreak } from 'prettier-printer' +import { KEYWORDS, LIST_MODULE, SET_MODULE } from '../constants' import { List, match, when } from '../extensions' -import { Assignment, Body, Class, Describe, Expression, Field, If, Import, Literal, Method, Mixin, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Sentence, Singleton, Test, Variable } from '../model' -import { Indent, WS, body, enclosedList, infixOperators, listEnclosers, listed, setEnclosers, stringify } from './utils' +import { Assignment, Body, Class, Describe, Expression, Field, If, Import, Literal, Method, Mixin, NamedArgument, New, Node, Package, Parameter, ParameterizedType, Program, Reference, Return, Self, Send, Sentence, Singleton, Test, Variable } from '../model' +import { DocTransformer, WS, body, enclosedList, infixOperators, listEnclosers, listed, setEnclosers, stringify } from './utils' type PrintSettings = { maxWidth: number, @@ -11,21 +11,27 @@ type PrintSettings = { size: number, }, /** - * i.e. `x += 1` instead of `x = x + 1` + * @example `x += 1` abbreviated to `x = x + 1` */ abbreviateAssignments: boolean } type PrintContext = { - indent: Indent, - abbreviateAssignments: boolean + indent: DocTransformer, + nest: DocTransformer, + abbreviateAssignments: boolean, } -export default (node: Node, { maxWidth, indentation, abbreviateAssignments }: PrintSettings): string => { +export default print + + +function print(node: Node, { maxWidth, indentation, abbreviateAssignments }: PrintSettings): string { + const indentationCharacters = (indentation.useSpaces ? ' ' : '\t').repeat(indentation.size) return render( maxWidth, format({ - indent: nativeIndent((indentation.useSpaces ? ' ' : '\t').repeat(indentation.size)), + indent: nativeIndent(indentationCharacters), + nest: nativeNest(indentationCharacters), abbreviateAssignments, })(node)) } @@ -55,7 +61,8 @@ const format: FormatterWithContext = context => node => { when(Send)(formatSend(context)), when(If)(formatIf(context)), when(New)(formatNew(context)), - when(ParameterizedType)(formatParameterizedType), + when(ParameterizedType)(formatParameterizedType(context)), + when(NamedArgument)(formatNamedArgument(context)), when(Return)(formatReturn(context)), when(Reference)(formatReference), when(Self)(formatSelf), @@ -78,10 +85,11 @@ const formatProgram: FormatterWithContext = context => node => interspe const formatMethod: FormatterWithContext = context => node => { const formatWithContext = format(context) const signature = [ + node.isOverride ? [KEYWORDS.OVERRIDE, WS] : [], KEYWORDS.METHOD, WS, node.name, - enclosedList(context.indent)(parens, node.parameters.map(formatWithContext)), + enclosedList(context.nest)(parens, node.parameters.map(formatWithContext)), ] if(node.isNative()){ @@ -104,7 +112,7 @@ const formatMethod: FormatterWithContext = context => node => { } } -const formatBody: (context: PrintContext) => Formatter = context => node => body(context.indent)(formatSentences(context)(node.sentences)) +const formatBody: (context: PrintContext) => Formatter = context => node => body(context.nest)(formatSentences(context)(node.sentences)) const formatReturn: FormatterWithContext = context => node => node.value ? [KEYWORDS.RETURN, WS, format(context)(node.value)] @@ -120,7 +128,7 @@ const formatField: FormatterWithContext = context => node => { return [ modifiers, WS, - formatAssign(context)(node.name, node.value), + formatAssign(context, true)(node.name, node.value), ] } @@ -138,7 +146,7 @@ const formatTest: FormatterWithContext = context => node => { return intersperse(WS, [ KEYWORDS.TEST, node.name, - body(context.indent)(formatSentences(context)(node.body.sentences)), + body(context.nest)(formatSentences(context)(node.body.sentences)), ]) } @@ -156,8 +164,8 @@ const formatAssignment: FormatterWithContext= context => node => const formatIf: FormatterWithContext = context => node => { const condition = [KEYWORDS.IF, WS, enclose(parens, format(context)(node.condition))] - const thenBody = body(context.indent)(formatSentences(context)(node.thenBody.sentences)) - const elseBody = node.elseBody.sentences.length > 0 ? body(context.indent)(formatSentences(context)(node.elseBody.sentences)) : undefined + const thenBody = body(context.nest)(formatSentences(context)(node.thenBody.sentences)) + const elseBody = node.elseBody.sentences.length > 0 ? body(context.nest)(formatSentences(context)(node.elseBody.sentences)) : undefined return [ condition, WS, @@ -168,7 +176,7 @@ const formatIf: FormatterWithContext = context => node => { const formatNew: FormatterWithContext = context => node => { const args = - enclosedList(context.indent)(parens, node.args.map(arg => intersperse(WS, [arg.name, '=', format(context)(arg.value)]))) + enclosedList(context.nest)(parens, node.args.map(arg => intersperse(WS, [arg.name, '=', format(context)(arg.value)]))) return [ KEYWORDS.NEW, WS, @@ -204,18 +212,16 @@ const formatLiteral: FormatterWithContext = context => node => { const formatSelf: Formatter = (_: Self) => KEYWORDS.SELF const formatClass: FormatterWithContext = context => node => { - const header = [ + let header: IDoc = [ KEYWORDS.CLASS, - WS, node.name, - node.superclass && node.superclass?.fullyQualifiedName !== OBJECT_MODULE ? [WS, KEYWORDS.INHERITS, WS, node.superclass.name] : [], ] - return [ - header, - WS, - formatModuleMembers(context)(node.members), - ] + if(inherits(node)){ + header = [...header, formatInheritance(context)(node)] + } + + return intersperse(WS, [...header, formatModuleMembers(context)(node.members)]) } const formatMixin: FormatterWithContext =context => node => { @@ -229,20 +235,22 @@ const formatMixin: FormatterWithContext =context => node => { return [declaration, WS, formatModuleMembers(context)(node.members)] } -const formatParameterizedType: Formatter = node => node.reference.name +const formatParameterizedType: FormatterWithContext = + context => node => [ + node.reference.name, + node.args.length > 0 ? + enclosedList(context.nest)(parens, node.args.map(format(context))) : + [], + ] + +const formatNamedArgument: FormatterWithContext = + context => node => intersperse(WS, [node.name, '=', format(context)(node.value)]) // SINGLETON FORMATTERS const formatSingleton: FormatterWithContext = context => (node: Singleton) => { - if(node.name){ - return formatWKO(context)(node) - } else { - if(node.isClosure()){ - return formatClosure(context)(node) - } else { - return formatAnonymousSingleton(context)(node) - } - } + const formatter = node.isClosure() ? formatClosure : formatWKO + return formatter(context)(node) } const formatClosure: FormatterWithContext = context => node => { @@ -258,16 +266,30 @@ const formatClosure: FormatterWithContext = context => node => { : enclose(braces, [parameters, lineBreak, context.indent(formatSentences(context)((applyMethod.body! as Body).sentences)), lineBreak]) } -const formatAnonymousSingleton: FormatterWithContext = context => node => intersperse(WS, [ - KEYWORDS.WKO, - formatModuleMembers(context)(node.members), -]) +const formatWKO: FormatterWithContext = context => node => { + const members = formatModuleMembers(context)(node.members) + let formatted: IDoc = [KEYWORDS.WKO] + + if(node.name){ + formatted = [...formatted, node.name] + } + + if(inherits(node)){ + formatted = [...formatted, formatInheritance(context)(node)] + } + + return intersperse(WS, [...formatted, members]) +} + +const inherits = (node: Singleton | Class) => node.supertypes.length > 0 + +const formatInheritance: FormatterWithContext = (context: PrintContext) => node => { + return intersperse(WS, [ + KEYWORDS.INHERITS, + listed(node.supertypes.map(format(context))), + ]) +} -const formatWKO: FormatterWithContext = context => node => intersperse(WS, [ - KEYWORDS.WKO, - node.name!, - formatModuleMembers(context)(node.members), -]) // SEND FORMATTERS @@ -281,7 +303,7 @@ const formatDotSend: FormatterWithContext = context => node => [ format(context)(node.receiver), '.', node.message, - enclosedList(context.indent)(parens, node.args.map(format(context))), + enclosedList(context.nest)(parens, node.args.map(format(context))), ] const formatInfixSend: FormatterWithContext = context => node => { @@ -311,19 +333,30 @@ const formatSentenceInBody = (context: PrintContext) => (sentence: Sentence, pre return [Array(distanceFromLastSentence + 1).fill(lineBreak), format(context)(sentence)] } -const formatAssign = (context: PrintContext) => (name: string, value: Expression, assignmentOperator = '=') => [ +const formatAssign = (context: PrintContext, ignoreNull = false) => (name: string, value: Expression, assignmentOperator = '=') => [ name, - WS, - assignmentOperator, - [softBreak, choice(WS, context.indent([]))], - format(context)(value), + // ToDo: diffentiate `var x` from `var x = null` + ignoreNull && value.is(Literal) && value.isNull() ? + [] : + [ + WS, + assignmentOperator, + [softBreak, choice(WS, context.indent([]))], + format(context)(value), + ], ] const formatCollection = (context: PrintContext) => (values: Expression[], enclosers: [IDoc, IDoc]) => { - return enclosedList(context.indent)(enclosers, values.map(format(context))) + return enclosedList(context.nest)(enclosers, values.map(format(context))) } -const formatModuleMembers = (context: PrintContext) => (members: List): IDoc => body(context.indent)(intersperse(lineBreaks, members.filter(member => !member.isSynthetic).map(format(context)))) +const formatModuleMembers = (context: PrintContext) => (members: List): IDoc => { + const formatter = format(context) + const concreteMembers = members.filter(member => !member.isSynthetic) + const fields = concreteMembers.filter(member => member.is(Field)).map(formatter) + const others = concreteMembers.filter(member => !member.is(Field)).map(formatter) + return body(context.nest)([fields.length > 0 ? [intersperse(lineBreak, fields), others.length > 0 ? lineBreaks : []] : [], intersperse(lineBreaks, others)]) +} // assignment operations const canBeAbbreviated = (node: Assignment): node is Assignment & {value: Send & {message: keyof typeof assignmentOperationByMessage}} => node.value.is(Send) && node.value.receiver.is(Reference) && node.value.receiver.name === node.variable.name && node.value.message in assignmentOperationByMessage diff --git a/src/printer/utils.ts b/src/printer/utils.ts index 1110f363..237b8732 100644 --- a/src/printer/utils.ts +++ b/src/printer/utils.ts @@ -1,7 +1,7 @@ -import { IDoc, braces, brackets, choice, dquotes, enclose, intersperse, lineBreak, softLine } from 'prettier-printer' +import { IDoc, append, braces, brackets, choice, dquotes, enclose, intersperse, lineBreak, softLine } from 'prettier-printer' import { INFIX_OPERATORS } from '../constants' -export type Indent = (doc: IDoc) => IDoc +export type DocTransformer = (doc: IDoc) => IDoc export const infixOperators = INFIX_OPERATORS.flat() @@ -10,22 +10,25 @@ type Encloser = [IDoc, IDoc] export const listEnclosers: Encloser = brackets export const setEnclosers: Encloser = ['#{', '}'] -export const WS: IDoc = ' ' +export const WS = ' ' as IDoc -export const body = (indent: Indent) => (content: IDoc): IDoc => enclose(braces, [lineBreak, indent([content]), lineBreak]) +export const body = (nest: DocTransformer) => (content: IDoc): IDoc => encloseIndented(braces, content, nest) /** - * Formats list of strings to "string1, string2, string3" spreading it over multiple lines when needed + * Formats a list of documents to "doc1, doc2, doc3" spreading it over multiple lines when needed */ export const listed = (contents: IDoc[], separator: IDoc = ','): IDoc => intersperse([separator, softLine], contents) -export const enclosedList = (indent: Indent) => (enclosers: [IDoc, IDoc], content: IDoc[], separator: IDoc = ','): IDoc => { - return enclose(enclosers)( +export const enclosedList = (nest: DocTransformer) => (enclosers: [IDoc, IDoc], content: IDoc[], separator: IDoc = ','): IDoc => + enclose( + enclosers, choice( - listed(content, separator), - content.length > 0 ? [lineBreak, indent(listed(content, separator)), lineBreak] : [] + intersperse([separator, WS], content), + encloseIndented(['', ''], intersperse([separator, lineBreak], content), nest) ) ) -} + +export const encloseIndented = (enclosers: [IDoc, IDoc], content: IDoc, nest: DocTransformer): IDoc => + enclose(enclosers, append(lineBreak, nest([lineBreak, content]))) export const stringify = enclose(dquotes) \ No newline at end of file diff --git a/test/assertions.ts b/test/assertions.ts index 6734ec55..b4343701 100644 --- a/test/assertions.ts +++ b/test/assertions.ts @@ -6,10 +6,11 @@ import { Validation } from '../src/validator' import { File, ParseError } from '../src/parser' import globby from 'globby' import { promises } from 'fs' -import { buildEnvironment as buildEnv, print } from '../src' +import { buildEnvironment as buildEnv, print, WRE } from '../src' import { join } from 'path' import validate from '../src/validator' import dedent from 'dedent' +import { fromJSON } from '../src/jsonUtils' const { readFile } = promises @@ -121,11 +122,12 @@ export const printerAssertions: Chai.ChaiPlugin = (chai) => { const { Assertion } = chai Assertion.addMethod('formattedTo', function (expected: string) { - const parsed = File('formatted').parse(this._obj) + 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 printerConfig = { maxWidth: 80, indentation: { size: 2, useSpaces: true }, abbreviateAssignments: true } - const formatted = print(parsed.value, printerConfig) + const formatted = print(environment.getNodeByFQN(fileName), printerConfig) new Assertion(formatted).to.equal(dedent(expected)) }) } diff --git a/test/printer.test.ts b/test/printer.test.ts index ea1250bc..000b8a95 100644 --- a/test/printer.test.ts +++ b/test/printer.test.ts @@ -5,6 +5,55 @@ use(printerAssertions) should() describe('Wollok Printer', () => { + describe('Basic expressions', () => { + describe('Send', () => { + it('Send long parameters', () => { + `object pepita { + method volar(a,b,c,d,e){} + + method prueba() { + self.volar("aaaaaaaaaaaa", "bbbbbbbbbbb", "cccccccccc", "dddddddddd", "eeeeeeeeee") + } + }`.should.be.formattedTo(` + object pepita { + method volar(a, b, c, d, e) { + + } + + method prueba() { + self.volar( + "aaaaaaaaaaaa", + "bbbbbbbbbbb", + "cccccccccc", + "dddddddddd", + "eeeeeeeeee" + ) + } + } + `) + }) + it('Send short parameters', () => { + `object pepita { + method volar(a,b){} + + method prueba() { + self.volar("aaaaaaaaaaaa", + "bbbbbbbbbbb") + } + }`.should.be.formattedTo(` + object pepita { + method volar(a, b) { + + } + + method prueba() { + self.volar("aaaaaaaaaaaa", "bbbbbbbbbbb") + } + } + `) + }) + }) + }) describe('Object definitions', () => { it('testBasicObjectDefinition', () => { `object pepita { var energia = 0 @@ -40,5 +89,218 @@ describe('Wollok Printer', () => { } `) }) + + it('testInheritingPositionalUnnamedObjectDefinition', () => { + ` + class A { var _n = 0 } + program prueba { + + const pepita = object inherits A(n= + + 5 + + ) + {var energia = + 0 + method volar() {energia+=1 } + } + } + `.should.be.formattedTo(` + class A { + var _n = 0 + } + + program prueba { + const pepita = object inherits A(n = 5) { + var energia = 0 + + method volar() { + energia += 1 + } + } + } + ` + ) + }) + + it('testInheritingNamedParametersUnnamedObjectDefinition', () => { + + ` + class A { var edad = 0 var nombre = ""} + program prueba { + + const pepita = object inherits A( + + + + edad = 22 + + + , + + + nombre + + + = + "Carlono" + ) + {var energia = + 0 + method volar() {energia+=1 } + } + } + `.should.be.formattedTo(` + class A { + var edad = 0 + var nombre = "" + } + + program prueba { + const pepita = object inherits A(edad = 22, nombre = "Carlono") { + var energia = 0 + + method volar() { + energia += 1 + } + } + } + ` + ) + }) + + it('testUnnamedObjectDefinitionInAnExpression', () => { + ` + program prueba { + + assert.equals( + + object { var energia + = 0}, object { + var energia = 0 + var color = "rojo" } + ) + }`.should.be.formattedTo(` + program prueba { + assert.equals( + object { + var energia = 0 + }, + object { + var energia = 0 + var color = "rojo" + } + ) + }`) + }) + + it('testInheritingPositionalParametersObjectDefinition', () => { + `class Ave{} + object pepita + inherits + Ave { + var energia = 0 + method volar() { energia = energia +10 } + } + `.should.be.formattedTo(` + class Ave { + + } + + object pepita inherits Ave { + var energia = 0 + + method volar() { + energia += 10 + } + }`) + }) + + it('classFormatting_oneLineBetweenVarsAndMethods', () => { + `class Golondrina { + const energia = 10 + const kmRecorridos = 0 + method comer(gr){energia=energia+gr} method jugar(){ return true }}`.should.be.formattedTo(` + class Golondrina { + const energia = 10 + const kmRecorridos = 0 + + method comer(gr) { + energia += gr + } + + method jugar() = true + }`) + }) + + it('testObjectDefinitionWithOneVariableOnly', () => { + ` + object pepita { + + + var + + z } + `.should.be.formattedTo(` + object pepita { + var z + }`) + }) + + it('testObjectDefinitionWithOneMethodOnly', () => { + ` + object pepita + + + { method volar() { return 2 } } + `.should.be.formattedTo(` + object pepita { + method volar() = 2 + }`) + }) + + it('testClassDefinitionWithOneMethodOnly', () => { + ` + class Ave + + + { method volar() { return 2 } }`.should.be.formattedTo(` + class Ave { + method volar() = 2 + }`) + }) + + it('testInheritingObjectDefinitionWithDefinitionItself', () => { + `class Ave{method volar() {} + + } + object pepita + + + + inherits + Ave + + + { var energia = 0 + + + override method volar() { energia += + 10 }}`.should.be.formattedTo(` + class Ave { + method volar() { + + } + } + + object pepita inherits Ave { + var energia = 0 + + override method volar() { + energia += 10 + } + } + `) + }) }) }) \ No newline at end of file