diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 48bb0a6dc11..4bc519c2b8e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4724,6 +4724,10 @@ namespace ts { return node.kind === SyntaxKind.BreakStatement; } + export function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement { + return node.kind === SyntaxKind.BreakStatement || node.kind === SyntaxKind.ContinueStatement; + } + export function isReturnStatement(node: Node): node is ReturnStatement { return node.kind === SyntaxKind.ReturnStatement; } diff --git a/src/services/documentHighlights.ts b/src/services/documentHighlights.ts index 4f11e33c638..d9a9a03c38b 100644 --- a/src/services/documentHighlights.ts +++ b/src/services/documentHighlights.ts @@ -53,95 +53,48 @@ namespace ts.DocumentHighlights { return [{ fileName: sourceFile.fileName, highlightSpans }]; } - // returns true if 'node' is defined and has a matching 'kind'. - function hasKind(node: Node, kind: SyntaxKind) { - return node !== undefined && node.kind === kind; - } - - // Null-propagating 'parent' function. - function parent(node: Node): Node { - return node && node.parent; - } - - function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] { - if (!node) { - return undefined; - } - + function getHighlightSpans(node: Node, sourceFile: SourceFile): HighlightSpan[] | undefined { switch (node.kind) { case SyntaxKind.IfKeyword: case SyntaxKind.ElseKeyword: - if (hasKind(node.parent, SyntaxKind.IfStatement)) { - return getIfElseOccurrences(node.parent, sourceFile); - } - break; + return isIfStatement(node.parent) ? getIfElseOccurrences(node.parent, sourceFile) : undefined; case SyntaxKind.ReturnKeyword: - if (hasKind(node.parent, SyntaxKind.ReturnStatement)) { - return highlightSpans(getReturnOccurrences(node.parent)); - } - break; + return useParent(node.parent, isReturnStatement, getReturnOccurrences); case SyntaxKind.ThrowKeyword: - if (hasKind(node.parent, SyntaxKind.ThrowStatement)) { - return highlightSpans(getThrowOccurrences(node.parent)); - } - break; + return useParent(node.parent, isThrowStatement, getThrowOccurrences); case SyntaxKind.TryKeyword: case SyntaxKind.CatchKeyword: case SyntaxKind.FinallyKeyword: - const tryStatement = node.kind === SyntaxKind.CatchKeyword ? parent(parent(node)) : parent(node); - if (hasKind(tryStatement, SyntaxKind.TryStatement)) { - return highlightSpans(getTryCatchFinallyOccurrences(tryStatement, sourceFile)); - } - break; + const tryStatement = node.kind === SyntaxKind.CatchKeyword ? node.parent.parent : node.parent; + return useParent(tryStatement, isTryStatement, getTryCatchFinallyOccurrences); case SyntaxKind.SwitchKeyword: - if (hasKind(node.parent, SyntaxKind.SwitchStatement)) { - return highlightSpans(getSwitchCaseDefaultOccurrences(node.parent)); - } - break; + return useParent(node.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); case SyntaxKind.CaseKeyword: case SyntaxKind.DefaultKeyword: - if (hasKind(parent(parent(parent(node))), SyntaxKind.SwitchStatement)) { - return highlightSpans(getSwitchCaseDefaultOccurrences(node.parent.parent.parent)); - } - break; + return useParent(node.parent.parent.parent, isSwitchStatement, getSwitchCaseDefaultOccurrences); case SyntaxKind.BreakKeyword: case SyntaxKind.ContinueKeyword: - if (hasKind(node.parent, SyntaxKind.BreakStatement) || hasKind(node.parent, SyntaxKind.ContinueStatement)) { - return highlightSpans(getBreakOrContinueStatementOccurrences(node.parent)); - } - break; + return useParent(node.parent, isBreakOrContinueStatement, getBreakOrContinueStatementOccurrences); case SyntaxKind.ForKeyword: - if (hasKind(node.parent, SyntaxKind.ForStatement) || - hasKind(node.parent, SyntaxKind.ForInStatement) || - hasKind(node.parent, SyntaxKind.ForOfStatement)) { - return highlightSpans(getLoopBreakContinueOccurrences(node.parent)); - } - break; case SyntaxKind.WhileKeyword: case SyntaxKind.DoKeyword: - if (hasKind(node.parent, SyntaxKind.WhileStatement) || hasKind(node.parent, SyntaxKind.DoStatement)) { - return highlightSpans(getLoopBreakContinueOccurrences(node.parent)); - } - break; + return useParent(node.parent, (n): n is IterationStatement => isIterationStatement(n, /*lookInLabeledStatements*/ true), getLoopBreakContinueOccurrences); case SyntaxKind.ConstructorKeyword: - if (hasKind(node.parent, SyntaxKind.Constructor)) { - return highlightSpans(getConstructorOccurrences(node.parent)); - } - break; + return useParent(node.parent, isConstructorDeclaration, getConstructorOccurrences); case SyntaxKind.GetKeyword: case SyntaxKind.SetKeyword: - if (hasKind(node.parent, SyntaxKind.GetAccessor) || hasKind(node.parent, SyntaxKind.SetAccessor)) { - return highlightSpans(getGetAndSetOccurrences(node.parent)); - } - break; + return useParent(node.parent, isAccessor, getGetAndSetOccurrences); default: - if (isModifierKind(node.kind) && node.parent && - (isDeclaration(node.parent) || node.parent.kind === SyntaxKind.VariableStatement)) { - return highlightSpans(getModifierOccurrences(node.kind, node.parent)); - } + return isModifierKind(node.kind) && (isDeclaration(node.parent) || isVariableStatement(node.parent)) + ? highlightSpans(getModifierOccurrences(node.kind, node.parent)) + : undefined; } - function highlightSpans(nodes: Node[]): HighlightSpan[] { + function useParent(node: Node, nodeTest: (node: Node) => node is T, getNodes: (node: T, sourceFile: SourceFile) => Node[] | undefined): HighlightSpan[] | undefined { + return nodeTest(node) ? highlightSpans(getNodes(node, sourceFile)) : undefined; + } + + function highlightSpans(nodes: Node[] | undefined): HighlightSpan[] | undefined { return nodes && nodes.map(node => getHighlightSpanForNode(node, sourceFile)); } } @@ -156,23 +109,21 @@ namespace ts.DocumentHighlights { return statementAccumulator; function aggregate(node: Node): void { - if (node.kind === SyntaxKind.ThrowStatement) { - statementAccumulator.push(node); + if (isThrowStatement(node)) { + statementAccumulator.push(node); } - else if (node.kind === SyntaxKind.TryStatement) { - const tryStatement = node; - - if (tryStatement.catchClause) { - aggregate(tryStatement.catchClause); + else if (isTryStatement(node)) { + if (node.catchClause) { + aggregate(node.catchClause); } else { // Exceptions thrown within a try block lacking a catch clause // are "owned" in the current context. - aggregate(tryStatement.tryBlock); + aggregate(node.tryBlock); } - if (tryStatement.finallyBlock) { - aggregate(tryStatement.finallyBlock); + if (node.finallyBlock) { + aggregate(node.finallyBlock); } } // Do not cross function boundaries. @@ -236,11 +187,11 @@ namespace ts.DocumentHighlights { } function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node { - for (let node = statement.parent; node; node = node.parent) { + return findAncestor(statement, node => { switch (node.kind) { case SyntaxKind.SwitchStatement: if (statement.kind === SyntaxKind.ContinueStatement) { - continue; + return false; } // falls through case SyntaxKind.ForStatement: @@ -248,20 +199,13 @@ namespace ts.DocumentHighlights { case SyntaxKind.ForOfStatement: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: - if (!statement.label || isLabeledBy(node, statement.label.text)) { - return node; - } - break; + return !statement.label || isLabeledBy(node, statement.label.text); default: // Don't cross function boundaries. - if (isFunctionLike(node)) { - return undefined; - } - break; + // TODO: GH#20090 + return (isFunctionLike(node) && "quit") as false | "quit"; } - } - - return undefined; + }); } function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): Node[] { @@ -494,16 +438,14 @@ namespace ts.DocumentHighlights { return keywords; } - function getReturnOccurrences(returnStatement: ReturnStatement): Node[] { + function getReturnOccurrences(returnStatement: ReturnStatement): Node[] | undefined { const func = getContainingFunction(returnStatement); - - // If we didn't find a containing function with a block body, bail out. - if (!(func && hasKind(func.body, SyntaxKind.Block))) { + if (!func) { return undefined; } const keywords: Node[] = []; - forEachReturnStatement(func.body, returnStatement => { + forEachReturnStatement(cast(func.body, isBlock), returnStatement => { pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); }); @@ -516,32 +458,7 @@ namespace ts.DocumentHighlights { } function getIfElseOccurrences(ifStatement: IfStatement, sourceFile: SourceFile): HighlightSpan[] { - const keywords: Node[] = []; - - // Traverse upwards through all parent if-statements linked by their else-branches. - while (hasKind(ifStatement.parent, SyntaxKind.IfStatement) && (ifStatement.parent).elseStatement === ifStatement) { - ifStatement = ifStatement.parent; - } - - // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. - while (ifStatement) { - const children = ifStatement.getChildren(); - pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); - - // Generally the 'else' keyword is second-to-last, so we traverse backwards. - for (let i = children.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { - break; - } - } - - if (!hasKind(ifStatement.elseStatement, SyntaxKind.IfStatement)) { - break; - } - - ifStatement = ifStatement.elseStatement; - } - + const keywords = getIfElseKeywords(ifStatement, sourceFile); const result: HighlightSpan[] = []; // We'd like to highlight else/ifs together if they are only separated by whitespace @@ -551,17 +468,17 @@ namespace ts.DocumentHighlights { const elseKeyword = keywords[i]; const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. - let shouldCombindElseAndIf = true; + let shouldCombineElseAndIf = true; // Avoid recalculating getStart() by iterating backwards. - for (let j = ifKeyword.getStart() - 1; j >= elseKeyword.end; j--) { + for (let j = ifKeyword.getStart(sourceFile) - 1; j >= elseKeyword.end; j--) { if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { - shouldCombindElseAndIf = false; + shouldCombineElseAndIf = false; break; } } - if (shouldCombindElseAndIf) { + if (shouldCombineElseAndIf) { result.push({ fileName: sourceFile.fileName, textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), @@ -579,6 +496,36 @@ namespace ts.DocumentHighlights { return result; } + function getIfElseKeywords(ifStatement: IfStatement, sourceFile: SourceFile): Node[] { + const keywords: Node[] = []; + + // Traverse upwards through all parent if-statements linked by their else-branches. + while (isIfStatement(ifStatement.parent) && ifStatement.parent.elseStatement === ifStatement) { + ifStatement = ifStatement.parent; + } + + // Now traverse back down through the else branches, aggregating if/else keywords of if-statements. + while (true) { + const children = ifStatement.getChildren(sourceFile); + pushKeywordIf(keywords, children[0], SyntaxKind.IfKeyword); + + // Generally the 'else' keyword is second-to-last, so we traverse backwards. + for (let i = children.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, children[i], SyntaxKind.ElseKeyword)) { + break; + } + } + + if (!ifStatement.elseStatement || !isIfStatement(ifStatement.elseStatement)) { + break; + } + + ifStatement = ifStatement.elseStatement; + } + + return keywords; + } + /** * Whether or not a 'node' is preceded by a label of the given string. * Note: 'node' cannot be a SourceFile. diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 23520f42952..0b93c408be4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2992,6 +2992,7 @@ declare namespace ts { function isForOfStatement(node: Node): node is ForOfStatement; function isContinueStatement(node: Node): node is ContinueStatement; function isBreakStatement(node: Node): node is BreakStatement; + function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement; function isReturnStatement(node: Node): node is ReturnStatement; function isWithStatement(node: Node): node is WithStatement; function isSwitchStatement(node: Node): node is SwitchStatement; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 68c2ed14f2b..8a8f5218f3a 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3045,6 +3045,7 @@ declare namespace ts { function isForOfStatement(node: Node): node is ForOfStatement; function isContinueStatement(node: Node): node is ContinueStatement; function isBreakStatement(node: Node): node is BreakStatement; + function isBreakOrContinueStatement(node: Node): node is BreakOrContinueStatement; function isReturnStatement(node: Node): node is ReturnStatement; function isWithStatement(node: Node): node is WithStatement; function isSwitchStatement(node: Node): node is SwitchStatement;