diff --git a/src/common/printer-helpers.js b/src/common/printer-helpers.js index bfd6f146f..74a6a180c 100644 --- a/src/common/printer-helpers.js +++ b/src/common/printer-helpers.js @@ -5,19 +5,33 @@ const { isNextLineEmpty } = util; export const printComments = (node, path, options, filter = () => true) => { if (!node.comments) return ''; + function isPrintable(comment) { + return ( + !comment.trailing && + !comment.leading && + !comment.printed && + filter(comment) + ); + } const document = join( line, path - .map((commentPath) => { + .map((commentPath, index, comments) => { const comment = commentPath.getValue(); - if (comment.trailing || comment.leading || comment.printed) { - return null; - } - if (!filter(comment)) { + if (!isPrintable(comment)) { return null; } comment.printed = true; - return options.printer.printComment(commentPath, options); + const isLast = + index === comments.length - 1 || + comments.slice(index + 1).findIndex(isPrintable) === -1; + return [ + options.printer.printComment(commentPath, options), + !isLast && + util.isNextLineEmpty(options.originalText, options.locEnd(comment)) + ? hardline + : '' + ]; }, 'comments') .filter(Boolean) ); diff --git a/src/nodes/SourceUnit.js b/src/nodes/SourceUnit.js index 413f97393..604cd3f31 100644 --- a/src/nodes/SourceUnit.js +++ b/src/nodes/SourceUnit.js @@ -1,11 +1,15 @@ import { doc } from 'prettier'; -import { printPreservingEmptyLines } from '../common/printer-helpers.js'; +import { + printComments, + printPreservingEmptyLines +} from '../common/printer-helpers.js'; const { line } = doc.builders; export const SourceUnit = { - print: ({ options, path, print }) => [ + print: ({ node, options, path, print }) => [ printPreservingEmptyLines(path, 'children', options, print), + printComments(node, path, options), options.parentParser ? '' : line ] }; diff --git a/src/slang-comments/handlers/handle-source-unit-members-comments.ts b/src/slang-comments/handlers/handle-source-unit-members-comments.ts new file mode 100644 index 000000000..7b38df78a --- /dev/null +++ b/src/slang-comments/handlers/handle-source-unit-members-comments.ts @@ -0,0 +1,28 @@ +import { NonterminalKind } from '@nomicfoundation/slang/cst'; +import addCollectionFirstComment from './add-collection-first-comment.js'; +import addCollectionLastComment from './add-collection-last-comment.js'; + +import type { HandlerParams } from './types.js'; + +export default function handleSourceUnitMembersComments({ + precedingNode, + followingNode, + comment +}: HandlerParams): boolean { + if ( + followingNode && + followingNode.kind === NonterminalKind.SourceUnitMembers + ) { + addCollectionFirstComment(followingNode, comment); + return true; + } + if ( + precedingNode && + precedingNode.kind === NonterminalKind.SourceUnitMembers + ) { + addCollectionLastComment(precedingNode, comment); + return true; + } + + return false; +} diff --git a/src/slang-comments/handlers/index.ts b/src/slang-comments/handlers/index.ts index dbc5f4c92..901d0a767 100644 --- a/src/slang-comments/handlers/index.ts +++ b/src/slang-comments/handlers/index.ts @@ -8,6 +8,7 @@ import handleLibraryDefinitionComments from './handle-library-definition-comment import handleModifierInvocationComments from './handle-modifier-invocation-comments.js'; import handleParametersDeclarationComments from './handle-parameters-declaration-comments.js'; import handlePositionalArgumentsDeclarationComments from './handle-positional-arguments-declaration-comments.js'; +import handleSourceUnitMembersComments from './handle-source-unit-members-comments.js'; import handleStorageLayoutSpecifierComments from './handle-storage-layout-specifier-comments.js'; import handleStructComments from './handle-struct-comments.js'; import handleWhileStatementComments from './handle-while-statement-comments.js'; @@ -24,6 +25,7 @@ export default [ handleModifierInvocationComments, handleParametersDeclarationComments, handlePositionalArgumentsDeclarationComments, + handleSourceUnitMembersComments, handleStorageLayoutSpecifierComments, handleStructComments, handleWhileStatementComments, diff --git a/src/slang-nodes/AddressType.ts b/src/slang-nodes/AddressType.ts index 735bb13cb..86862359a 100644 --- a/src/slang-nodes/AddressType.ts +++ b/src/slang-nodes/AddressType.ts @@ -1,5 +1,4 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; -import { joinExisting } from '../slang-utils/join-existing.js'; import { SlangNode } from './SlangNode.js'; import type * as ast from '@nomicfoundation/slang/ast'; @@ -17,6 +16,6 @@ export class AddressType extends SlangNode { } print(): Doc { - return joinExisting(' ', ['address', this.payableKeyword]); + return ['address', this.payableKeyword ? ' payable' : '']; } } diff --git a/src/slang-nodes/ArrayExpression.ts b/src/slang-nodes/ArrayExpression.ts index 0f7bb46f2..27ac10d45 100644 --- a/src/slang-nodes/ArrayExpression.ts +++ b/src/slang-nodes/ArrayExpression.ts @@ -1,4 +1,3 @@ -import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { SlangNode } from './SlangNode.js'; import { ArrayValues } from './ArrayValues.js'; @@ -8,8 +7,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group } = doc.builders; - export class ArrayExpression extends SlangNode { readonly kind = NonterminalKind.ArrayExpression; @@ -24,6 +21,6 @@ export class ArrayExpression extends SlangNode { } print(path: AstPath, print: PrintFunction): Doc { - return group(['[', path.call(print, 'items'), ']']); + return ['[', path.call(print, 'items'), ']']; } } diff --git a/src/slang-nodes/AssignmentExpression.ts b/src/slang-nodes/AssignmentExpression.ts index 1fdc186d1..38aa55e19 100644 --- a/src/slang-nodes/AssignmentExpression.ts +++ b/src/slang-nodes/AssignmentExpression.ts @@ -1,6 +1,6 @@ import { NonterminalKind, TerminalKind } from '@nomicfoundation/slang/cst'; -import { doc } from 'prettier'; import { isBinaryOperation } from '../slang-utils/is-binary-operation.js'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; @@ -9,8 +9,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; - export class AssignmentExpression extends SlangNode { readonly kind = NonterminalKind.AssignmentExpression; @@ -32,14 +30,14 @@ export class AssignmentExpression extends SlangNode { print(path: AstPath, print: PrintFunction): Doc { const rightOperandVariant = this.rightOperand.variant; - const rightOperand = path.call(print, 'rightOperand'); return [ path.call(print, 'leftOperand'), ` ${this.operator}`, - rightOperandVariant.kind !== TerminalKind.Identifier && - isBinaryOperation(rightOperandVariant) - ? group(indent([line, rightOperand])) - : [' ', rightOperand] + printIndentedGroupOrSpacedDocument( + path.call(print, 'rightOperand'), + rightOperandVariant.kind !== TerminalKind.Identifier && + isBinaryOperation(rightOperandVariant) + ) ]; } } diff --git a/src/slang-nodes/ContractMembers.ts b/src/slang-nodes/ContractMembers.ts index cd82c186c..90ddcde58 100644 --- a/src/slang-nodes/ContractMembers.ts +++ b/src/slang-nodes/ContractMembers.ts @@ -1,7 +1,6 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; -import { printComments } from '../slang-printers/print-comments.js'; import { printPreservingEmptyLines } from '../slang-printers/print-preserving-empty-lines.js'; import { SlangNode } from './SlangNode.js'; import { ContractMember } from './ContractMember.js'; @@ -31,14 +30,10 @@ export class ContractMembers extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return this.items.length === 0 && this.comments.length === 0 - ? '' - : printSeparatedItem( - [ - printPreservingEmptyLines(path, print, options), - printComments(path) - ], - { firstSeparator: hardline, grouped: false } - ); + return this.items.length > 0 || this.comments.length > 0 + ? printSeparatedItem(printPreservingEmptyLines(path, print, options), { + firstSeparator: hardline + }) + : ''; } } diff --git a/src/slang-nodes/ElseBranch.ts b/src/slang-nodes/ElseBranch.ts index 4c00ea025..3f7e1b074 100644 --- a/src/slang-nodes/ElseBranch.ts +++ b/src/slang-nodes/ElseBranch.ts @@ -1,6 +1,6 @@ -import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { createKindCheckFunction } from '../slang-utils/create-kind-check-function.js'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { Statement } from './Statement.js'; @@ -9,8 +9,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; - const isIfStatementOrBlock = createKindCheckFunction([ NonterminalKind.Block, NonterminalKind.IfStatement @@ -30,12 +28,12 @@ export class ElseBranch extends SlangNode { } print(path: AstPath, print: PrintFunction): Doc { - const body = path.call(print, 'body'); return [ 'else', - isIfStatementOrBlock(this.body.variant) - ? [' ', body] - : group(indent([line, body])) + printIndentedGroupOrSpacedDocument( + path.call(print, 'body'), + !isIfStatementOrBlock(this.body.variant) + ) ]; } } diff --git a/src/slang-nodes/EventDefinition.ts b/src/slang-nodes/EventDefinition.ts index 77837800e..f981bece9 100644 --- a/src/slang-nodes/EventDefinition.ts +++ b/src/slang-nodes/EventDefinition.ts @@ -32,8 +32,7 @@ export class EventDefinition extends SlangNode { 'event ', path.call(print, 'name'), path.call(print, 'parameters'), - this.anonymousKeyword ? ' anonymous' : '', - ';' + this.anonymousKeyword ? ' anonymous;' : ';' ]; } } diff --git a/src/slang-nodes/ForStatement.ts b/src/slang-nodes/ForStatement.ts index 5e682e3aa..4364494a1 100644 --- a/src/slang-nodes/ForStatement.ts +++ b/src/slang-nodes/ForStatement.ts @@ -1,6 +1,7 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { doc } from 'prettier'; import { printSeparatedList } from '../slang-printers/print-separated-list.js'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { ForStatementInitialization } from './ForStatementInitialization.js'; import { ForStatementCondition } from './ForStatementCondition.js'; @@ -12,7 +13,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; +const { line } = doc.builders; export class ForStatement extends SlangNode { readonly kind = NonterminalKind.ForStatement; @@ -50,7 +51,6 @@ export class ForStatement extends SlangNode { const initialization = path.call(print, 'initialization'); const condition = path.call(print, 'condition'); const iterator = path.call(print, 'iterator'); - const body = path.call(print, 'body'); return [ 'for (', @@ -61,9 +61,10 @@ export class ForStatement extends SlangNode { : '' }), ')', - this.body.variant.kind === NonterminalKind.Block - ? [' ', body] - : group(indent([line, body])) + printIndentedGroupOrSpacedDocument( + path.call(print, 'body'), + this.body.variant.kind !== NonterminalKind.Block + ) ]; } } diff --git a/src/slang-nodes/FunctionCallExpression.ts b/src/slang-nodes/FunctionCallExpression.ts index a115160ab..8451b4f10 100644 --- a/src/slang-nodes/FunctionCallExpression.ts +++ b/src/slang-nodes/FunctionCallExpression.ts @@ -1,6 +1,7 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { isLabel } from '../slang-utils/is-label.js'; +import { printGroupAndIndentIfBreakPair } from '../slang-printers/print-group-and-indent-if-break-pair.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; import { ArgumentsDeclaration } from './ArgumentsDeclaration.js'; @@ -10,7 +11,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indentIfBreak, label } = doc.builders; +const { label } = doc.builders; export class FunctionCallExpression extends SlangNode { readonly kind = NonterminalKind.FunctionCallExpression; @@ -38,13 +39,12 @@ export class FunctionCallExpression extends SlangNode { // If we are at the end of a MemberAccessChain we should indent the // arguments accordingly. if (isLabel(operand) && operand.label === 'MemberAccessChain') { - const groupId = Symbol('Slang.FunctionCallExpression.operand'); // We wrap the expression in a label in case there is an IndexAccess or // a FunctionCall following this IndexAccess. - return label('MemberAccessChain', [ - group(operand.contents, { id: groupId }), - indentIfBreak(argumentsDoc, { groupId }) - ]); + return label( + 'MemberAccessChain', + printGroupAndIndentIfBreakPair(operand.contents, argumentsDoc) + ); } return [operand, argumentsDoc].flat(); diff --git a/src/slang-nodes/IfStatement.ts b/src/slang-nodes/IfStatement.ts index 9f7ca7b36..0ffa8dd60 100644 --- a/src/slang-nodes/IfStatement.ts +++ b/src/slang-nodes/IfStatement.ts @@ -1,6 +1,7 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { isBlockComment } from '../slang-utils/is-comment.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; @@ -12,7 +13,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, hardline, indent, line } = doc.builders; +const { hardline } = doc.builders; export class IfStatement extends SlangNode { readonly kind = NonterminalKind.IfStatement; @@ -37,16 +38,16 @@ export class IfStatement extends SlangNode { print(path: AstPath, print: PrintFunction): Doc { const { kind: bodyKind, comments: bodyComments } = this.body.variant; - const body = path.call(print, 'body'); return [ 'if (', printSeparatedItem(path.call(print, 'condition')), ')', - bodyKind === NonterminalKind.Block - ? [' ', body] - : group(indent([line, body]), { - shouldBreak: bodyKind === NonterminalKind.IfStatement // `if` within `if` - }), + printIndentedGroupOrSpacedDocument( + path.call(print, 'body'), + bodyKind !== NonterminalKind.Block, + // `if` within `if` + { shouldBreak: bodyKind === NonterminalKind.IfStatement } + ), this.elseBranch ? [ bodyKind !== NonterminalKind.Block || // else on a new line if body is not a block diff --git a/src/slang-nodes/ImportDeconstructionSymbols.ts b/src/slang-nodes/ImportDeconstructionSymbols.ts index a5ebf0f7b..bcdb32dee 100644 --- a/src/slang-nodes/ImportDeconstructionSymbols.ts +++ b/src/slang-nodes/ImportDeconstructionSymbols.ts @@ -37,8 +37,7 @@ export class ImportDeconstructionSymbols extends SlangNode { ? { // if the compiler exists and is greater than or equal to 0.7.4 we will // split the ImportDirective. - firstSeparator: bracketSpacing ? line : softline, - separator: [',', line] + firstSeparator: bracketSpacing ? line : softline } : { // if the compiler is not given or is lower than 0.7.4 we will not diff --git a/src/slang-nodes/IndexAccessExpression.ts b/src/slang-nodes/IndexAccessExpression.ts index 1ba396516..8dc39f95a 100644 --- a/src/slang-nodes/IndexAccessExpression.ts +++ b/src/slang-nodes/IndexAccessExpression.ts @@ -1,6 +1,7 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; +import { printGroupAndIndentIfBreakPair } from '../slang-printers/print-group-and-indent-if-break-pair.js'; import { isLabel } from '../slang-utils/is-label.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; @@ -11,7 +12,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indentIfBreak, label } = doc.builders; +const { label } = doc.builders; export class IndexAccessExpression extends SlangNode { readonly kind = NonterminalKind.IndexAccessExpression; @@ -47,13 +48,12 @@ export class IndexAccessExpression extends SlangNode { // If we are at the end of a MemberAccessChain we should indent the // arguments accordingly. if (isLabel(operand) && operand.label === 'MemberAccessChain') { - const groupId = Symbol('Slang.IndexAccessExpression.operand'); // We wrap the expression in a label in case there is an IndexAccess or // a FunctionCall following this IndexAccess. - return label('MemberAccessChain', [ - group(operand.contents, { id: groupId }), - indentIfBreak(indexDoc, { groupId }) - ]); + return label( + 'MemberAccessChain', + printGroupAndIndentIfBreakPair(operand.contents, indexDoc) + ); } return [operand, indexDoc].flat(); diff --git a/src/slang-nodes/InterfaceMembers.ts b/src/slang-nodes/InterfaceMembers.ts index 6ee398a26..657078bc4 100644 --- a/src/slang-nodes/InterfaceMembers.ts +++ b/src/slang-nodes/InterfaceMembers.ts @@ -30,10 +30,9 @@ export class InterfaceMembers extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return this.items.length > 0 + return this.items.length > 0 || this.comments.length > 0 ? printSeparatedItem(printPreservingEmptyLines(path, print, options), { - firstSeparator: hardline, - grouped: false + firstSeparator: hardline }) : ''; } diff --git a/src/slang-nodes/LibraryMembers.ts b/src/slang-nodes/LibraryMembers.ts index 92ccfd0ed..8cb97a6b0 100644 --- a/src/slang-nodes/LibraryMembers.ts +++ b/src/slang-nodes/LibraryMembers.ts @@ -30,10 +30,9 @@ export class LibraryMembers extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return this.items.length > 0 + return this.items.length > 0 || this.comments.length > 0 ? printSeparatedItem(printPreservingEmptyLines(path, print, options), { - firstSeparator: hardline, - grouped: false + firstSeparator: hardline }) : ''; } diff --git a/src/slang-nodes/Parameters.ts b/src/slang-nodes/Parameters.ts index 9d95fabd7..9c32afa3e 100644 --- a/src/slang-nodes/Parameters.ts +++ b/src/slang-nodes/Parameters.ts @@ -23,12 +23,16 @@ export class Parameters extends SlangNode { this.updateMetadata(this.items); } - print(path: AstPath, print: PrintFunction): Doc { + print( + path: AstPath, + print: PrintFunction, + options: ParserOptions + ): Doc { if (this.items.length > 0) { return printSeparatedList(path.map(print, 'items'), { grouped: false }); } - const parameterComments = printComments(path); + const parameterComments = printComments(path, options); return parameterComments.length > 0 ? printSeparatedItem(parameterComments) diff --git a/src/slang-nodes/PositionalArguments.ts b/src/slang-nodes/PositionalArguments.ts index 51a198e73..c56346575 100644 --- a/src/slang-nodes/PositionalArguments.ts +++ b/src/slang-nodes/PositionalArguments.ts @@ -23,11 +23,15 @@ export class PositionalArguments extends SlangNode { this.updateMetadata(this.items); } - print(path: AstPath, print: PrintFunction): Doc { + print( + path: AstPath, + print: PrintFunction, + options: ParserOptions + ): Doc { if (this.items.length > 0) { return printSeparatedList(path.map(print, 'items')); } - const argumentComments = printComments(path); + const argumentComments = printComments(path, options); return argumentComments.length > 0 ? printSeparatedItem(argumentComments) diff --git a/src/slang-nodes/ReturnStatement.ts b/src/slang-nodes/ReturnStatement.ts index 2d729aa47..8c8bf301f 100644 --- a/src/slang-nodes/ReturnStatement.ts +++ b/src/slang-nodes/ReturnStatement.ts @@ -1,5 +1,5 @@ -import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; @@ -8,26 +8,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; - -function printExpression( - node: ReturnStatement, - path: AstPath, - print: PrintFunction, - options: ParserOptions -): Doc { - const expressionVariantKind = node.expression?.variant.kind; - const expression = path.call(print, 'expression'); - if (expressionVariantKind) { - return expressionVariantKind === NonterminalKind.TupleExpression || - (options.experimentalTernaries && - expressionVariantKind === NonterminalKind.ConditionalExpression) - ? [' ', expression] - : group(indent([line, expression])); - } - return ''; -} - export class ReturnStatement extends SlangNode { readonly kind = NonterminalKind.ReturnStatement; @@ -48,6 +28,18 @@ export class ReturnStatement extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return ['return', printExpression(this, path, print, options), ';']; + const expressionVariantKind = this.expression?.variant.kind; + return [ + 'return', + expressionVariantKind + ? printIndentedGroupOrSpacedDocument( + path.call(print, 'expression'), + expressionVariantKind !== NonterminalKind.TupleExpression && + (!options.experimentalTernaries || + expressionVariantKind !== NonterminalKind.ConditionalExpression) + ) + : '', + ';' + ]; } } diff --git a/src/slang-nodes/StateVariableDefinition.ts b/src/slang-nodes/StateVariableDefinition.ts index a8b406716..49bb4e628 100644 --- a/src/slang-nodes/StateVariableDefinition.ts +++ b/src/slang-nodes/StateVariableDefinition.ts @@ -1,5 +1,6 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { doc } from 'prettier'; +import { printGroupAndIndentIfBreakPair } from '../slang-printers/print-group-and-indent-if-break-pair.js'; import { SlangNode } from './SlangNode.js'; import { TypeName } from './TypeName.js'; import { StateVariableAttributes } from './StateVariableAttributes.js'; @@ -11,7 +12,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, indentIfBreak } = doc.builders; +const { indent } = doc.builders; export class StateVariableDefinition extends SlangNode { readonly kind = NonterminalKind.StateVariableDefinition; @@ -41,14 +42,14 @@ export class StateVariableDefinition extends SlangNode { } print(path: AstPath, print: PrintFunction): Doc { - const groupId = Symbol('Slang.StateVariableDefinition.attributes'); - return [ - path.call(print, 'typeName'), - group(indent(path.call(print, 'attributes')), { id: groupId }), - ' ', - path.call(print, 'name'), - this.value ? indentIfBreak(path.call(print, 'value'), { groupId }) : '', - ';' - ]; + return printGroupAndIndentIfBreakPair( + [ + path.call(print, 'typeName'), + indent(path.call(print, 'attributes')), + ' ', + path.call(print, 'name') + ], + [path.call(print, 'value'), ';'] + ); } } diff --git a/src/slang-nodes/StateVariableDefinitionValue.ts b/src/slang-nodes/StateVariableDefinitionValue.ts index d082c71f5..637b10fde 100644 --- a/src/slang-nodes/StateVariableDefinitionValue.ts +++ b/src/slang-nodes/StateVariableDefinitionValue.ts @@ -1,5 +1,5 @@ -import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; @@ -8,8 +8,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; - export class StateVariableDefinitionValue extends SlangNode { readonly kind = NonterminalKind.StateVariableDefinitionValue; @@ -30,9 +28,12 @@ export class StateVariableDefinitionValue extends SlangNode { path: AstPath, print: PrintFunction ): Doc { - const value = path.call(print, 'value'); - return this.value.variant.kind === NonterminalKind.ArrayExpression - ? [' = ', value] - : group([' =', indent([line, value])]); + return [ + ' =', + printIndentedGroupOrSpacedDocument( + path.call(print, 'value'), + this.value.variant.kind !== NonterminalKind.ArrayExpression + ) + ]; } } diff --git a/src/slang-nodes/Statements.ts b/src/slang-nodes/Statements.ts index 0c32e3c18..1f56baaf9 100644 --- a/src/slang-nodes/Statements.ts +++ b/src/slang-nodes/Statements.ts @@ -1,7 +1,6 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; -import { printComments } from '../slang-printers/print-comments.js'; import { printPreservingEmptyLines } from '../slang-printers/print-preserving-empty-lines.js'; import { SlangNode } from './SlangNode.js'; import { Statement } from './Statement.js'; @@ -31,14 +30,10 @@ export class Statements extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return this.items.length === 0 && this.comments.length === 0 - ? '' - : printSeparatedItem( - [ - printPreservingEmptyLines(path, print, options), - printComments(path) - ], - { firstSeparator: hardline, grouped: false } - ); + return this.items.length > 0 || this.comments.length > 0 + ? printSeparatedItem(printPreservingEmptyLines(path, print, options), { + firstSeparator: hardline + }) + : ''; } } diff --git a/src/slang-nodes/TupleDeconstructionStatement.ts b/src/slang-nodes/TupleDeconstructionStatement.ts index 74e167602..eddb26316 100644 --- a/src/slang-nodes/TupleDeconstructionStatement.ts +++ b/src/slang-nodes/TupleDeconstructionStatement.ts @@ -1,5 +1,5 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; -import { doc } from 'prettier'; +import { printGroupAndIndentIfBreakPair } from '../slang-printers/print-group-and-indent-if-break-pair.js'; import { SlangNode } from './SlangNode.js'; import { TupleDeconstructionElements } from './TupleDeconstructionElements.js'; import { Expression } from './Expression.js'; @@ -9,8 +9,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indentIfBreak } = doc.builders; - export class TupleDeconstructionStatement extends SlangNode { readonly kind = NonterminalKind.TupleDeconstructionStatement; @@ -37,13 +35,9 @@ export class TupleDeconstructionStatement extends SlangNode { path: AstPath, print: PrintFunction ): Doc { - const groupId = Symbol('Slang.VariableDeclarationStatement.variables'); - return [ - group( - [this.varKeyword ? 'var (' : '(', path.call(print, 'elements'), ')'], - { id: groupId } - ), - indentIfBreak([' = ', path.call(print, 'expression'), ';'], { groupId }) - ]; + return printGroupAndIndentIfBreakPair( + [this.varKeyword ? 'var (' : '(', path.call(print, 'elements'), ') = '], + [path.call(print, 'expression'), ';'] + ); } } diff --git a/src/slang-nodes/UsingDirective.ts b/src/slang-nodes/UsingDirective.ts index 6201516d0..279131986 100644 --- a/src/slang-nodes/UsingDirective.ts +++ b/src/slang-nodes/UsingDirective.ts @@ -1,5 +1,4 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; -import { joinExisting } from '../slang-utils/join-existing.js'; import { SlangNode } from './SlangNode.js'; import { UsingClause } from './UsingClause.js'; import { UsingTarget } from './UsingTarget.js'; @@ -30,14 +29,11 @@ export class UsingDirective extends SlangNode { print(path: AstPath, print: PrintFunction): Doc { return [ - joinExisting(' ', [ - 'using', - path.call(print, 'clause'), - 'for', - path.call(print, 'target'), - this.globalKeyword - ]), - ';' + 'using ', + path.call(print, 'clause'), + ' for ', + path.call(print, 'target'), + this.globalKeyword ? ' global;' : ';' ]; } } diff --git a/src/slang-nodes/VariableDeclarationStatement.ts b/src/slang-nodes/VariableDeclarationStatement.ts index 4312cc45f..e808e47db 100644 --- a/src/slang-nodes/VariableDeclarationStatement.ts +++ b/src/slang-nodes/VariableDeclarationStatement.ts @@ -1,5 +1,6 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { doc } from 'prettier'; +import { printGroupAndIndentIfBreakPair } from '../slang-printers/print-group-and-indent-if-break-pair.js'; import { SlangNode } from './SlangNode.js'; import { VariableDeclarationType } from './VariableDeclarationType.js'; import { StorageLocation } from './StorageLocation.js'; @@ -11,7 +12,7 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, indentIfBreak, line } = doc.builders; +const { indent, line } = doc.builders; export class VariableDeclarationStatement extends SlangNode { readonly kind = NonterminalKind.VariableDeclarationStatement; @@ -46,23 +47,16 @@ export class VariableDeclarationStatement extends SlangNode { path: AstPath, print: PrintFunction ): Doc { - const groupId = Symbol('Slang.VariableDeclarationStatement.variables'); - return [ - group( - [ - path.call(print, 'variableType'), - indent([ - this.storageLocation - ? [line, path.call(print, 'storageLocation')] - : '', - ' ', - path.call(print, 'name') - ]) - ], - { id: groupId } - ), - indentIfBreak(path.call(print, 'value'), { groupId }), - ';' - ]; + return printGroupAndIndentIfBreakPair( + [ + path.call(print, 'variableType'), + this.storageLocation + ? indent([line, path.call(print, 'storageLocation')]) + : '', + ' ', + path.call(print, 'name') + ], + [path.call(print, 'value'), ';'] + ); } } diff --git a/src/slang-nodes/WhileStatement.ts b/src/slang-nodes/WhileStatement.ts index 6a9514464..121d44b23 100644 --- a/src/slang-nodes/WhileStatement.ts +++ b/src/slang-nodes/WhileStatement.ts @@ -1,6 +1,6 @@ -import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; +import { printIndentedGroupOrSpacedDocument } from '../slang-printers/print-indented-group-or-spaced-document.js'; import { SlangNode } from './SlangNode.js'; import { Expression } from './Expression.js'; import { Statement } from './Statement.js'; @@ -10,8 +10,6 @@ import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode } from './types.d.ts'; import type { PrintFunction } from '../types.d.ts'; -const { group, indent, line } = doc.builders; - export class WhileStatement extends SlangNode { readonly kind = NonterminalKind.WhileStatement; @@ -29,14 +27,14 @@ export class WhileStatement extends SlangNode { } print(path: AstPath, print: PrintFunction): Doc { - const body = path.call(print, 'body'); return [ 'while (', printSeparatedItem(path.call(print, 'condition')), ')', - this.body.variant.kind === NonterminalKind.Block - ? [' ', body] - : group(indent([line, body])) + printIndentedGroupOrSpacedDocument( + path.call(print, 'body'), + this.body.variant.kind !== NonterminalKind.Block + ) ]; } } diff --git a/src/slang-nodes/YulStatements.ts b/src/slang-nodes/YulStatements.ts index 516a0485e..6afaca46c 100644 --- a/src/slang-nodes/YulStatements.ts +++ b/src/slang-nodes/YulStatements.ts @@ -1,7 +1,6 @@ import { doc } from 'prettier'; import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { printSeparatedItem } from '../slang-printers/print-separated-item.js'; -import { printComments } from '../slang-printers/print-comments.js'; import { printPreservingEmptyLines } from '../slang-printers/print-preserving-empty-lines.js'; import { SlangNode } from './SlangNode.js'; import { YulStatement } from './YulStatement.js'; @@ -31,14 +30,10 @@ export class YulStatements extends SlangNode { print: PrintFunction, options: ParserOptions ): Doc { - return this.items.length === 0 && this.comments.length === 0 - ? '' - : printSeparatedItem( - [ - printPreservingEmptyLines(path, print, options), - printComments(path) - ], - { firstSeparator: hardline, grouped: false } - ); + return this.items.length > 0 || this.comments.length > 0 + ? printSeparatedItem(printPreservingEmptyLines(path, print, options), { + firstSeparator: hardline + }) + : ''; } } diff --git a/src/slang-printers/print-comments.ts b/src/slang-printers/print-comments.ts index ee660df14..596cc7d14 100644 --- a/src/slang-printers/print-comments.ts +++ b/src/slang-printers/print-comments.ts @@ -1,26 +1,38 @@ -import { doc } from 'prettier'; +import { doc, util } from 'prettier'; import { printComment } from '../slang-comments/printer.js'; -import { isBlockComment } from '../slang-utils/is-comment.js'; import { joinExisting } from '../slang-utils/join-existing.js'; -import type { AstPath, Doc } from 'prettier'; -import type { AstNode } from '../slang-nodes/types.d.ts'; +import type { AstPath, Doc, ParserOptions } from 'prettier'; +import type { AstNode, Comment } from '../slang-nodes/types.d.ts'; +import { locEnd } from '../slang-utils/loc.js'; -const { breakParent, line } = doc.builders; +const { hardline, line } = doc.builders; -export function printComments(path: AstPath): Doc[] { - const document = joinExisting( +function isPrintable(comment: Comment): boolean { + return !comment.trailing && !comment.leading && !comment.printed; +} + +export function printComments( + path: AstPath, + options: ParserOptions +): Doc[] { + return joinExisting( line, - path.map((commentPath) => { + path.map((commentPath, index, comments: Comment[]) => { const comment = commentPath.node; - if (comment.trailing || comment.leading || comment.printed) { + if (!isPrintable(comment)) { return ''; } comment.printed = true; - const printed = printComment(commentPath); - return isBlockComment(comment) ? printed : [printed, breakParent]; + const isLast = + index === comments.length - 1 || + comments.slice(index + 1).findIndex(isPrintable) === -1; + return [ + printComment(commentPath), + !isLast && util.isNextLineEmpty(options.originalText, locEnd(comment)) + ? hardline + : '' + ]; }, 'comments') ); - - return document; } diff --git a/src/slang-printers/print-function.ts b/src/slang-printers/print-function.ts index e95ea50f4..a379de4c6 100644 --- a/src/slang-printers/print-function.ts +++ b/src/slang-printers/print-function.ts @@ -18,8 +18,8 @@ export function printFunction( group([ functionName, path.call(print, 'parameters'), - indent( - group([ + group( + indent([ joinExisting(line, [ path.call(print, 'attributes'), path.call(print, 'returns') diff --git a/src/slang-printers/print-group-and-indent-if-break-pair.ts b/src/slang-printers/print-group-and-indent-if-break-pair.ts new file mode 100644 index 000000000..222d4be03 --- /dev/null +++ b/src/slang-printers/print-group-and-indent-if-break-pair.ts @@ -0,0 +1,16 @@ +import { doc } from 'prettier'; + +import type { Doc } from 'prettier'; + +const { group, indentIfBreak } = doc.builders; + +export function printGroupAndIndentIfBreakPair( + groupDoc: Doc, + indentIfBreakDoc: Doc +): [doc.builders.Group, doc.builders.IndentIfBreak] { + const groupId = Symbol('Slang.GroupAndIndentIfBreakPair'); + return [ + group(groupDoc, { id: groupId }), + indentIfBreak(indentIfBreakDoc, { groupId }) + ]; +} diff --git a/src/slang-printers/print-indented-group-or-spaced-document.ts b/src/slang-printers/print-indented-group-or-spaced-document.ts new file mode 100644 index 000000000..eedea56c7 --- /dev/null +++ b/src/slang-printers/print-indented-group-or-spaced-document.ts @@ -0,0 +1,15 @@ +import { doc } from 'prettier'; + +import type { Doc } from 'prettier'; + +const { group, indent, line } = doc.builders; + +export function printIndentedGroupOrSpacedDocument( + document: Doc, + shouldGroup = true, + groupOptions: doc.builders.GroupOptions = {} +): doc.builders.Group | [' ', Doc] { + return shouldGroup + ? group(indent([line, document]), groupOptions) + : [' ', document]; +} diff --git a/src/slang-printers/print-preserving-empty-lines.ts b/src/slang-printers/print-preserving-empty-lines.ts index bd43449db..41bc29495 100644 --- a/src/slang-printers/print-preserving-empty-lines.ts +++ b/src/slang-printers/print-preserving-empty-lines.ts @@ -1,6 +1,7 @@ import { NonterminalKind } from '@nomicfoundation/slang/cst'; import { doc, util } from 'prettier'; import { locEnd } from '../slang-utils/loc.js'; +import { printComments } from './print-comments.js'; import type { AstPath, Doc, ParserOptions } from 'prettier'; import type { AstNode, LineCollection } from '../slang-nodes/types.d.ts'; @@ -13,25 +14,27 @@ export function printPreservingEmptyLines( print: PrintFunction, options: ParserOptions ): Doc { - return path.map((childPath) => { - const node = childPath.node; + return path.node.items.length > 0 + ? path.map((childPath) => { + const node = childPath.node; - return [ - // Only attempt to prepend an empty line if `node` is not the first item - !childPath.isFirst && - // YulLabel adds a dedented line so we don't have to prepend a hardline. - (node.kind !== NonterminalKind.YulStatement || - node.variant.kind !== NonterminalKind.YulLabel) - ? hardline - : '', - print(childPath), - // Only attempt to append an empty line if `node` is not the last item - !childPath.isLast && - // Append an empty line if the original text already had an one after the - // current `node` - util.isNextLineEmpty(options.originalText, locEnd(node)) - ? hardline - : '' - ]; - }, 'items'); + return [ + // Only attempt to prepend an empty line if `node` is not the first item + !childPath.isFirst && + // YulLabel adds a dedented line so we don't have to prepend a hardline. + (node.kind !== NonterminalKind.YulStatement || + node.variant.kind !== NonterminalKind.YulLabel) + ? hardline + : '', + print(childPath), + // Only attempt to append an empty line if `node` is not the last item + !childPath.isLast && + // Append an empty line if the original text already had an one after the + // current `node` + util.isNextLineEmpty(options.originalText, locEnd(node)) + ? hardline + : '' + ]; + }, 'items') + : printComments(path, options); } diff --git a/src/slang-printers/print-separated-item.ts b/src/slang-printers/print-separated-item.ts index 873c079d6..34912723e 100644 --- a/src/slang-printers/print-separated-item.ts +++ b/src/slang-printers/print-separated-item.ts @@ -3,7 +3,7 @@ import { doc } from 'prettier'; import type { Doc } from 'prettier'; import type { PrintSeparatedOptions } from './types.d.ts'; -const { group, indent, softline } = doc.builders; +const { group, hardline, indent, softline } = doc.builders; // This function will add an indentation to the `item` and separate it from the // rest of the `doc` in most cases by a `softline`. @@ -12,10 +12,12 @@ export function printSeparatedItem( { firstSeparator = softline, lastSeparator = firstSeparator, - grouped = true + grouped = firstSeparator !== hardline }: PrintSeparatedOptions = {} -): Doc { - return grouped - ? group([indent([firstSeparator, item]), lastSeparator]) - : [indent([firstSeparator, item]), lastSeparator]; +): doc.builders.Group | [doc.builders.Indent, Doc] { + const document: [doc.builders.Indent, Doc] = [ + indent([firstSeparator, item]), + lastSeparator + ]; + return grouped ? group(document) : document; } diff --git a/src/slang-printers/print-separated-list.ts b/src/slang-printers/print-separated-list.ts index 6cc472aa8..3d07b3dbf 100644 --- a/src/slang-printers/print-separated-list.ts +++ b/src/slang-printers/print-separated-list.ts @@ -18,7 +18,7 @@ export function printSeparatedList( lastSeparator, grouped }: PrintSeparatedOptions = {} -): Doc { +): doc.builders.Group | [doc.builders.Indent, Doc] { return printSeparatedItem(join(separator, list), { firstSeparator, lastSeparator, diff --git a/tests/format/Empty/EmptyFile.sol b/tests/format/Empty/EmptyFile.sol new file mode 100644 index 000000000..e69de29bb diff --git a/tests/format/Empty/EmptyFileWithComments.sol b/tests/format/Empty/EmptyFileWithComments.sol new file mode 100644 index 000000000..a2f2f52a3 --- /dev/null +++ b/tests/format/Empty/EmptyFileWithComments.sol @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MIT + +/** + * TODO: Write contract. + */ \ No newline at end of file diff --git a/tests/format/Empty/__snapshots__/format.test.js.snap b/tests/format/Empty/__snapshots__/format.test.js.snap new file mode 100644 index 000000000..7b6677462 --- /dev/null +++ b/tests/format/Empty/__snapshots__/format.test.js.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`EmptyFile.sol format 1`] = ` +====================================options===================================== +parsers: ["slang"] +printWidth: 80 + | printWidth +=====================================input====================================== + +=====================================output===================================== + +================================================================================ +`; + +exports[`EmptyFileWithComments.sol format 1`] = ` +====================================options===================================== +parsers: ["slang"] +printWidth: 80 + | printWidth +=====================================input====================================== +// SPDX-License-Identifier: MIT + +/** + * TODO: Write contract. + */ +=====================================output===================================== +// SPDX-License-Identifier: MIT + +/** + * TODO: Write contract. + */ + +================================================================================ +`; diff --git a/tests/format/Empty/format.test.js b/tests/format/Empty/format.test.js new file mode 100644 index 000000000..6021bbb0d --- /dev/null +++ b/tests/format/Empty/format.test.js @@ -0,0 +1 @@ +runFormatTest(import.meta, ['slang']);