Skip to content

Commit

Permalink
multiline enclosed list + override method
Browse files Browse the repository at this point in the history
  • Loading branch information
ivojawer committed Nov 6, 2023
1 parent a44e16c commit 3fd9211
Show file tree
Hide file tree
Showing 4 changed files with 366 additions and 66 deletions.
137 changes: 85 additions & 52 deletions src/printer/print.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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))
}
Expand Down Expand Up @@ -55,7 +61,8 @@ const format: FormatterWithContext<Node> = 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),
Expand All @@ -78,10 +85,11 @@ const formatProgram: FormatterWithContext<Program> = context => node => interspe
const formatMethod: FormatterWithContext<Method> = 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()){
Expand All @@ -104,7 +112,7 @@ const formatMethod: FormatterWithContext<Method> = context => node => {
}
}

const formatBody: (context: PrintContext) => Formatter<Body> = context => node => body(context.indent)(formatSentences(context)(node.sentences))
const formatBody: (context: PrintContext) => Formatter<Body> = context => node => body(context.nest)(formatSentences(context)(node.sentences))

const formatReturn: FormatterWithContext<Return> = context => node => node.value ?
[KEYWORDS.RETURN, WS, format(context)(node.value)]
Expand All @@ -120,7 +128,7 @@ const formatField: FormatterWithContext<Field> = context => node => {
return [
modifiers,
WS,
formatAssign(context)(node.name, node.value),
formatAssign(context, true)(node.name, node.value),
]
}

Expand All @@ -138,7 +146,7 @@ const formatTest: FormatterWithContext<Test> = 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)),
])
}

Expand All @@ -156,8 +164,8 @@ const formatAssignment: FormatterWithContext<Assignment>= context => node =>

const formatIf: FormatterWithContext<If> = 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,
Expand All @@ -168,7 +176,7 @@ const formatIf: FormatterWithContext<If> = context => node => {

const formatNew: FormatterWithContext<New> = 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,
Expand Down Expand Up @@ -204,18 +212,16 @@ const formatLiteral: FormatterWithContext<Literal> = context => node => {
const formatSelf: Formatter<Self> = (_: Self) => KEYWORDS.SELF

const formatClass: FormatterWithContext<Class> = 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<Mixin> =context => node => {
Expand All @@ -229,20 +235,22 @@ const formatMixin: FormatterWithContext<Mixin> =context => node => {
return [declaration, WS, formatModuleMembers(context)(node.members)]
}

const formatParameterizedType: Formatter<ParameterizedType> = node => node.reference.name
const formatParameterizedType: FormatterWithContext<ParameterizedType> =
context => node => [
node.reference.name,
node.args.length > 0 ?
enclosedList(context.nest)(parens, node.args.map(format(context))) :
[],
]

const formatNamedArgument: FormatterWithContext<NamedArgument> =
context => node => intersperse(WS, [node.name, '=', format(context)(node.value)])

// SINGLETON FORMATTERS

const formatSingleton: FormatterWithContext<Singleton> = 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<Singleton> = context => node => {
Expand All @@ -258,16 +266,30 @@ const formatClosure: FormatterWithContext<Singleton> = context => node => {
: enclose(braces, [parameters, lineBreak, context.indent(formatSentences(context)((applyMethod.body! as Body).sentences)), lineBreak])
}

const formatAnonymousSingleton: FormatterWithContext<Singleton> = context => node => intersperse(WS, [
KEYWORDS.WKO,
formatModuleMembers(context)(node.members),
])
const formatWKO: FormatterWithContext<Singleton> = 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<Singleton | Class> = (context: PrintContext) => node => {
return intersperse(WS, [
KEYWORDS.INHERITS,
listed(node.supertypes.map(format(context))),
])
}

const formatWKO: FormatterWithContext<Singleton> = context => node => intersperse(WS, [
KEYWORDS.WKO,
node.name!,
formatModuleMembers(context)(node.members),
])

// SEND FORMATTERS

Expand All @@ -281,7 +303,7 @@ const formatDotSend: FormatterWithContext<Send> = 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<Send> = context => node => {
Expand Down Expand Up @@ -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<Field | Method | Test>): IDoc => body(context.indent)(intersperse(lineBreaks, members.filter(member => !member.isSynthetic).map(format(context))))
const formatModuleMembers = (context: PrintContext) => (members: List<Field | Method | Test>): 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
Expand Down
23 changes: 13 additions & 10 deletions src/printer/utils.ts
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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)
10 changes: 6 additions & 4 deletions test/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

0 comments on commit 3fd9211

Please sign in to comment.