diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 12fbd6fb4ff..71afec908ab 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2158,8 +2158,19 @@ namespace ts { return (arg: T) => f(arg) && g(arg); } - export function or(f: (arg: T) => boolean, g: (arg: T) => boolean): (arg: T) => boolean { - return arg => f(arg) || g(arg); + export function or(...fs: ((arg: T) => boolean)[]): (arg: T) => boolean { + return arg => { + for (const f of fs) { + if (f(arg)) { + return true; + } + } + return false; + }; + } + + export function not(fn: (...args: T) => boolean): (...args: T) => boolean { + return (...args) => !fn(...args); } export function assertType(_: T): void { } // tslint:disable-line no-empty diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index 905f6335596..f158aff7e5c 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -1186,6 +1186,9 @@ namespace ts.formatting { recordReplace(previousRange.end, currentRange.pos - previousRange.end, " "); return onLaterLine ? LineAction.LineRemoved : LineAction.None; } + break; + case RuleAction.TrailingSemicolon: + recordReplace(previousRange.end, 0, ";"); } return LineAction.None; } diff --git a/src/services/formatting/rule.ts b/src/services/formatting/rule.ts index 60d53e32210..cd8aa4fc004 100644 --- a/src/services/formatting/rule.ts +++ b/src/services/formatting/rule.ts @@ -12,14 +12,15 @@ namespace ts.formatting { export const anyContext: ReadonlyArray = emptyArray; export const enum RuleAction { - Ignore = 1 << 0, - Space = 1 << 1, - NewLine = 1 << 2, - DeleteTrivia = 1 << 3, - DeleteToken = 1 << 4, + Ignore = 1 << 0, + Space = 1 << 1, + NewLine = 1 << 2, + DeleteTrivia = 1 << 3, + DeleteToken = 1 << 4, + TrailingSemicolon = 1 << 5, TriviaAction = Space | NewLine | DeleteTrivia, - TokenAction = DeleteToken + TokenAction = DeleteToken | TrailingSemicolon } export const enum RuleFlags { diff --git a/src/services/formatting/rules.ts b/src/services/formatting/rules.ts index 82fa18a839c..8af57d93190 100644 --- a/src/services/formatting/rules.ts +++ b/src/services/formatting/rules.ts @@ -310,7 +310,8 @@ namespace ts.formatting { rule("SpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionEnabled("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.Space), rule("NoSpaceBeforeTypeAnnotation", anyToken, SyntaxKind.ColonToken, [isOptionDisabledOrUndefined("insertSpaceBeforeTypeAnnotation"), isNonJsxSameLineTokenContext, isTypeAnnotationContext], RuleAction.DeleteTrivia), - rule("NoDiscretionarySemicolon", SyntaxKind.SemicolonToken, anyToken, [isSemicolonDeletionContext], RuleAction.DeleteToken), + rule("NoOptionalSemicolon", SyntaxKind.SemicolonToken, anyToken, [isOptionDisabled("insertTrailingSemicolon"), isSemicolonDeletionContext], RuleAction.DeleteToken), + rule("OptionalSemicolon", anyToken, anyToken, [isOptionEnabled("insertTrailingSemicolon"), isSemicolonInsertionContext], RuleAction.TrailingSemicolon), ]; function isSemicolonDeletionContext(context: FormattingContext): boolean { @@ -329,6 +330,17 @@ namespace ts.formatting { && context.nextTokenSpan.kind !== SyntaxKind.DotToken; } + function isSemicolonInsertionContext(context: FormattingContext): boolean { + const contextAncestor = findAncestor(context.currentTokenParent, ancestor => { + if (ancestor.end !== context.currentTokenSpan.end) { + return "quit"; + } + return syntaxMayBeASICandidate(ancestor.kind); + }); + + return !!contextAncestor && isASICandidate(contextAncestor); + } + // These rules are lower in priority than user-configurable. Rules earlier in this list have priority over rules later in the list. const lowPriorityCommonRules = [ // Space after keyword but not before ; or : or ? diff --git a/src/services/types.ts b/src/services/types.ts index 53d552394e0..fa2cdc1c821 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -734,6 +734,7 @@ namespace ts { readonly placeOpenBraceOnNewLineForControlBlocks?: boolean; readonly insertSpaceBeforeTypeAnnotation?: boolean; readonly indentMultiLineObjectLiteralBeginningOnBlankLine?: boolean; + readonly insertTrailingSemicolon?: boolean; } export function getDefaultFormatCodeSettings(newLineCharacter?: string): FormatCodeSettings { @@ -757,6 +758,7 @@ namespace ts { insertSpaceBeforeFunctionParenthesis: false, placeOpenBraceOnNewLineForFunctions: false, placeOpenBraceOnNewLineForControlBlocks: false, + insertTrailingSemicolon: true, }; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 2e6e6875a81..cf93f126f9b 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -743,7 +743,7 @@ namespace ts { return findPrecedingToken(position, file); } - export function findNextToken(previousToken: Node, parent: Node, sourceFile: SourceFile): Node | undefined { + export function findNextToken(previousToken: Node, parent: Node, sourceFile: SourceFileLike): Node | undefined { return find(parent); function find(n: Node): Node | undefined { @@ -1981,7 +1981,27 @@ namespace ts { return typeIsAccessible ? res : undefined; } - export function syntaxUsuallyHasTrailingSemicolon(kind: SyntaxKind) { + export function syntaxRequiresTrailingCommaOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.CallSignature + || kind === SyntaxKind.ConstructSignature + || kind === SyntaxKind.IndexSignature + || kind === SyntaxKind.PropertySignature + || kind === SyntaxKind.MethodSignature; + } + + export function syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.FunctionDeclaration + || kind === SyntaxKind.Constructor + || kind === SyntaxKind.MethodDeclaration + || kind === SyntaxKind.GetAccessor + || kind === SyntaxKind.SetAccessor; + } + + export function syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(kind: SyntaxKind) { + return kind === SyntaxKind.ModuleDeclaration; + } + + export function syntaxRequiresTrailingSemicolonOrASI(kind: SyntaxKind) { return kind === SyntaxKind.VariableStatement || kind === SyntaxKind.ExpressionStatement || kind === SyntaxKind.DoStatement @@ -1994,7 +2014,57 @@ namespace ts { || kind === SyntaxKind.TypeAliasDeclaration || kind === SyntaxKind.ImportDeclaration || kind === SyntaxKind.ImportEqualsDeclaration - || kind === SyntaxKind.ExportDeclaration; + || kind === SyntaxKind.ExportDeclaration + || kind === SyntaxKind.NamespaceExportDeclaration + || kind === SyntaxKind.ExportAssignment; + } + + export const syntaxMayBeASICandidate = or( + syntaxRequiresTrailingCommaOrSemicolonOrASI, + syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI, + syntaxRequiresTrailingModuleBlockOrSemicolonOrASI, + syntaxRequiresTrailingSemicolonOrASI); + + export function isASICandidate(node: Node): boolean { + const lastToken = node.getLastToken(); + if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) { + return false; + } + + if (syntaxRequiresTrailingCommaOrSemicolonOrASI(node.kind)) { + if (lastToken && lastToken.kind === SyntaxKind.CommaToken) { + return false; + } + } + if (syntaxRequiresTrailingModuleBlockOrSemicolonOrASI(node.kind)) { + const children = node.parent.getChildren(); + const nextChild = children[children.indexOf(node) + 1]; + if (nextChild && isModuleBlock(nextChild)) { + return false; + } + } + else if (syntaxRequiresTrailingFunctionBlockOrSemicolonOrASI(node.kind)) { + const children = node.parent.getChildren(); + const nextChild = children[children.indexOf(node) + 1]; + if (nextChild && isFunctionBlock(nextChild)) { + return false; + } + } + else if (!syntaxRequiresTrailingSemicolonOrASI(node.kind)) { + return false; + } + + const sourceFile = node.getSourceFile(); + const nextToken = findNextToken(node, sourceFile, sourceFile); + if (!nextToken) { + return false; + } + + if (nextToken.kind === SyntaxKind.CloseBraceToken) { + return true; + } + + return !positionsAreOnSameLine(node.getEnd(), nextToken.getStart(sourceFile), sourceFile); } export function probablyUsesSemicolons(sourceFile: SourceFile): boolean { @@ -2002,7 +2072,7 @@ namespace ts { let withoutSemicolon = 0; const nStatementsToObserve = 5; forEachChild(sourceFile, function visit(node): boolean | undefined { - if (syntaxUsuallyHasTrailingSemicolon(node.kind)) { + if (syntaxRequiresTrailingSemicolonOrASI(node.kind)) { const lastToken = node.getLastToken(sourceFile); if (lastToken && lastToken.kind === SyntaxKind.SemicolonToken) { withSemicolon++;