diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 31f8a37aa3c..b226fa7bb78 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -232,8 +232,8 @@ namespace Harness.LanguageService { } getHost() { return this.host; } getLanguageService(): ts.LanguageService { return ts.createLanguageService(this.host); } - getClassifier(): ts.Classifier { return ts.createClassifier(); } - getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJavaScriptFileExtension(fileName)); } + getClassifier(): ts.Classifier { return ts.Classifier.createClassifier(); } + getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { return ts.PreProcess.preProcessFile(fileContents, /* readImportFiles */ true, ts.hasJavaScriptFileExtension(fileName)); } } /// Shim adapter @@ -258,7 +258,7 @@ namespace Harness.LanguageService { }; this.getModuleResolutionsForFile = (fileName) => { const scriptInfo = this.getScriptInfo(fileName); - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); + const preprocessInfo = ts.PreProcess.preProcessFile(scriptInfo.content, /*readImportFiles*/ true); const imports = ts.createMap(); for (const module of preprocessInfo.importedFiles) { const resolutionInfo = ts.resolveModuleName(module.fileName, fileName, compilerOptions, moduleResolutionHost); @@ -271,7 +271,7 @@ namespace Harness.LanguageService { this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { const scriptInfo = this.getScriptInfo(fileName); if (scriptInfo) { - const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); + const preprocessInfo = ts.PreProcess.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); const resolutions = ts.createMap(); const settings = this.nativeHost.getCompilationSettings(); for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { diff --git a/src/harness/unittests/services/preProcessFile.ts b/src/harness/unittests/services/preProcessFile.ts index 403cccc8cf3..6e181e7aef2 100644 --- a/src/harness/unittests/services/preProcessFile.ts +++ b/src/harness/unittests/services/preProcessFile.ts @@ -2,7 +2,7 @@ describe("PreProcessFile:", function () { function test(sourceText: string, readImportFile: boolean, detectJavaScriptImports: boolean, expectedPreProcess: ts.PreProcessedFileInfo): void { - const resultPreProcess = ts.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); + const resultPreProcess = ts.PreProcess.preProcessFile(sourceText, readImportFile, detectJavaScriptImports); assert.equal(resultPreProcess.isLibFile, expectedPreProcess.isLibFile, "Pre-processed file has different value for isLibFile. Expected: " + expectedPreProcess.isLibFile + ". Actual: " + resultPreProcess.isLibFile); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e0f9e8d5cf6..87e26ce2bf3 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1566,7 +1566,7 @@ namespace ts.server { this.setCompilerOptions(defaultOpts); } this.languageService = ts.createLanguageService(this.host, this.documentRegistry); - this.classifier = ts.createClassifier(); + this.classifier = ts.Classifier.createClassifier(); } setCompilerOptions(opt: ts.CompilerOptions) { diff --git a/src/services/allocators.ts b/src/services/allocators.ts new file mode 100644 index 00000000000..4bfef21cf1b --- /dev/null +++ b/src/services/allocators.ts @@ -0,0 +1,633 @@ +/* @internal */ +namespace ts.Allocators { + function createNode(kind: SyntaxKind, pos: number, end: number, parent?: Node): NodeObject | TokenObject | IdentifierObject { + const node = kind >= SyntaxKind.FirstNode ? new NodeObject(kind, pos, end) : + kind === SyntaxKind.Identifier ? new IdentifierObject(kind, pos, end) : + new TokenObject(kind, pos, end); + node.parent = parent; + return node; + } + + class NodeObject implements Node { + public kind: SyntaxKind; + public pos: number; + public end: number; + public flags: NodeFlags; + public parent: Node; + public jsDocComments: JSDocComment[]; + public original: Node; + public transformFlags: TransformFlags; + public excludeTransformFlags: TransformFlags; + private _children: Node[]; + + constructor(kind: SyntaxKind, pos: number, end: number) { + this.pos = pos; + this.end = end; + this.flags = NodeFlags.None; + this.transformFlags = undefined; + this.excludeTransformFlags = undefined; + this.parent = undefined; + this.kind = kind; + } + + public getSourceFile(): SourceFile { + return getSourceFileOfNode(this); + } + + public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { + return getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } + + public getFullStart(): number { + return this.pos; + } + + public getEnd(): number { + return this.end; + } + + public getWidth(sourceFile?: SourceFile): number { + return this.getEnd() - this.getStart(sourceFile); + } + + public getFullWidth(): number { + return this.end - this.pos; + } + + public getLeadingTriviaWidth(sourceFile?: SourceFile): number { + return this.getStart(sourceFile) - this.pos; + } + + public getFullText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } + + public getText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.getStart(), this.getEnd()); + } + + private addSyntheticNodes(nodes: Node[], pos: number, end: number, useJSDocScanner?: boolean): number { + scanner.setTextPos(pos); + while (pos < end) { + const token = useJSDocScanner ? scanner.scanJSDocToken() : scanner.scan(); + const textPos = scanner.getTextPos(); + if (textPos <= end) { + nodes.push(createNode(token, pos, textPos, this)); + } + pos = textPos; + } + return pos; + } + + private createSyntaxList(nodes: NodeArray): Node { + const list = createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, this); + list._children = []; + let pos = nodes.pos; + + for (const node of nodes) { + if (pos < node.pos) { + pos = this.addSyntheticNodes(list._children, pos, node.pos); + } + list._children.push(node); + pos = node.end; + } + if (pos < nodes.end) { + this.addSyntheticNodes(list._children, pos, nodes.end); + } + return list; + } + + private createChildren(sourceFile?: SourceFile) { + let children: Node[]; + if (this.kind >= SyntaxKind.FirstNode) { + scanner.setText((sourceFile || this.getSourceFile()).text); + children = []; + let pos = this.pos; + const useJSDocScanner = this.kind >= SyntaxKind.FirstJSDocTagNode && this.kind <= SyntaxKind.LastJSDocTagNode; + const processNode = (node: Node) => { + const isJSDocTagNode = isJSDocTag(node); + if (!isJSDocTagNode && pos < node.pos) { + pos = this.addSyntheticNodes(children, pos, node.pos, useJSDocScanner); + } + children.push(node); + if (!isJSDocTagNode) { + pos = node.end; + } + }; + const processNodes = (nodes: NodeArray) => { + if (pos < nodes.pos) { + pos = this.addSyntheticNodes(children, pos, nodes.pos, useJSDocScanner); + } + children.push(this.createSyntaxList(>nodes)); + pos = nodes.end; + }; + // jsDocComments need to be the first children + if (this.jsDocComments) { + for (const jsDocComment of this.jsDocComments) { + processNode(jsDocComment); + } + } + // For syntactic classifications, all trivia are classcified together, including jsdoc comments. + // For that to work, the jsdoc comments should still be the leading trivia of the first child. + // Restoring the scanner position ensures that. + pos = this.pos; + forEachChild(this, processNode, processNodes); + if (pos < this.end) { + this.addSyntheticNodes(children, pos, this.end); + } + scanner.setText(undefined); + } + this._children = children || emptyArray; + } + + public getChildCount(sourceFile?: SourceFile): number { + if (!this._children) this.createChildren(sourceFile); + return this._children.length; + } + + public getChildAt(index: number, sourceFile?: SourceFile): Node { + if (!this._children) this.createChildren(sourceFile); + return this._children[index]; + } + + public getChildren(sourceFile?: SourceFile): Node[] { + if (!this._children) this.createChildren(sourceFile); + return this._children; + } + + public getFirstToken(sourceFile?: SourceFile): Node { + const children = this.getChildren(sourceFile); + if (!children.length) { + return undefined; + } + + const child = children[0]; + + return child.kind < SyntaxKind.FirstNode ? child : child.getFirstToken(sourceFile); + } + + public getLastToken(sourceFile?: SourceFile): Node { + const children = this.getChildren(sourceFile); + + const child = lastOrUndefined(children); + if (!child) { + return undefined; + } + + return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); + } + } + + class TokenOrIdentifierObject implements Token { + public kind: SyntaxKind; + public pos: number; + public end: number; + public flags: NodeFlags; + public parent: Node; + public jsDocComments: JSDocComment[]; + public __tokenTag: any; + + constructor(pos: number, end: number) { + // Set properties in same order as NodeObject + this.pos = pos; + this.end = end; + this.flags = NodeFlags.None; + this.parent = undefined; + } + + public getSourceFile(): SourceFile { + return getSourceFileOfNode(this); + } + + public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { + return getTokenPosOfNode(this, sourceFile, includeJsDocComment); + } + + public getFullStart(): number { + return this.pos; + } + + public getEnd(): number { + return this.end; + } + + public getWidth(sourceFile?: SourceFile): number { + return this.getEnd() - this.getStart(sourceFile); + } + + public getFullWidth(): number { + return this.end - this.pos; + } + + public getLeadingTriviaWidth(sourceFile?: SourceFile): number { + return this.getStart(sourceFile) - this.pos; + } + + public getFullText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); + } + + public getText(sourceFile?: SourceFile): string { + return (sourceFile || this.getSourceFile()).text.substring(this.getStart(), this.getEnd()); + } + + public getChildCount(sourceFile?: SourceFile): number { + return 0; + } + + public getChildAt(index: number, sourceFile?: SourceFile): Node { + return undefined; + } + + public getChildren(sourceFile?: SourceFile): Node[] { + return emptyArray; + } + + public getFirstToken(sourceFile?: SourceFile): Node { + return undefined; + } + + public getLastToken(sourceFile?: SourceFile): Node { + return undefined; + } + } + + class SymbolObject implements Symbol { + flags: SymbolFlags; + name: string; + declarations: Declaration[]; + + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no doc comment, then the empty string will be returned. + documentationComment: SymbolDisplayPart[]; + + constructor(flags: SymbolFlags, name: string) { + this.flags = flags; + this.name = name; + } + + getFlags(): SymbolFlags { + return this.flags; + } + + getName(): string { + return this.name; + } + + getDeclarations(): Declaration[] { + return this.declarations; + } + + getDocumentationComment(): SymbolDisplayPart[] { + if (this.documentationComment === undefined) { + this.documentationComment = JsDoc.getJsDocCommentsFromDeclarations(this.declarations, this.name, !(this.flags & SymbolFlags.Property)); + } + + return this.documentationComment; + } + } + + class TokenObject extends TokenOrIdentifierObject { + public kind: SyntaxKind; + constructor(kind: SyntaxKind, pos: number, end: number) { + super(pos, end); + this.kind = kind; + } + } + + class IdentifierObject extends TokenOrIdentifierObject { + constructor(kind: SyntaxKind, pos: number, end: number) { + super(pos, end); + } + } + IdentifierObject.prototype.kind = SyntaxKind.Identifier; + + class TypeObject implements Type { + checker: TypeChecker; + flags: TypeFlags; + id: number; + symbol: Symbol; + constructor(checker: TypeChecker, flags: TypeFlags) { + this.checker = checker; + this.flags = flags; + } + getFlags(): TypeFlags { + return this.flags; + } + getSymbol(): Symbol { + return this.symbol; + } + getProperties(): Symbol[] { + return this.checker.getPropertiesOfType(this); + } + getProperty(propertyName: string): Symbol { + return this.checker.getPropertyOfType(this, propertyName); + } + getApparentProperties(): Symbol[] { + return this.checker.getAugmentedPropertiesOfType(this); + } + getCallSignatures(): Signature[] { + return this.checker.getSignaturesOfType(this, SignatureKind.Call); + } + getConstructSignatures(): Signature[] { + return this.checker.getSignaturesOfType(this, SignatureKind.Construct); + } + getStringIndexType(): Type { + return this.checker.getIndexTypeOfType(this, IndexKind.String); + } + getNumberIndexType(): Type { + return this.checker.getIndexTypeOfType(this, IndexKind.Number); + } + getBaseTypes(): ObjectType[] { + return this.flags & (TypeFlags.Class | TypeFlags.Interface) + ? this.checker.getBaseTypes(this) + : undefined; + } + getNonNullableType(): Type { + return this.checker.getNonNullableType(this); + } + } + + class SignatureObject implements Signature { + checker: TypeChecker; + declaration: SignatureDeclaration; + typeParameters: TypeParameter[]; + parameters: Symbol[]; + thisParameter: Symbol; + resolvedReturnType: Type; + minArgumentCount: number; + hasRestParameter: boolean; + hasLiteralTypes: boolean; + + // Undefined is used to indicate the value has not been computed. If, after computing, the + // symbol has no doc comment, then the empty string will be returned. + documentationComment: SymbolDisplayPart[]; + + constructor(checker: TypeChecker) { + this.checker = checker; + } + getDeclaration(): SignatureDeclaration { + return this.declaration; + } + getTypeParameters(): Type[] { + return this.typeParameters; + } + getParameters(): Symbol[] { + return this.parameters; + } + getReturnType(): Type { + return this.checker.getReturnTypeOfSignature(this); + } + + getDocumentationComment(): SymbolDisplayPart[] { + if (this.documentationComment === undefined) { + this.documentationComment = this.declaration ? JsDoc.getJsDocCommentsFromDeclarations( + [this.declaration], + /*name*/ undefined, + /*canUseParsedParamTagComments*/ false) : []; + } + + return this.documentationComment; + } + } + + class SourceFileObject extends NodeObject implements SourceFile { + public _declarationBrand: any; + public fileName: string; + public path: Path; + public text: string; + public scriptSnapshot: IScriptSnapshot; + public lineMap: number[]; + + public statements: NodeArray; + public endOfFileToken: Node; + + public amdDependencies: { name: string; path: string }[]; + public moduleName: string; + public referencedFiles: FileReference[]; + public typeReferenceDirectives: FileReference[]; + + public syntacticDiagnostics: Diagnostic[]; + public referenceDiagnostics: Diagnostic[]; + public parseDiagnostics: Diagnostic[]; + public bindDiagnostics: Diagnostic[]; + + public isDeclarationFile: boolean; + public isDefaultLib: boolean; + public hasNoDefaultLib: boolean; + public externalModuleIndicator: Node; // The first node that causes this file to be an external module + public commonJsModuleIndicator: Node; // The first node that causes this file to be a CommonJS module + public nodeCount: number; + public identifierCount: number; + public symbolCount: number; + public version: string; + public scriptKind: ScriptKind; + public languageVersion: ScriptTarget; + public languageVariant: LanguageVariant; + public identifiers: Map; + public nameTable: Map; + public resolvedModules: Map; + public resolvedTypeReferenceDirectiveNames: Map; + public imports: LiteralExpression[]; + public moduleAugmentations: LiteralExpression[]; + private namedDeclarations: Map; + + constructor(kind: SyntaxKind, pos: number, end: number) { + super(kind, pos, end); + } + + public update(newText: string, textChangeRange: TextChangeRange): SourceFile { + return updateSourceFile(this, newText, textChangeRange); + } + + public getLineAndCharacterOfPosition(position: number): LineAndCharacter { + return ts.getLineAndCharacterOfPosition(this, position); + } + + public getLineStarts(): number[] { + return getLineStarts(this); + } + + public getPositionOfLineAndCharacter(line: number, character: number): number { + return ts.getPositionOfLineAndCharacter(this, line, character); + } + + public getNamedDeclarations(): Map { + if (!this.namedDeclarations) { + this.namedDeclarations = this.computeNamedDeclarations(); + } + + return this.namedDeclarations; + } + + private computeNamedDeclarations(): Map { + const result = createMap(); + + forEachChild(this, visit); + + return result; + + function addDeclaration(declaration: Declaration) { + const name = getDeclarationName(declaration); + if (name) { + multiMapAdd(result, name, declaration); + } + } + + function getDeclarations(name: string) { + return result[name] || (result[name] = []); + } + + function getDeclarationName(declaration: Declaration) { + if (declaration.name) { + const result = getTextOfIdentifierOrLiteral(declaration.name); + if (result !== undefined) { + return result; + } + + if (declaration.name.kind === SyntaxKind.ComputedPropertyName) { + const expr = (declaration.name).expression; + if (expr.kind === SyntaxKind.PropertyAccessExpression) { + return (expr).name.text; + } + + return getTextOfIdentifierOrLiteral(expr); + } + } + + return undefined; + } + + function getTextOfIdentifierOrLiteral(node: Node) { + if (node) { + if (node.kind === SyntaxKind.Identifier || + node.kind === SyntaxKind.StringLiteral || + node.kind === SyntaxKind.NumericLiteral) { + + return (node).text; + } + } + + return undefined; + } + + function visit(node: Node): void { + switch (node.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + const functionDeclaration = node; + const declarationName = getDeclarationName(functionDeclaration); + + if (declarationName) { + const declarations = getDeclarations(declarationName); + const lastDeclaration = lastOrUndefined(declarations); + + // Check whether this declaration belongs to an "overload group". + if (lastDeclaration && functionDeclaration.parent === lastDeclaration.parent && functionDeclaration.symbol === lastDeclaration.symbol) { + // Overwrite the last declaration if it was an overload + // and this one is an implementation. + if (functionDeclaration.body && !(lastDeclaration).body) { + declarations[declarations.length - 1] = functionDeclaration; + } + } + else { + declarations.push(functionDeclaration); + } + + forEachChild(node, visit); + } + break; + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportClause: + case SyntaxKind.NamespaceImport: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.TypeLiteral: + addDeclaration(node); + forEachChild(node, visit); + break; + + case SyntaxKind.Parameter: + // Only consider parameter properties + if (!hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { + break; + } + // fall through + case SyntaxKind.VariableDeclaration: + case SyntaxKind.BindingElement: { + const decl = node; + if (isBindingPattern(decl.name)) { + forEachChild(decl.name, visit); + break; + } + if (decl.initializer) + visit(decl.initializer); + } + case SyntaxKind.EnumMember: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + addDeclaration(node); + break; + + case SyntaxKind.ExportDeclaration: + // Handle named exports case e.g.: + // export {a, b as B} from "mod"; + if ((node).exportClause) { + forEach((node).exportClause.elements, visit); + } + break; + + case SyntaxKind.ImportDeclaration: + const importClause = (node).importClause; + if (importClause) { + // Handle default import case e.g.: + // import d from "mod"; + if (importClause.name) { + addDeclaration(importClause); + } + + // Handle named bindings in imports e.g.: + // import * as NS from "mod"; + // import {a, b as B} from "mod"; + if (importClause.namedBindings) { + if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { + addDeclaration(importClause.namedBindings); + } + else { + forEach((importClause.namedBindings).elements, visit); + } + } + } + break; + + default: + forEachChild(node, visit); + } + } + } + } + + export function getServicesObjectAllocator(): ObjectAllocator { + return { + getNodeConstructor: () => NodeObject, + getTokenConstructor: () => TokenObject, + getIdentifierConstructor: () => IdentifierObject, + getSourceFileConstructor: () => SourceFileObject, + getSymbolConstructor: () => SymbolObject, + getTypeConstructor: () => TypeObject, + getSignatureConstructor: () => SignatureObject, + }; + } +} diff --git a/src/services/classifier.ts b/src/services/classifier.ts new file mode 100644 index 00000000000..a6c6144317d --- /dev/null +++ b/src/services/classifier.ts @@ -0,0 +1,982 @@ +/* @internal */ +namespace ts.Classifier { + /// Classifier + export function createClassifier(): Classifier { + const scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false); + + /// We do not have a full parser support to know when we should parse a regex or not + /// If we consider every slash token to be a regex, we could be missing cases like "1/2/3", where + /// we have a series of divide operator. this list allows us to be more accurate by ruling out + /// locations where a regexp cannot exist. + const noRegexTable: boolean[] = []; + noRegexTable[SyntaxKind.Identifier] = true; + noRegexTable[SyntaxKind.StringLiteral] = true; + noRegexTable[SyntaxKind.NumericLiteral] = true; + noRegexTable[SyntaxKind.RegularExpressionLiteral] = true; + noRegexTable[SyntaxKind.ThisKeyword] = true; + noRegexTable[SyntaxKind.PlusPlusToken] = true; + noRegexTable[SyntaxKind.MinusMinusToken] = true; + noRegexTable[SyntaxKind.CloseParenToken] = true; + noRegexTable[SyntaxKind.CloseBracketToken] = true; + noRegexTable[SyntaxKind.CloseBraceToken] = true; + noRegexTable[SyntaxKind.TrueKeyword] = true; + noRegexTable[SyntaxKind.FalseKeyword] = true; + + // Just a stack of TemplateHeads and OpenCurlyBraces, used to perform rudimentary (inexact) + // classification on template strings. Because of the context free nature of templates, + // the only precise way to classify a template portion would be by propagating the stack across + // lines, just as we do with the end-of-line state. However, this is a burden for implementers, + // and the behavior is entirely subsumed by the syntactic classifier anyway, so we instead + // flatten any nesting when the template stack is non-empty and encode it in the end-of-line state. + // Situations in which this fails are + // 1) When template strings are nested across different lines: + // `hello ${ `world + // ` }` + // + // Where on the second line, you will get the closing of a template, + // a closing curly, and a new template. + // + // 2) When substitution expressions have curly braces and the curly brace falls on the next line: + // `hello ${ () => { + // return "world" } } ` + // + // Where on the second line, you will get the 'return' keyword, + // a string literal, and a template end consisting of '} } `'. + const templateStack: SyntaxKind[] = []; + + /** Returns true if 'keyword2' can legally follow 'keyword1' in any language construct. */ + function canFollow(keyword1: SyntaxKind, keyword2: SyntaxKind) { + if (isAccessibilityModifier(keyword1)) { + if (keyword2 === SyntaxKind.GetKeyword || + keyword2 === SyntaxKind.SetKeyword || + keyword2 === SyntaxKind.ConstructorKeyword || + keyword2 === SyntaxKind.StaticKeyword) { + + // Allow things like "public get", "public constructor" and "public static". + // These are all legal. + return true; + } + + // Any other keyword following "public" is actually an identifier an not a real + // keyword. + return false; + } + + // Assume any other keyword combination is legal. This can be refined in the future + // if there are more cases we want the classifier to be better at. + return true; + } + + function convertClassifications(classifications: Classifications, text: string): ClassificationResult { + const entries: ClassificationInfo[] = []; + const dense = classifications.spans; + let lastEnd = 0; + + for (let i = 0, n = dense.length; i < n; i += 3) { + const start = dense[i]; + const length = dense[i + 1]; + const type = dense[i + 2]; + + // Make a whitespace entry between the last item and this one. + if (lastEnd >= 0) { + const whitespaceLength = start - lastEnd; + if (whitespaceLength > 0) { + entries.push({ length: whitespaceLength, classification: TokenClass.Whitespace }); + } + } + + entries.push({ length, classification: convertClassification(type) }); + lastEnd = start + length; + } + + const whitespaceLength = text.length - lastEnd; + if (whitespaceLength > 0) { + entries.push({ length: whitespaceLength, classification: TokenClass.Whitespace }); + } + + return { entries, finalLexState: classifications.endOfLineState }; + } + + function convertClassification(type: ClassificationType): TokenClass { + switch (type) { + case ClassificationType.comment: return TokenClass.Comment; + case ClassificationType.keyword: return TokenClass.Keyword; + case ClassificationType.numericLiteral: return TokenClass.NumberLiteral; + case ClassificationType.operator: return TokenClass.Operator; + case ClassificationType.stringLiteral: return TokenClass.StringLiteral; + case ClassificationType.whiteSpace: return TokenClass.Whitespace; + case ClassificationType.punctuation: return TokenClass.Punctuation; + case ClassificationType.identifier: + case ClassificationType.className: + case ClassificationType.enumName: + case ClassificationType.interfaceName: + case ClassificationType.moduleName: + case ClassificationType.typeParameterName: + case ClassificationType.typeAliasName: + case ClassificationType.text: + case ClassificationType.parameterName: + default: + return TokenClass.Identifier; + } + } + + function getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): ClassificationResult { + return convertClassifications(getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent), text); + } + + // If there is a syntactic classifier ('syntacticClassifierAbsent' is false), + // we will be more conservative in order to avoid conflicting with the syntactic classifier. + function getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): Classifications { + let offset = 0; + let token = SyntaxKind.Unknown; + let lastNonTriviaToken = SyntaxKind.Unknown; + + // Empty out the template stack for reuse. + while (templateStack.length > 0) { + templateStack.pop(); + } + + // If we're in a string literal, then prepend: "\ + // (and a newline). That way when we lex we'll think we're still in a string literal. + // + // If we're in a multiline comment, then prepend: /* + // (and a newline). That way when we lex we'll think we're still in a multiline comment. + switch (lexState) { + case EndOfLineState.InDoubleQuoteStringLiteral: + text = "\"\\\n" + text; + offset = 3; + break; + case EndOfLineState.InSingleQuoteStringLiteral: + text = "'\\\n" + text; + offset = 3; + break; + case EndOfLineState.InMultiLineCommentTrivia: + text = "/*\n" + text; + offset = 3; + break; + case EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate: + text = "`\n" + text; + offset = 2; + break; + case EndOfLineState.InTemplateMiddleOrTail: + text = "}\n" + text; + offset = 2; + // fallthrough + case EndOfLineState.InTemplateSubstitutionPosition: + templateStack.push(SyntaxKind.TemplateHead); + break; + } + + scanner.setText(text); + + const result: Classifications = { + endOfLineState: EndOfLineState.None, + spans: [] + }; + + // We can run into an unfortunate interaction between the lexical and syntactic classifier + // when the user is typing something generic. Consider the case where the user types: + // + // Foo tokens. It's a weak heuristic, but should + // work well enough in practice. + let angleBracketStack = 0; + + do { + token = scanner.scan(); + + if (!isTrivia(token)) { + if ((token === SyntaxKind.SlashToken || token === SyntaxKind.SlashEqualsToken) && !noRegexTable[lastNonTriviaToken]) { + if (scanner.reScanSlashToken() === SyntaxKind.RegularExpressionLiteral) { + token = SyntaxKind.RegularExpressionLiteral; + } + } + else if (lastNonTriviaToken === SyntaxKind.DotToken && isKeyword(token)) { + token = SyntaxKind.Identifier; + } + else if (isKeyword(lastNonTriviaToken) && isKeyword(token) && !canFollow(lastNonTriviaToken, token)) { + // We have two keywords in a row. Only treat the second as a keyword if + // it's a sequence that could legally occur in the language. Otherwise + // treat it as an identifier. This way, if someone writes "private var" + // we recognize that 'var' is actually an identifier here. + token = SyntaxKind.Identifier; + } + else if (lastNonTriviaToken === SyntaxKind.Identifier && + token === SyntaxKind.LessThanToken) { + // Could be the start of something generic. Keep track of that by bumping + // up the current count of generic contexts we may be in. + angleBracketStack++; + } + else if (token === SyntaxKind.GreaterThanToken && angleBracketStack > 0) { + // If we think we're currently in something generic, then mark that that + // generic entity is complete. + angleBracketStack--; + } + else if (token === SyntaxKind.AnyKeyword || + token === SyntaxKind.StringKeyword || + token === SyntaxKind.NumberKeyword || + token === SyntaxKind.BooleanKeyword || + token === SyntaxKind.SymbolKeyword) { + if (angleBracketStack > 0 && !syntacticClassifierAbsent) { + // If it looks like we're could be in something generic, don't classify this + // as a keyword. We may just get overwritten by the syntactic classifier, + // causing a noisy experience for the user. + token = SyntaxKind.Identifier; + } + } + else if (token === SyntaxKind.TemplateHead) { + templateStack.push(token); + } + else if (token === SyntaxKind.OpenBraceToken) { + // If we don't have anything on the template stack, + // then we aren't trying to keep track of a previously scanned template head. + if (templateStack.length > 0) { + templateStack.push(token); + } + } + else if (token === SyntaxKind.CloseBraceToken) { + // If we don't have anything on the template stack, + // then we aren't trying to keep track of a previously scanned template head. + if (templateStack.length > 0) { + const lastTemplateStackToken = lastOrUndefined(templateStack); + + if (lastTemplateStackToken === SyntaxKind.TemplateHead) { + token = scanner.reScanTemplateToken(); + + // Only pop on a TemplateTail; a TemplateMiddle indicates there is more for us. + if (token === SyntaxKind.TemplateTail) { + templateStack.pop(); + } + else { + Debug.assert(token === SyntaxKind.TemplateMiddle, "Should have been a template middle. Was " + token); + } + } + else { + Debug.assert(lastTemplateStackToken === SyntaxKind.OpenBraceToken, "Should have been an open brace. Was: " + token); + templateStack.pop(); + } + } + } + + lastNonTriviaToken = token; + } + + processToken(); + } + while (token !== SyntaxKind.EndOfFileToken); + + return result; + + function processToken(): void { + const start = scanner.getTokenPos(); + const end = scanner.getTextPos(); + + addResult(start, end, classFromKind(token)); + + if (end >= text.length) { + if (token === SyntaxKind.StringLiteral) { + // Check to see if we finished up on a multiline string literal. + const tokenText = scanner.getTokenText(); + if (scanner.isUnterminated()) { + const lastCharIndex = tokenText.length - 1; + + let numBackslashes = 0; + while (tokenText.charCodeAt(lastCharIndex - numBackslashes) === CharacterCodes.backslash) { + numBackslashes++; + } + + // If we have an odd number of backslashes, then the multiline string is unclosed + if (numBackslashes & 1) { + const quoteChar = tokenText.charCodeAt(0); + result.endOfLineState = quoteChar === CharacterCodes.doubleQuote + ? EndOfLineState.InDoubleQuoteStringLiteral + : EndOfLineState.InSingleQuoteStringLiteral; + } + } + } + else if (token === SyntaxKind.MultiLineCommentTrivia) { + // Check to see if the multiline comment was unclosed. + if (scanner.isUnterminated()) { + result.endOfLineState = EndOfLineState.InMultiLineCommentTrivia; + } + } + else if (isTemplateLiteralKind(token)) { + if (scanner.isUnterminated()) { + if (token === SyntaxKind.TemplateTail) { + result.endOfLineState = EndOfLineState.InTemplateMiddleOrTail; + } + else if (token === SyntaxKind.NoSubstitutionTemplateLiteral) { + result.endOfLineState = EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate; + } + else { + Debug.fail("Only 'NoSubstitutionTemplateLiteral's and 'TemplateTail's can be unterminated; got SyntaxKind #" + token); + } + } + } + else if (templateStack.length > 0 && lastOrUndefined(templateStack) === SyntaxKind.TemplateHead) { + result.endOfLineState = EndOfLineState.InTemplateSubstitutionPosition; + } + } + } + + function addResult(start: number, end: number, classification: ClassificationType): void { + if (classification === ClassificationType.whiteSpace) { + // Don't bother with whitespace classifications. They're not needed. + return; + } + + if (start === 0 && offset > 0) { + // We're classifying the first token, and this was a case where we prepended + // text. We should consider the start of this token to be at the start of + // the original text. + start += offset; + } + + // All our tokens are in relation to the augmented text. Move them back to be + // relative to the original text. + start -= offset; + end -= offset; + const length = end - start; + + if (length > 0) { + result.spans.push(start); + result.spans.push(length); + result.spans.push(classification); + } + } + } + + function isBinaryExpressionOperatorToken(token: SyntaxKind): boolean { + switch (token) { + case SyntaxKind.AsteriskToken: + case SyntaxKind.SlashToken: + case SyntaxKind.PercentToken: + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.LessThanLessThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + case SyntaxKind.LessThanToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.InstanceOfKeyword: + case SyntaxKind.InKeyword: + case SyntaxKind.AsKeyword: + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + case SyntaxKind.AmpersandToken: + case SyntaxKind.CaretToken: + case SyntaxKind.BarToken: + case SyntaxKind.AmpersandAmpersandToken: + case SyntaxKind.BarBarToken: + case SyntaxKind.BarEqualsToken: + case SyntaxKind.AmpersandEqualsToken: + case SyntaxKind.CaretEqualsToken: + case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.PlusEqualsToken: + case SyntaxKind.MinusEqualsToken: + case SyntaxKind.AsteriskEqualsToken: + case SyntaxKind.SlashEqualsToken: + case SyntaxKind.PercentEqualsToken: + case SyntaxKind.EqualsToken: + case SyntaxKind.CommaToken: + return true; + default: + return false; + } + } + + function isPrefixUnaryExpressionOperatorToken(token: SyntaxKind): boolean { + switch (token) { + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + case SyntaxKind.ExclamationToken: + case SyntaxKind.PlusPlusToken: + case SyntaxKind.MinusMinusToken: + return true; + default: + return false; + } + } + + function isKeyword(token: SyntaxKind): boolean { + return token >= SyntaxKind.FirstKeyword && token <= SyntaxKind.LastKeyword; + } + + function classFromKind(token: SyntaxKind): ClassificationType { + if (isKeyword(token)) { + return ClassificationType.keyword; + } + else if (isBinaryExpressionOperatorToken(token) || isPrefixUnaryExpressionOperatorToken(token)) { + return ClassificationType.operator; + } + else if (token >= SyntaxKind.FirstPunctuation && token <= SyntaxKind.LastPunctuation) { + return ClassificationType.punctuation; + } + + switch (token) { + case SyntaxKind.NumericLiteral: + return ClassificationType.numericLiteral; + case SyntaxKind.StringLiteral: + return ClassificationType.stringLiteral; + case SyntaxKind.RegularExpressionLiteral: + return ClassificationType.regularExpressionLiteral; + case SyntaxKind.ConflictMarkerTrivia: + case SyntaxKind.MultiLineCommentTrivia: + case SyntaxKind.SingleLineCommentTrivia: + return ClassificationType.comment; + case SyntaxKind.WhitespaceTrivia: + case SyntaxKind.NewLineTrivia: + return ClassificationType.whiteSpace; + case SyntaxKind.Identifier: + default: + if (isTemplateLiteralKind(token)) { + return ClassificationType.stringLiteral; + } + return ClassificationType.identifier; + } + } + + return { + getClassificationsForLine, + getEncodedLexicalClassifications + }; + } + + export function getSemanticClassifications(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFile: SourceFile, classifiableNames: Map, span: TextSpan): ClassifiedSpan[] { + return convertClassifications(getEncodedSemanticClassifications(typeChecker, cancellationToken, sourceFile, classifiableNames, span)); + } + + function checkForClassificationCancellation(cancellationToken: CancellationToken, kind: SyntaxKind) { + // We don't want to actually call back into our host on every node to find out if we've + // been canceled. That would be an enormous amount of chattyness, along with the all + // the overhead of marshalling the data to/from the host. So instead we pick a few + // reasonable node kinds to bother checking on. These node kinds represent high level + // constructs that we would expect to see commonly, but just at a far less frequent + // interval. + // + // For example, in checker.ts (around 750k) we only have around 600 of these constructs. + // That means we're calling back into the host around every 1.2k of the file we process. + // Lib.d.ts has similar numbers. + switch (kind) { + case SyntaxKind.ModuleDeclaration: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.FunctionDeclaration: + cancellationToken.throwIfCancellationRequested(); + } + } + + export function getEncodedSemanticClassifications(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFile: SourceFile, classifiableNames: Map, span: TextSpan): Classifications { + const result: number[] = []; + processNode(sourceFile); + + return { spans: result, endOfLineState: EndOfLineState.None }; + + function pushClassification(start: number, length: number, type: ClassificationType) { + result.push(start); + result.push(length); + result.push(type); + } + + function classifySymbol(symbol: Symbol, meaningAtPosition: Meaning.SemanticMeaning): ClassificationType { + const flags = symbol.getFlags(); + if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) { + return; + } + + if (flags & SymbolFlags.Class) { + return ClassificationType.className; + } + else if (flags & SymbolFlags.Enum) { + return ClassificationType.enumName; + } + else if (flags & SymbolFlags.TypeAlias) { + return ClassificationType.typeAliasName; + } + else if (meaningAtPosition & Meaning.SemanticMeaning.Type) { + if (flags & SymbolFlags.Interface) { + return ClassificationType.interfaceName; + } + else if (flags & SymbolFlags.TypeParameter) { + return ClassificationType.typeParameterName; + } + } + else if (flags & SymbolFlags.Module) { + // Only classify a module as such if + // - It appears in a namespace context. + // - There exists a module declaration which actually impacts the value side. + if (meaningAtPosition & Meaning.SemanticMeaning.Namespace || + (meaningAtPosition & Meaning.SemanticMeaning.Value && hasValueSideModule(symbol))) { + return ClassificationType.moduleName; + } + } + + return undefined; + + /** + * Returns true if there exists a module that introduces entities on the value side. + */ + function hasValueSideModule(symbol: Symbol): boolean { + return forEach(symbol.declarations, declaration => { + return declaration.kind === SyntaxKind.ModuleDeclaration && + getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated; + }); + } + } + + function processNode(node: Node) { + // Only walk into nodes that intersect the requested span. + if (node && textSpanIntersectsWith(span, node.getFullStart(), node.getFullWidth())) { + const kind = node.kind; + checkForClassificationCancellation(cancellationToken, kind); + + if (kind === SyntaxKind.Identifier && !nodeIsMissing(node)) { + const identifier = node; + + // Only bother calling into the typechecker if this is an identifier that + // could possibly resolve to a type name. This makes classification run + // in a third of the time it would normally take. + if (classifiableNames[identifier.text]) { + const symbol = typeChecker.getSymbolAtLocation(node); + if (symbol) { + const type = classifySymbol(symbol, Meaning.getMeaningFromLocation(node)); + if (type) { + pushClassification(node.getStart(), node.getWidth(), type); + } + } + } + } + + forEachChild(node, processNode); + } + } + } + + function getClassificationTypeName(type: ClassificationType) { + switch (type) { + case ClassificationType.comment: return ClassificationTypeNames.comment; + case ClassificationType.identifier: return ClassificationTypeNames.identifier; + case ClassificationType.keyword: return ClassificationTypeNames.keyword; + case ClassificationType.numericLiteral: return ClassificationTypeNames.numericLiteral; + case ClassificationType.operator: return ClassificationTypeNames.operator; + case ClassificationType.stringLiteral: return ClassificationTypeNames.stringLiteral; + case ClassificationType.whiteSpace: return ClassificationTypeNames.whiteSpace; + case ClassificationType.text: return ClassificationTypeNames.text; + case ClassificationType.punctuation: return ClassificationTypeNames.punctuation; + case ClassificationType.className: return ClassificationTypeNames.className; + case ClassificationType.enumName: return ClassificationTypeNames.enumName; + case ClassificationType.interfaceName: return ClassificationTypeNames.interfaceName; + case ClassificationType.moduleName: return ClassificationTypeNames.moduleName; + case ClassificationType.typeParameterName: return ClassificationTypeNames.typeParameterName; + case ClassificationType.typeAliasName: return ClassificationTypeNames.typeAliasName; + case ClassificationType.parameterName: return ClassificationTypeNames.parameterName; + case ClassificationType.docCommentTagName: return ClassificationTypeNames.docCommentTagName; + case ClassificationType.jsxOpenTagName: return ClassificationTypeNames.jsxOpenTagName; + case ClassificationType.jsxCloseTagName: return ClassificationTypeNames.jsxCloseTagName; + case ClassificationType.jsxSelfClosingTagName: return ClassificationTypeNames.jsxSelfClosingTagName; + case ClassificationType.jsxAttribute: return ClassificationTypeNames.jsxAttribute; + case ClassificationType.jsxText: return ClassificationTypeNames.jsxText; + case ClassificationType.jsxAttributeStringLiteralValue: return ClassificationTypeNames.jsxAttributeStringLiteralValue; + } + } + + function convertClassifications(classifications: Classifications): ClassifiedSpan[] { + Debug.assert(classifications.spans.length % 3 === 0); + const dense = classifications.spans; + const result: ClassifiedSpan[] = []; + for (let i = 0, n = dense.length; i < n; i += 3) { + result.push({ + textSpan: createTextSpan(dense[i], dense[i + 1]), + classificationType: getClassificationTypeName(dense[i + 2]) + }); + } + + return result; + } + + export function getSyntacticClassifications(cancellationToken: CancellationToken, sourceFile: SourceFile, span: TextSpan): ClassifiedSpan[] { + return convertClassifications(getEncodedSyntacticClassifications(cancellationToken, sourceFile, span)); + } + + export function getEncodedSyntacticClassifications(cancellationToken: CancellationToken, sourceFile: SourceFile, span: TextSpan): Classifications { + const spanStart = span.start; + const spanLength = span.length; + + // Make a scanner we can get trivia from. + const triviaScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, sourceFile.languageVariant, sourceFile.text); + const mergeConflictScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, sourceFile.languageVariant, sourceFile.text); + + const result: number[] = []; + processElement(sourceFile); + + return { spans: result, endOfLineState: EndOfLineState.None }; + + function pushClassification(start: number, length: number, type: ClassificationType) { + result.push(start); + result.push(length); + result.push(type); + } + + function classifyLeadingTriviaAndGetTokenStart(token: Node): number { + triviaScanner.setTextPos(token.pos); + while (true) { + const start = triviaScanner.getTextPos(); + // only bother scanning if we have something that could be trivia. + if (!couldStartTrivia(sourceFile.text, start)) { + return start; + } + + const kind = triviaScanner.scan(); + const end = triviaScanner.getTextPos(); + const width = end - start; + + // The moment we get something that isn't trivia, then stop processing. + if (!isTrivia(kind)) { + return start; + } + + // Don't bother with newlines/whitespace. + if (kind === SyntaxKind.NewLineTrivia || kind === SyntaxKind.WhitespaceTrivia) { + continue; + } + + // Only bother with the trivia if it at least intersects the span of interest. + if (isComment(kind)) { + classifyComment(token, kind, start, width); + + // Classifying a comment might cause us to reuse the trivia scanner + // (because of jsdoc comments). So after we classify the comment make + // sure we set the scanner position back to where it needs to be. + triviaScanner.setTextPos(end); + continue; + } + + if (kind === SyntaxKind.ConflictMarkerTrivia) { + const text = sourceFile.text; + const ch = text.charCodeAt(start); + + // for the <<<<<<< and >>>>>>> markers, we just add them in as comments + // in the classification stream. + if (ch === CharacterCodes.lessThan || ch === CharacterCodes.greaterThan) { + pushClassification(start, width, ClassificationType.comment); + continue; + } + + // for the ======== add a comment for the first line, and then lex all + // subsequent lines up until the end of the conflict marker. + Debug.assert(ch === CharacterCodes.equals); + classifyDisabledMergeCode(text, start, end); + } + } + } + + function classifyComment(token: Node, kind: SyntaxKind, start: number, width: number) { + if (kind === SyntaxKind.MultiLineCommentTrivia) { + // See if this is a doc comment. If so, we'll classify certain portions of it + // specially. + const docCommentAndDiagnostics = parseIsolatedJSDocComment(sourceFile.text, start, width); + if (docCommentAndDiagnostics && docCommentAndDiagnostics.jsDocComment) { + docCommentAndDiagnostics.jsDocComment.parent = token; + classifyJSDocComment(docCommentAndDiagnostics.jsDocComment); + return; + } + } + + // Simple comment. Just add as is. + pushCommentRange(start, width); + } + + function pushCommentRange(start: number, width: number) { + pushClassification(start, width, ClassificationType.comment); + } + + function classifyJSDocComment(docComment: JSDocComment) { + let pos = docComment.pos; + + for (const tag of docComment.tags) { + // As we walk through each tag, classify the portion of text from the end of + // the last tag (or the start of the entire doc comment) as 'comment'. + if (tag.pos !== pos) { + pushCommentRange(pos, tag.pos - pos); + } + + pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); + pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); + + pos = tag.tagName.end; + + switch (tag.kind) { + case SyntaxKind.JSDocParameterTag: + processJSDocParameterTag(tag); + break; + case SyntaxKind.JSDocTemplateTag: + processJSDocTemplateTag(tag); + break; + case SyntaxKind.JSDocTypeTag: + processElement((tag).typeExpression); + break; + case SyntaxKind.JSDocReturnTag: + processElement((tag).typeExpression); + break; + } + + pos = tag.end; + } + + if (pos !== docComment.end) { + pushCommentRange(pos, docComment.end - pos); + } + + return; + + function processJSDocParameterTag(tag: JSDocParameterTag) { + if (tag.preParameterName) { + pushCommentRange(pos, tag.preParameterName.pos - pos); + pushClassification(tag.preParameterName.pos, tag.preParameterName.end - tag.preParameterName.pos, ClassificationType.parameterName); + pos = tag.preParameterName.end; + } + + if (tag.typeExpression) { + pushCommentRange(pos, tag.typeExpression.pos - pos); + processElement(tag.typeExpression); + pos = tag.typeExpression.end; + } + + if (tag.postParameterName) { + pushCommentRange(pos, tag.postParameterName.pos - pos); + pushClassification(tag.postParameterName.pos, tag.postParameterName.end - tag.postParameterName.pos, ClassificationType.parameterName); + pos = tag.postParameterName.end; + } + } + } + + function processJSDocTemplateTag(tag: JSDocTemplateTag) { + for (const child of tag.getChildren()) { + processElement(child); + } + } + + function classifyDisabledMergeCode(text: string, start: number, end: number) { + // Classify the line that the ======= marker is on as a comment. Then just lex + // all further tokens and add them to the result. + let i: number; + for (i = start; i < end; i++) { + if (isLineBreak(text.charCodeAt(i))) { + break; + } + } + pushClassification(start, i - start, ClassificationType.comment); + mergeConflictScanner.setTextPos(i); + + while (mergeConflictScanner.getTextPos() < end) { + classifyDisabledCodeToken(); + } + } + + function classifyDisabledCodeToken() { + const start = mergeConflictScanner.getTextPos(); + const tokenKind = mergeConflictScanner.scan(); + const end = mergeConflictScanner.getTextPos(); + + const type = classifyTokenType(tokenKind); + if (type) { + pushClassification(start, end - start, type); + } + } + + /** + * Returns true if node should be treated as classified and no further processing is required. + * False will mean that node is not classified and traverse routine should recurse into node contents. + */ + function tryClassifyNode(node: Node): boolean { + if (isJSDocTag(node)) { + return true; + } + + if (nodeIsMissing(node)) { + return true; + } + + const classifiedElementName = tryClassifyJsxElementName(node); + if (!isToken(node) && node.kind !== SyntaxKind.JsxText && classifiedElementName === undefined) { + return false; + } + + const tokenStart = node.kind === SyntaxKind.JsxText ? node.pos : classifyLeadingTriviaAndGetTokenStart(node); + + const tokenWidth = node.end - tokenStart; + Debug.assert(tokenWidth >= 0); + if (tokenWidth > 0) { + const type = classifiedElementName || classifyTokenType(node.kind, node); + if (type) { + pushClassification(tokenStart, tokenWidth, type); + } + } + + return true; + } + + function tryClassifyJsxElementName(token: Node): ClassificationType { + switch (token.parent && token.parent.kind) { + case SyntaxKind.JsxOpeningElement: + if ((token.parent).tagName === token) { + return ClassificationType.jsxOpenTagName; + } + break; + case SyntaxKind.JsxClosingElement: + if ((token.parent).tagName === token) { + return ClassificationType.jsxCloseTagName; + } + break; + case SyntaxKind.JsxSelfClosingElement: + if ((token.parent).tagName === token) { + return ClassificationType.jsxSelfClosingTagName; + } + break; + case SyntaxKind.JsxAttribute: + if ((token.parent).name === token) { + return ClassificationType.jsxAttribute; + } + break; + } + return undefined; + } + + // for accurate classification, the actual token should be passed in. however, for + // cases like 'disabled merge code' classification, we just get the token kind and + // classify based on that instead. + function classifyTokenType(tokenKind: SyntaxKind, token?: Node): ClassificationType { + if (isKeyword(tokenKind)) { + return ClassificationType.keyword; + } + + // Special case < and > If they appear in a generic context they are punctuation, + // not operators. + if (tokenKind === SyntaxKind.LessThanToken || tokenKind === SyntaxKind.GreaterThanToken) { + // If the node owning the token has a type argument list or type parameter list, then + // we can effectively assume that a '<' and '>' belong to those lists. + if (token && getTypeArgumentOrTypeParameterList(token.parent)) { + return ClassificationType.punctuation; + } + } + + if (isPunctuation(tokenKind)) { + if (token) { + if (tokenKind === SyntaxKind.EqualsToken) { + // the '=' in a variable declaration is special cased here. + if (token.parent.kind === SyntaxKind.VariableDeclaration || + token.parent.kind === SyntaxKind.PropertyDeclaration || + token.parent.kind === SyntaxKind.Parameter || + token.parent.kind === SyntaxKind.JsxAttribute) { + return ClassificationType.operator; + } + } + + if (token.parent.kind === SyntaxKind.BinaryExpression || + token.parent.kind === SyntaxKind.PrefixUnaryExpression || + token.parent.kind === SyntaxKind.PostfixUnaryExpression || + token.parent.kind === SyntaxKind.ConditionalExpression) { + return ClassificationType.operator; + } + } + + return ClassificationType.punctuation; + } + else if (tokenKind === SyntaxKind.NumericLiteral) { + return ClassificationType.numericLiteral; + } + else if (tokenKind === SyntaxKind.StringLiteral) { + return token.parent.kind === SyntaxKind.JsxAttribute ? ClassificationType.jsxAttributeStringLiteralValue : ClassificationType.stringLiteral; + } + else if (tokenKind === SyntaxKind.RegularExpressionLiteral) { + // TODO: we should get another classification type for these literals. + return ClassificationType.stringLiteral; + } + else if (isTemplateLiteralKind(tokenKind)) { + // TODO (drosen): we should *also* get another classification type for these literals. + return ClassificationType.stringLiteral; + } + else if (tokenKind === SyntaxKind.JsxText) { + return ClassificationType.jsxText; + } + else if (tokenKind === SyntaxKind.Identifier) { + if (token) { + switch (token.parent.kind) { + case SyntaxKind.ClassDeclaration: + if ((token.parent).name === token) { + return ClassificationType.className; + } + return; + case SyntaxKind.TypeParameter: + if ((token.parent).name === token) { + return ClassificationType.typeParameterName; + } + return; + case SyntaxKind.InterfaceDeclaration: + if ((token.parent).name === token) { + return ClassificationType.interfaceName; + } + return; + case SyntaxKind.EnumDeclaration: + if ((token.parent).name === token) { + return ClassificationType.enumName; + } + return; + case SyntaxKind.ModuleDeclaration: + if ((token.parent).name === token) { + return ClassificationType.moduleName; + } + return; + case SyntaxKind.Parameter: + if ((token.parent).name === token) { + const isThis = token.kind === SyntaxKind.Identifier && (token).originalKeywordKind === SyntaxKind.ThisKeyword; + return isThis ? ClassificationType.keyword : ClassificationType.parameterName; + } + return; + } + } + return ClassificationType.identifier; + } + } + + function processElement(element: Node) { + if (!element) { + return; + } + + // Ignore nodes that don't intersect the original span to classify. + if (decodedTextSpanIntersectsWith(spanStart, spanLength, element.pos, element.getFullWidth())) { + checkForClassificationCancellation(cancellationToken, element.kind); + + const children = element.getChildren(sourceFile); + for (let i = 0, n = children.length; i < n; i++) { + const child = children[i]; + if (!tryClassifyNode(child)) { + // Recurse into our child nodes. + processElement(child); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/services/completions.ts b/src/services/completions.ts new file mode 100644 index 00000000000..6ea24e36025 --- /dev/null +++ b/src/services/completions.ts @@ -0,0 +1,1647 @@ +/* @internal */ +namespace ts.Completions { + export function getCompletionsAtPosition(host: LanguageServiceHost, typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number): CompletionInfo { + if (isInReferenceComment(sourceFile, position)) { + return getTripleSlashReferenceCompletion(sourceFile, position); + } + + if (isInString(sourceFile, position)) { + return getStringLiteralCompletionEntries(sourceFile, position); + } + + const completionData = getCompletionData(typeChecker, log, sourceFile, position); + if (!completionData) { + return undefined; + } + + const { symbols, isMemberCompletion, isNewIdentifierLocation, location, isJsDocTagName } = completionData; + + if (isJsDocTagName) { + // If the current position is a jsDoc tag name, only tag names should be provided for completion + return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: JsDoc.getAllJsDocCompletionEntries() }; + } + + const entries: CompletionEntry[] = []; + + if (isSourceFileJavaScript(sourceFile)) { + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ false); + addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames)); + } + else { + if (!symbols || symbols.length === 0) { + if (sourceFile.languageVariant === LanguageVariant.JSX && + location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) { + // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, + // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. + // For example: + // var x =
completion list at "1" will contain "div" with type any + const tagName = (location.parent.parent).openingElement.tagName; + entries.push({ + name: (tagName).text, + kind: undefined, + kindModifiers: undefined, + sortText: "0", + }); + } + else { + return undefined; + } + } + + getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true); + } + + // Add keywords if this is not a member completion list + if (!isMemberCompletion && !isJsDocTagName) { + addRange(entries, keywordCompletions); + } + + return { isMemberCompletion, isNewIdentifierLocation: isNewIdentifierLocation || isSourceFileJavaScript(sourceFile), entries }; + + function getJavaScriptCompletionEntries(sourceFile: SourceFile, position: number, uniqueNames: Map): CompletionEntry[] { + const entries: CompletionEntry[] = []; + + const nameTable = getNameTable(sourceFile); + for (const name in nameTable) { + // Skip identifiers produced only from the current location + if (nameTable[name] === position) { + continue; + } + + if (!uniqueNames[name]) { + uniqueNames[name] = name; + const displayName = getCompletionEntryDisplayName(unescapeIdentifier(name), compilerOptions.target, /*performCharacterChecks*/ true); + if (displayName) { + const entry = { + name: displayName, + kind: ScriptElementKind.warning, + kindModifiers: "", + sortText: "1" + }; + entries.push(entry); + } + } + } + + return entries; + } + + function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean): CompletionEntry { + // Try to get a valid display name for this symbol, if we could not find one, then ignore it. + // We would like to only show things that can be added after a dot, so for instance numeric properties can + // not be accessed with a dot (a.1 <- invalid) + const displayName = getCompletionEntryDisplayNameForSymbol(typeChecker, symbol, compilerOptions.target, performCharacterChecks, location); + if (!displayName) { + return undefined; + } + + // TODO(drosen): Right now we just permit *all* semantic meanings when calling + // 'getSymbolKind' which is permissible given that it is backwards compatible; but + // really we should consider passing the meaning for the node so that we don't report + // that a suggestion for a value is an interface. We COULD also just do what + // 'getSymbolModifiers' does, which is to use the first declaration. + + // Use a 'sortText' of 0' so that all symbol completion entries come before any other + // entries (like JavaScript identifier entries). + return { + name: displayName, + kind: SymbolDisplay.getSymbolKind(typeChecker, symbol, location), + kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), + sortText: "0", + }; + + } + + function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[], location: Node, performCharacterChecks: boolean): Map { + const start = timestamp(); + const uniqueNames = createMap(); + if (symbols) { + for (const symbol of symbols) { + const entry = createCompletionEntry(symbol, location, performCharacterChecks); + if (entry) { + const id = escapeIdentifier(entry.name); + if (!uniqueNames[id]) { + entries.push(entry); + uniqueNames[id] = id; + } + } + } + } + + log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); + return uniqueNames; + } + + function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number) { + const node = findPrecedingToken(position, sourceFile); + if (!node || node.kind !== SyntaxKind.StringLiteral) { + return undefined; + } + + if (node.parent.kind === SyntaxKind.PropertyAssignment && node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression) { + // Get quoted name of properties of the object literal expression + // i.e. interface ConfigFiles { + // 'jspm:dev': string + // } + // let files: ConfigFiles = { + // '/*completion position*/' + // } + // + // function foo(c: ConfigFiles) {} + // foo({ + // '/*completion position*/' + // }); + return getStringLiteralCompletionEntriesFromPropertyAssignment(node.parent); + } + else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) { + // Get all names of properties on the expression + // i.e. interface A { + // 'prop1': string + // } + // let a: A; + // a['/*completion position*/'] + return getStringLiteralCompletionEntriesFromElementAccess(node.parent); + } + else if (node.parent.kind === SyntaxKind.ImportDeclaration || isExpressionOfExternalModuleImportEqualsDeclaration(node) || isRequireCall(node.parent, false)) { + // Get all known external module names or complete a path to a module + // i.e. import * as ns from "/*completion position*/"; + // import x = require("/*completion position*/"); + // var y = require("/*completion position*/"); + return getStringLiteralCompletionEntriesFromModuleNames(node); + } + else { + const argumentInfo = SignatureHelp.getContainingArgumentInfo(node, position, sourceFile); + if (argumentInfo) { + // Get string literal completions from specialized signatures of the target + // i.e. declare function f(a: 'A'); + // f("/*completion position*/") + return getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, node); + } + + // Get completion for string literal from string literal type + // i.e. var x: "hi" | "hello" = "/*completion position*/" + return getStringLiteralCompletionEntriesFromContextualType(node); + } + } + + function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement) { + const type = typeChecker.getContextualType((element.parent)); + const entries: CompletionEntry[] = []; + if (type) { + getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/false); + if (entries.length) { + return { isMemberCompletion: true, isNewIdentifierLocation: true, entries }; + } + } + } + + function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, location: Node) { + const candidates: Signature[] = []; + const entries: CompletionEntry[] = []; + + typeChecker.getResolvedSignature(argumentInfo.invocation, candidates); + + for (const candidate of candidates) { + if (candidate.parameters.length > argumentInfo.argumentIndex) { + const parameter = candidate.parameters[argumentInfo.argumentIndex]; + addStringLiteralCompletionsFromType(typeChecker.getTypeAtLocation(parameter.valueDeclaration), entries); + } + } + + if (entries.length) { + return { isMemberCompletion: false, isNewIdentifierLocation: true, entries }; + } + + return undefined; + } + + function getStringLiteralCompletionEntriesFromElementAccess(node: ElementAccessExpression) { + const type = typeChecker.getTypeAtLocation(node.expression); + const entries: CompletionEntry[] = []; + if (type) { + getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/false); + if (entries.length) { + return { isMemberCompletion: true, isNewIdentifierLocation: true, entries }; + } + } + return undefined; + } + + function getStringLiteralCompletionEntriesFromContextualType(node: StringLiteral) { + const type = typeChecker.getContextualType(node); + if (type) { + const entries: CompletionEntry[] = []; + addStringLiteralCompletionsFromType(type, entries); + if (entries.length) { + return { isMemberCompletion: false, isNewIdentifierLocation: false, entries }; + } + } + return undefined; + } + + function addStringLiteralCompletionsFromType(type: Type, result: CompletionEntry[]): void { + if (!type) { + return; + } + if (type.flags & TypeFlags.Union) { + forEach((type).types, t => addStringLiteralCompletionsFromType(t, result)); + } + else { + if (type.flags & TypeFlags.StringLiteral) { + result.push({ + name: (type).text, + kindModifiers: ScriptElementKindModifier.none, + kind: ScriptElementKind.variableElement, + sortText: "0" + }); + } + } + } + + function getStringLiteralCompletionEntriesFromModuleNames(node: StringLiteral): CompletionInfo { + const literalValue = normalizeSlashes(node.text); + + const scriptPath = node.getSourceFile().path; + const scriptDirectory = getDirectoryPath(scriptPath); + + const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); + let entries: CompletionEntry[]; + if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { + if (compilerOptions.rootDirs) { + entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( + compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + } + else { + entries = getCompletionEntriesForDirectoryFragment( + literalValue, scriptDirectory, getSupportedExtensions(compilerOptions), /*includeExtensions*/false, span, scriptPath); + } + } + else { + // Check for node modules + entries = getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, span); + } + return { + isMemberCompletion: false, + isNewIdentifierLocation: true, + entries + }; + } + + /** + * Takes a script path and returns paths for all potential folders that could be merged with its + * containing folder via the "rootDirs" compiler option + */ + function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptPath: string, ignoreCase: boolean): string[] { + // Make all paths absolute/normalized if they are not already + rootDirs = map(rootDirs, rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); + + // Determine the path to the directory containing the script relative to the root directory it is contained within + let relativeDirectory: string; + for (const rootDirectory of rootDirs) { + if (containsPath(rootDirectory, scriptPath, basePath, ignoreCase)) { + relativeDirectory = scriptPath.substr(rootDirectory.length); + break; + } + } + + // Now find a path for each potential directory that is to be merged with the one containing the script + return deduplicate(map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory))); + } + + function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, exclude?: string): CompletionEntry[] { + const basePath = compilerOptions.project || host.getCurrentDirectory(); + const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); + const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptPath, ignoreCase); + + const result: CompletionEntry[] = []; + + for (const baseDirectory of baseDirectories) { + getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensions, includeExtensions, span, exclude, result); + } + + return result; + } + + function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] { + fragment = getDirectoryPath(fragment); + if (!fragment) { + fragment = "./"; + } + else { + fragment = ensureTrailingDirectorySeparator(fragment); + } + + const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); + const baseDirectory = getDirectoryPath(absolutePath); + const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); + + if (directoryProbablyExists(baseDirectory, host)) { + if (host.readDirectory) { + // Enumerate the available files if possible + const files = host.readDirectory(baseDirectory, extensions, /*exclude*/undefined, /*include*/["./*"]); + const foundFiles = createMap(); + for (let filePath of files) { + filePath = normalizePath(filePath); + if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) { + continue; + } + + const foundFileName = includeExtensions ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath)); + + if (!foundFiles[foundFileName]) { + foundFiles[foundFileName] = true; + } + } + + for (const foundFile in foundFiles) { + result.push(createCompletionEntryForModule(foundFile, ScriptElementKind.scriptElement, span)); + } + } + + // If possible, get folder completion as well + if (host.getDirectories) { + const directories = host.getDirectories(baseDirectory); + for (const directory of directories) { + const directoryName = getBaseFileName(normalizePath(directory)); + + result.push(createCompletionEntryForModule(directoryName, ScriptElementKind.directory, span)); + } + } + } + + return result; + } + + /** + * Check all of the declared modules and those in node modules. Possible sources of modules: + * Modules that are found by the type checker + * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) + * Modules from node_modules (i.e. those listed in package.json) + * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions + */ + function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan): CompletionEntry[] { + const { baseUrl, paths } = compilerOptions; + + let result: CompletionEntry[]; + + if (baseUrl) { + const fileExtensions = getSupportedExtensions(compilerOptions); + const projectDir = compilerOptions.project || host.getCurrentDirectory(); + const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl); + result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span); + + if (paths) { + for (const path in paths) { + if (paths.hasOwnProperty(path)) { + if (path === "*") { + if (paths[path]) { + for (const pattern of paths[path]) { + for (const match of getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions)) { + result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName, span)); + } + } + } + } + else if (startsWith(path, fragment)) { + const entry = paths[path] && paths[path].length === 1 && paths[path][0]; + if (entry) { + result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName, span)); + } + } + } + } + } + } + else { + result = []; + } + + getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span, result); + + for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions)) { + result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); + } + + return result; + } + + function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: string[]): string[] { + if (host.readDirectory) { + const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; + if (parsed) { + // The prefix has two effective parts: the directory path and the base component after the filepath that is not a + // full directory component. For example: directory/path/of/prefix/base* + const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix); + const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); + const normalizedPrefixBase = getBaseFileName(normalizedPrefix); + + const fragmentHasPath = fragment.indexOf(directorySeparator) !== -1; + + // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call + const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory; + + const normalizedSuffix = normalizePath(parsed.suffix); + const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory); + const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; + + // If we have a suffix, then we need to read the directory all the way down. We could create a glob + // that encodes the suffix, but we would have to escape the character "?" which readDirectory + // doesn't support. For now, this is safer but slower + const includeGlob = normalizedSuffix ? "**/*" : "./*"; + + const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]); + const result: string[] = []; + + // Trim away prefix and suffix + for (const match of matches) { + const normalizedMatch = normalizePath(match); + if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) { + continue; + } + + const start = completePrefix.length; + const length = normalizedMatch.length - start - normalizedSuffix.length; + + result.push(removeFileExtension(normalizedMatch.substr(start, length))); + } + return result; + } + } + + return undefined; + } + + function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions): string[] { + // Check If this is a nested module + const isNestedModule = fragment.indexOf(directorySeparator) !== -1; + const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined; + + // Get modules that the type checker picked up + const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name)); + let nonRelativeModules = filter(ambientModules, moduleName => startsWith(moduleName, fragment)); + + // Nested modules of the form "module-name/sub" need to be adjusted to only return the string + // after the last '/' that appears in the fragment because that's where the replacement span + // starts + if (isNestedModule) { + const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment); + nonRelativeModules = map(nonRelativeModules, moduleName => { + if (startsWith(fragment, moduleNameWithSeperator)) { + return moduleName.substr(moduleNameWithSeperator.length); + } + return moduleName; + }); + } + + + if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) { + for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) { + if (!isNestedModule) { + nonRelativeModules.push(visibleModule.moduleName); + } + else if (host.readDirectory && startsWith(visibleModule.moduleName, moduleNameFragment)) { + const nestedFiles = host.readDirectory(visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/undefined, /*include*/["./*"]); + + for (let f of nestedFiles) { + f = normalizePath(f); + const nestedModule = removeFileExtension(getBaseFileName(f)); + nonRelativeModules.push(nestedModule); + } + } + } + } + + return deduplicate(nonRelativeModules); + } + + function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number): CompletionInfo { + const token = getTokenAtPosition(sourceFile, position); + if (!token) { + return undefined; + } + const commentRanges: CommentRange[] = getLeadingCommentRanges(sourceFile.text, token.pos); + + if (!commentRanges || !commentRanges.length) { + return undefined; + } + + const range = forEach(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end && commentRange); + + if (!range) { + return undefined; + } + + const text = sourceFile.text.substr(range.pos, position - range.pos); + + const match = tripleSlashDirectiveFragmentRegex.exec(text); + if (match) { + const prefix = match[1]; + const kind = match[2]; + const toComplete = match[3]; + + const scriptPath = getDirectoryPath(sourceFile.path); + let entries: CompletionEntry[]; + if (kind === "path") { + // Give completions for a relative path + const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); + entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(compilerOptions), /*includeExtensions*/true, span, sourceFile.path); + } + else { + // Give completions based on the typings available + const span: TextSpan = { start: range.pos + prefix.length, length: match[0].length - prefix.length }; + entries = getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, span); + } + + return { + isMemberCompletion: false, + isNewIdentifierLocation: true, + entries + }; + } + + return undefined; + } + + function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] { + // Check for typings specified in compiler options + if (options.types) { + for (const moduleName of options.types) { + result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); + } + } + else if (host.getDirectories) { + const typeRoots = getEffectiveTypeRoots(options, host); + for (const root of typeRoots) { + getCompletionEntriesFromDirectories(host, options, root, span, result); + } + } + + if (host.getDirectories) { + // Also get all @types typings installed in visible node_modules directories + for (const package of findPackageJsons(scriptPath)) { + const typesDir = combinePaths(getDirectoryPath(package), "node_modules/@types"); + getCompletionEntriesFromDirectories(host, options, typesDir, span, result); + } + } + + return result; + } + + function getCompletionEntriesFromDirectories(host: LanguageServiceHost, options: CompilerOptions, directory: string, span: TextSpan, result: CompletionEntry[]) { + if (host.getDirectories && directoryProbablyExists(directory, host)) { + for (let typeDirectory of host.getDirectories(directory)) { + typeDirectory = normalizePath(typeDirectory); + result.push(createCompletionEntryForModule(getBaseFileName(typeDirectory), ScriptElementKind.externalModuleName, span)); + } + } + } + + function findPackageJsons(currentDir: string): string[] { + const paths: string[] = []; + let currentConfigPath: string; + while (true) { + currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json"); + if (currentConfigPath) { + paths.push(currentConfigPath); + + currentDir = getDirectoryPath(currentConfigPath); + const parent = getDirectoryPath(currentDir); + if (currentDir === parent) { + break; + } + currentDir = parent; + } + else { + break; + } + } + + return paths; + } + + + function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) { + const result: VisibleModuleInfo[] = []; + + if (host.readFile && host.fileExists) { + for (const packageJson of findPackageJsons(scriptPath)) { + const package = tryReadingPackageJson(packageJson); + if (!package) { + return; + } + + const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); + const foundModuleNames: string[] = []; + + // Provide completions for all non @types dependencies + for (const key of nodeModulesDependencyKeys) { + addPotentialPackageNames(package[key], foundModuleNames); + } + + for (const moduleName of foundModuleNames) { + const moduleDir = combinePaths(nodeModulesDir, moduleName); + result.push({ + moduleName, + moduleDir + }); + } + } + } + + return result; + + function tryReadingPackageJson(filePath: string) { + try { + const fileText = host.readFile(filePath); + return JSON.parse(fileText); + } + catch (e) { + return undefined; + } + } + + function addPotentialPackageNames(dependencies: any, result: string[]) { + if (dependencies) { + for (const dep in dependencies) { + if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) { + result.push(dep); + } + } + } + } + } + + function createCompletionEntryForModule(name: string, kind: string, replacementSpan: TextSpan): CompletionEntry { + return { name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: name, replacementSpan }; + } + + // Replace everything after the last directory seperator that appears + function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan { + const index = text.lastIndexOf(directorySeparator); + const offset = index !== -1 ? index + 1 : 0; + return { start: textStart + offset, length: text.length - offset }; + } + + // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..) + function isPathRelativeToScript(path: string) { + if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) { + const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1; + const slashCharCode = path.charCodeAt(slashIndex); + return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash; + } + return false; + } + + function normalizeAndPreserveTrailingSlash(path: string) { + return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalizePath(path)) : normalizePath(path); + } + } + + export function getCompletionEntryDetails(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): CompletionEntryDetails { + // Compute all the completion symbols again. + const completionData = getCompletionData(typeChecker, log, sourceFile, position); + if (completionData) { + const { symbols, location } = completionData; + + // Find the symbol with the matching entry name. + // We don't need to perform character checks here because we're only comparing the + // name against 'entryName' (which is known to be good), not building a new + // completion entry. + const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(typeChecker, s, compilerOptions.target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined); + + if (symbol) { + const { displayParts, documentation, symbolKind } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, Meaning.SemanticMeaning.All); + return { + name: entryName, + kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), + kind: symbolKind, + displayParts, + documentation + }; + } + } + + // Didn't find a symbol with this name. See if we can find a keyword instead. + const keywordCompletion = forEach(keywordCompletions, c => c.name === entryName); + if (keywordCompletion) { + return { + name: entryName, + kind: ScriptElementKind.keyword, + kindModifiers: ScriptElementKindModifier.none, + displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)], + documentation: undefined + }; + } + + return undefined; + } + + function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number) { + const isJavaScriptFile = isSourceFileJavaScript(sourceFile); + + let isJsDocTagName = false; + + let start = timestamp(); + const currentToken = getTokenAtPosition(sourceFile, position); + log("getCompletionData: Get current token: " + (timestamp() - start)); + + start = timestamp(); + // Completion not allowed inside comments, bail out if this is the case + const insideComment = isInsideComment(sourceFile, currentToken, position); + log("getCompletionData: Is inside comment: " + (timestamp() - start)); + + if (insideComment) { + // The current position is next to the '@' sign, when no tag name being provided yet. + // Provide a full list of tag names + if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { + isJsDocTagName = true; + } + + // Completion should work inside certain JsDoc tags. For example: + // /** @type {number | string} */ + // Completion should work in the brackets + let insideJsDocTagExpression = false; + const tag = getJsDocTagAtPosition(sourceFile, position); + if (tag) { + if (tag.tagName.pos <= position && position <= tag.tagName.end) { + isJsDocTagName = true; + } + + switch (tag.kind) { + case SyntaxKind.JSDocTypeTag: + case SyntaxKind.JSDocParameterTag: + case SyntaxKind.JSDocReturnTag: + const tagWithExpression = tag; + if (tagWithExpression.typeExpression) { + insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end; + } + break; + } + } + + if (isJsDocTagName) { + return { symbols: undefined, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName }; + } + + if (!insideJsDocTagExpression) { + // Proceed if the current position is in jsDoc tag expression; otherwise it is a normal + // comment or the plain text part of a jsDoc comment, so no completion should be available + log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment."); + return undefined; + } + } + + start = timestamp(); + const previousToken = findPrecedingToken(position, sourceFile); + log("getCompletionData: Get previous token 1: " + (timestamp() - start)); + + // The decision to provide completion depends on the contextToken, which is determined through the previousToken. + // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file + let contextToken = previousToken; + + // Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS| + // Skip this partial identifier and adjust the contextToken to the token that precedes it. + if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) { + const start = timestamp(); + contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile); + log("getCompletionData: Get previous token 2: " + (timestamp() - start)); + } + + // Find the node where completion is requested on. + // Also determine whether we are trying to complete with members of that node + // or attributes of a JSX tag. + let node = currentToken; + let isRightOfDot = false; + let isRightOfOpenTag = false; + let isStartingCloseTag = false; + + let location = getTouchingPropertyName(sourceFile, position); + if (contextToken) { + // Bail out if this is a known invalid completion location + if (isCompletionListBlocker(contextToken)) { + log("Returning an empty list because completion was requested in an invalid position."); + return undefined; + } + + const { parent, kind } = contextToken; + if (kind === SyntaxKind.DotToken) { + if (parent.kind === SyntaxKind.PropertyAccessExpression) { + node = (contextToken.parent).expression; + isRightOfDot = true; + } + else if (parent.kind === SyntaxKind.QualifiedName) { + node = (contextToken.parent).left; + isRightOfDot = true; + } + else { + // There is nothing that precedes the dot, so this likely just a stray character + // or leading into a '...' token. Just bail out instead. + return undefined; + } + } + else if (sourceFile.languageVariant === LanguageVariant.JSX) { + if (kind === SyntaxKind.LessThanToken) { + isRightOfOpenTag = true; + location = contextToken; + } + else if (kind === SyntaxKind.SlashToken && contextToken.parent.kind === SyntaxKind.JsxClosingElement) { + isStartingCloseTag = true; + location = contextToken; + } + } + } + + const semanticStart = timestamp(); + let isMemberCompletion: boolean; + let isNewIdentifierLocation: boolean; + let symbols: Symbol[] = []; + + if (isRightOfDot) { + getTypeScriptMemberSymbols(); + } + else if (isRightOfOpenTag) { + const tagSymbols = typeChecker.getJsxIntrinsicTagNames(); + if (tryGetGlobalSymbols()) { + symbols = tagSymbols.concat(symbols.filter(s => !!(s.flags & (SymbolFlags.Value | SymbolFlags.Alias)))); + } + else { + symbols = tagSymbols; + } + isMemberCompletion = true; + isNewIdentifierLocation = false; + } + else if (isStartingCloseTag) { + const tagName = (contextToken.parent.parent).openingElement.tagName; + const tagSymbol = typeChecker.getSymbolAtLocation(tagName); + + if (!typeChecker.isUnknownSymbol(tagSymbol)) { + symbols = [tagSymbol]; + } + isMemberCompletion = true; + isNewIdentifierLocation = false; + } + else { + // For JavaScript or TypeScript, if we're not after a dot, then just try to get the + // global symbols in scope. These results should be valid for either language as + // the set of symbols that can be referenced from this location. + if (!tryGetGlobalSymbols()) { + return undefined; + } + } + + log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); + + return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName }; + + function getTypeScriptMemberSymbols(): void { + // Right of dot member completion list + isMemberCompletion = true; + isNewIdentifierLocation = false; + + if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) { + let symbol = typeChecker.getSymbolAtLocation(node); + + // This is an alias, follow what it aliases + if (symbol && symbol.flags & SymbolFlags.Alias) { + symbol = typeChecker.getAliasedSymbol(symbol); + } + + if (symbol && symbol.flags & SymbolFlags.HasExports) { + // Extract module or enum members + const exportedSymbols = typeChecker.getExportsOfModule(symbol); + forEach(exportedSymbols, symbol => { + if (typeChecker.isValidPropertyAccess((node.parent), symbol.name)) { + symbols.push(symbol); + } + }); + } + } + + const type = typeChecker.getTypeAtLocation(node); + addTypeProperties(type); + } + + function addTypeProperties(type: Type) { + if (type) { + // Filter private properties + for (const symbol of type.getApparentProperties()) { + if (typeChecker.isValidPropertyAccess((node.parent), symbol.name)) { + symbols.push(symbol); + } + } + + if (isJavaScriptFile && type.flags & TypeFlags.Union) { + // In javascript files, for union types, we don't just get the members that + // the individual types have in common, we also include all the members that + // each individual type has. This is because we're going to add all identifiers + // anyways. So we might as well elevate the members that were at least part + // of the individual types to a higher status since we know what they are. + const unionType = type; + for (const elementType of unionType.types) { + addTypeProperties(elementType); + } + } + } + } + + function tryGetGlobalSymbols(): boolean { + let objectLikeContainer: ObjectLiteralExpression | BindingPattern; + let namedImportsOrExports: NamedImportsOrExports; + let jsxContainer: JsxOpeningLikeElement; + + if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) { + return tryGetObjectLikeCompletionSymbols(objectLikeContainer); + } + + if (namedImportsOrExports = tryGetNamedImportsOrExportsForCompletion(contextToken)) { + // cursor is in an import clause + // try to show exported member for imported module + return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports); + } + + if (jsxContainer = tryGetContainingJsxElement(contextToken)) { + let attrsType: Type; + if ((jsxContainer.kind === SyntaxKind.JsxSelfClosingElement) || (jsxContainer.kind === SyntaxKind.JsxOpeningElement)) { + // Cursor is inside a JSX self-closing element or opening element + attrsType = typeChecker.getJsxElementAttributesType(jsxContainer); + + if (attrsType) { + symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (jsxContainer).attributes); + isMemberCompletion = true; + isNewIdentifierLocation = false; + return true; + } + + } + } + + // Get all entities in the current scope. + isMemberCompletion = false; + isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken); + + if (previousToken !== contextToken) { + Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'."); + } + // We need to find the node that will give us an appropriate scope to begin + // aggregating completion candidates. This is achieved in 'getScopeNode' + // by finding the first node that encompasses a position, accounting for whether a node + // is "complete" to decide whether a position belongs to the node. + // + // However, at the end of an identifier, we are interested in the scope of the identifier + // itself, but fall outside of the identifier. For instance: + // + // xyz => x$ + // + // the cursor is outside of both the 'x' and the arrow function 'xyz => x', + // so 'xyz' is not returned in our results. + // + // We define 'adjustedPosition' so that we may appropriately account for + // being at the end of an identifier. The intention is that if requesting completion + // at the end of an identifier, it should be effectively equivalent to requesting completion + // anywhere inside/at the beginning of the identifier. So in the previous case, the + // 'adjustedPosition' will work as if requesting completion in the following: + // + // xyz => $x + // + // If previousToken !== contextToken, then + // - 'contextToken' was adjusted to the token prior to 'previousToken' + // because we were at the end of an identifier. + // - 'previousToken' is defined. + const adjustedPosition = previousToken !== contextToken ? + previousToken.getStart() : + position; + + const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile; + + /// TODO filter meaning based on the current context + const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias; + symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings); + + return true; + } + + /** + * Finds the first node that "embraces" the position, so that one may + * accurately aggregate locals from the closest containing scope. + */ + function getScopeNode(initialToken: Node, position: number, sourceFile: SourceFile) { + let scope = initialToken; + while (scope && !positionBelongsToNode(scope, position, sourceFile)) { + scope = scope.parent; + } + return scope; + } + + function isCompletionListBlocker(contextToken: Node): boolean { + const start = timestamp(); + const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || + isSolelyIdentifierDefinitionLocation(contextToken) || + isDotOfNumericLiteral(contextToken) || + isInJsxText(contextToken); + log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start)); + return result; + } + + function isInJsxText(contextToken: Node): boolean { + if (contextToken.kind === SyntaxKind.JsxText) { + return true; + } + + if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) { + if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) { + return true; + } + + if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { + return contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement; + } + } + return false; + } + + function isNewIdentifierDefinitionLocation(previousToken: Node): boolean { + if (previousToken) { + const containingNodeKind = previousToken.parent.kind; + switch (previousToken.kind) { + case SyntaxKind.CommaToken: + return containingNodeKind === SyntaxKind.CallExpression // func( a, | + || containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ + || containingNodeKind === SyntaxKind.NewExpression // new C(a, | + || containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, | + || containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, | + || containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list| + + case SyntaxKind.OpenParenToken: + return containingNodeKind === SyntaxKind.CallExpression // func( | + || containingNodeKind === SyntaxKind.Constructor // constructor( | + || containingNodeKind === SyntaxKind.NewExpression // new C(a| + || containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a| + || containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ + + case SyntaxKind.OpenBracketToken: + return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ | + || containingNodeKind === SyntaxKind.IndexSignature // [ | : string ] + || containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ + + case SyntaxKind.ModuleKeyword: // module | + case SyntaxKind.NamespaceKeyword: // namespace | + return true; + + case SyntaxKind.DotToken: + return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.| + + case SyntaxKind.OpenBraceToken: + return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ | + + case SyntaxKind.EqualsToken: + return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a| + || containingNodeKind === SyntaxKind.BinaryExpression; // x = a| + + case SyntaxKind.TemplateHead: + return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${| + + case SyntaxKind.TemplateMiddle: + return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${| + + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public | + } + + // Previous token may have been a keyword that was converted to an identifier. + switch (previousToken.getText()) { + case "public": + case "protected": + case "private": + return true; + } + } + + return false; + } + + function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean { + if (contextToken.kind === SyntaxKind.StringLiteral + || contextToken.kind === SyntaxKind.RegularExpressionLiteral + || isTemplateLiteralKind(contextToken.kind)) { + const start = contextToken.getStart(); + const end = contextToken.getEnd(); + + // To be "in" one of these literals, the position has to be: + // 1. entirely within the token text. + // 2. at the end position of an unterminated token. + // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). + if (start < position && position < end) { + return true; + } + + if (position === end) { + return !!(contextToken).isUnterminated + || contextToken.kind === SyntaxKind.RegularExpressionLiteral; + } + } + + return false; + } + + /** + * Aggregates relevant symbols for completion in object literals and object binding patterns. + * Relevant symbols are stored in the captured 'symbols' variable. + * + * @returns true if 'symbols' was successfully populated; false otherwise. + */ + function tryGetObjectLikeCompletionSymbols(objectLikeContainer: ObjectLiteralExpression | BindingPattern): boolean { + // We're looking up possible property names from contextual/inferred/declared type. + isMemberCompletion = true; + + let typeForObject: Type; + let existingMembers: Declaration[]; + + if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { + // We are completing on contextual types, but may also include properties + // other than those within the declared type. + isNewIdentifierLocation = true; + + // If the object literal is being assigned to something of type 'null | { hello: string }', + // it clearly isn't trying to satisfy the 'null' type. So we grab the non-nullable type if possible. + typeForObject = typeChecker.getContextualType(objectLikeContainer); + typeForObject = typeForObject && typeForObject.getNonNullableType(); + + existingMembers = (objectLikeContainer).properties; + } + else if (objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern) { + // We are *only* completing on properties from the type being destructured. + isNewIdentifierLocation = false; + + const rootDeclaration = getRootDeclaration(objectLikeContainer.parent); + if (isVariableLike(rootDeclaration)) { + // We don't want to complete using the type acquired by the shape + // of the binding pattern; we are only interested in types acquired + // through type declaration or inference. + // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - + // type of parameter will flow in from the contextual type of the function + let canGetType = !!(rootDeclaration.initializer || rootDeclaration.type); + if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) { + if (isExpression(rootDeclaration.parent)) { + canGetType = !!typeChecker.getContextualType(rootDeclaration.parent); + } + else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) { + canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(rootDeclaration.parent.parent); + } + } + if (canGetType) { + typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); + existingMembers = (objectLikeContainer).elements; + } + } + else { + Debug.fail("Root declaration is not variable-like."); + } + } + else { + Debug.fail("Expected object literal or binding pattern, got " + objectLikeContainer.kind); + } + + if (!typeForObject) { + return false; + } + + const typeMembers = typeChecker.getPropertiesOfType(typeForObject); + if (typeMembers && typeMembers.length > 0) { + // Add filtered items to the completion list + symbols = filterObjectMembersList(typeMembers, existingMembers); + } + return true; + } + + /** + * Aggregates relevant symbols for completion in import clauses and export clauses + * whose declarations have a module specifier; for instance, symbols will be aggregated for + * + * import { | } from "moduleName"; + * export { a as foo, | } from "moduleName"; + * + * but not for + * + * export { | }; + * + * Relevant symbols are stored in the captured 'symbols' variable. + * + * @returns true if 'symbols' was successfully populated; false otherwise. + */ + function tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports: NamedImportsOrExports): boolean { + const declarationKind = namedImportsOrExports.kind === SyntaxKind.NamedImports ? + SyntaxKind.ImportDeclaration : + SyntaxKind.ExportDeclaration; + const importOrExportDeclaration = getAncestor(namedImportsOrExports, declarationKind); + const moduleSpecifier = importOrExportDeclaration.moduleSpecifier; + + if (!moduleSpecifier) { + return false; + } + + isMemberCompletion = true; + isNewIdentifierLocation = false; + + let exports: Symbol[]; + const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importOrExportDeclaration.moduleSpecifier); + if (moduleSpecifierSymbol) { + exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol); + } + + symbols = exports ? filterNamedImportOrExportCompletionItems(exports, namedImportsOrExports.elements) : emptyArray; + + return true; + } + + /** + * Returns the immediate owning object literal or binding pattern of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ + function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | BindingPattern { + if (contextToken) { + switch (contextToken.kind) { + case SyntaxKind.OpenBraceToken: // const x = { | + case SyntaxKind.CommaToken: // const x = { a: 0, | + const parent = contextToken.parent; + if (parent && (parent.kind === SyntaxKind.ObjectLiteralExpression || parent.kind === SyntaxKind.ObjectBindingPattern)) { + return parent; + } + break; + } + } + + return undefined; + } + + /** + * Returns the containing list of named imports or exports of a context token, + * on the condition that one exists and that the context implies completion should be given. + */ + function tryGetNamedImportsOrExportsForCompletion(contextToken: Node): NamedImportsOrExports { + if (contextToken) { + switch (contextToken.kind) { + case SyntaxKind.OpenBraceToken: // import { | + case SyntaxKind.CommaToken: // import { a as 0, | + switch (contextToken.parent.kind) { + case SyntaxKind.NamedImports: + case SyntaxKind.NamedExports: + return contextToken.parent; + } + } + } + + return undefined; + } + + function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement { + if (contextToken) { + const parent = contextToken.parent; + switch (contextToken.kind) { + case SyntaxKind.LessThanSlashToken: + case SyntaxKind.SlashToken: + case SyntaxKind.Identifier: + case SyntaxKind.JsxAttribute: + case SyntaxKind.JsxSpreadAttribute: + if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) { + return parent; + } + else if (parent.kind === SyntaxKind.JsxAttribute) { + return parent.parent; + } + break; + + // The context token is the closing } or " of an attribute, which means + // its parent is a JsxExpression, whose parent is a JsxAttribute, + // whose parent is a JsxOpeningLikeElement + case SyntaxKind.StringLiteral: + if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) { + return parent.parent; + } + + break; + + case SyntaxKind.CloseBraceToken: + if (parent && + parent.kind === SyntaxKind.JsxExpression && + parent.parent && + (parent.parent.kind === SyntaxKind.JsxAttribute)) { + return parent.parent.parent; + } + + if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) { + return parent.parent; + } + + break; + } + } + return undefined; + } + + function isFunction(kind: SyntaxKind): boolean { + switch (kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.CallSignature: + case SyntaxKind.ConstructSignature: + case SyntaxKind.IndexSignature: + return true; + } + return false; + } + + /** + * @returns true if we are certain that the currently edited location must define a new location; false otherwise. + */ + function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean { + const containingNodeKind = contextToken.parent.kind; + switch (contextToken.kind) { + case SyntaxKind.CommaToken: + return containingNodeKind === SyntaxKind.VariableDeclaration || + containingNodeKind === SyntaxKind.VariableDeclarationList || + containingNodeKind === SyntaxKind.VariableStatement || + containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, | + isFunction(containingNodeKind) || + containingNodeKind === SyntaxKind.ClassDeclaration || // class A(); + + for (const element of namedImportsOrExports) { + // If this is the current item we are editing right now, do not filter it out + if (element.getStart() <= position && position <= element.getEnd()) { + continue; + } + + const name = element.propertyName || element.name; + existingImportsOrExports[name.text] = true; + } + + if (!someProperties(existingImportsOrExports)) { + return filter(exportsOfModule, e => e.name !== "default"); + } + + return filter(exportsOfModule, e => e.name !== "default" && !existingImportsOrExports[e.name]); + } + + /** + * Filters out completion suggestions for named imports or exports. + * + * @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations + * do not occur at the current position and have not otherwise been typed. + */ + function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: Declaration[]): Symbol[] { + if (!existingMembers || existingMembers.length === 0) { + return contextualMemberSymbols; + } + + const existingMemberNames = createMap(); + for (const m of existingMembers) { + // Ignore omitted expressions for missing members + if (m.kind !== SyntaxKind.PropertyAssignment && + m.kind !== SyntaxKind.ShorthandPropertyAssignment && + m.kind !== SyntaxKind.BindingElement && + m.kind !== SyntaxKind.MethodDeclaration) { + continue; + } + + // If this is the current item we are editing right now, do not filter it out + if (m.getStart() <= position && position <= m.getEnd()) { + continue; + } + + let existingName: string; + + if (m.kind === SyntaxKind.BindingElement && (m).propertyName) { + // include only identifiers in completion list + if ((m).propertyName.kind === SyntaxKind.Identifier) { + existingName = ((m).propertyName).text; + } + } + else { + // TODO(jfreeman): Account for computed property name + // NOTE: if one only performs this step when m.name is an identifier, + // things like '__proto__' are not filtered out. + existingName = (m.name).text; + } + + existingMemberNames[existingName] = true; + } + + return filter(contextualMemberSymbols, m => !existingMemberNames[m.name]); + } + + /** + * Filters out completion suggestions from 'symbols' according to existing JSX attributes. + * + * @returns Symbols to be suggested in a JSX element, barring those whose attributes + * do not occur at the current position and have not otherwise been typed. + */ + function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray): Symbol[] { + const seenNames = createMap(); + for (const attr of attributes) { + // If this is the current item we are editing right now, do not filter it out + if (attr.getStart() <= position && position <= attr.getEnd()) { + continue; + } + + if (attr.kind === SyntaxKind.JsxAttribute) { + seenNames[(attr).name.text] = true; + } + } + + return filter(symbols, a => !seenNames[a.name]); + } + } + + /** + * Get the name to be display in completion from a given symbol. + * + * @return undefined if the name is of external module otherwise a name with striped of any quote + */ + function getCompletionEntryDisplayNameForSymbol(typeChecker: TypeChecker, symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string { + const displayName: string = getDeclaredName(typeChecker, symbol, location); + + if (displayName) { + const firstCharCode = displayName.charCodeAt(0); + // First check of the displayName is not external module; if it is an external module, it is not valid entry + if ((symbol.flags & SymbolFlags.Namespace) && (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) { + // If the symbol is external module, don't show it in the completion list + // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) + return undefined; + } + } + + return getCompletionEntryDisplayName(displayName, target, performCharacterChecks); + } + + /** + * Get a displayName from a given for completion list, performing any necessary quotes stripping + * and checking whether the name is valid identifier name. + */ + function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string { + if (!name) { + return undefined; + } + + name = stripQuotes(name); + + if (!name) { + return undefined; + } + + // If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an + // invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name. + // e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid. + // We, thus, need to check if whatever was inside the quotes is actually a valid identifier name. + if (performCharacterChecks) { + if (!isIdentifierText(name, target)) { + return undefined; + } + } + + return name; + } + + // A cache of completion entries for keywords, these do not change between sessions + const keywordCompletions: CompletionEntry[] = []; + for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) { + keywordCompletions.push({ + name: tokenToString(i), + kind: ScriptElementKind.keyword, + kindModifiers: ScriptElementKindModifier.none, + sortText: "0" + }); + } + + /** + * Matches a triple slash reference directive with an incomplete string literal for its path. Used + * to determine if the caret is currently within the string literal and capture the literal fragment + * for completions. + * For example, this matches /// (); + const result: DocumentHighlights[] = []; + for (const referencedSymbol of referencedSymbols) { + for (const referenceEntry of referencedSymbol.references) { + const fileName = referenceEntry.fileName; + let documentHighlights = fileNameToDocumentHighlights[fileName]; + if (!documentHighlights) { + documentHighlights = { fileName, highlightSpans: [] }; + + fileNameToDocumentHighlights[fileName] = documentHighlights; + result.push(documentHighlights); + } + + documentHighlights.highlightSpans.push({ + textSpan: referenceEntry.textSpan, + kind: referenceEntry.isWriteAccess ? HighlightSpanKind.writtenReference : HighlightSpanKind.reference + }); + } + } + + return result; + } + } + + function getSyntacticDocumentHighlights(node: Node): DocumentHighlights[] { + const fileName = sourceFile.fileName; + + const highlightSpans = getHighlightSpans(node); + if (!highlightSpans || highlightSpans.length === 0) { + return undefined; + } + + return [{ 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): HighlightSpan[] { + if (node) { + switch (node.kind) { + case SyntaxKind.IfKeyword: + case SyntaxKind.ElseKeyword: + if (hasKind(node.parent, SyntaxKind.IfStatement)) { + return getIfElseOccurrences(node.parent); + } + break; + case SyntaxKind.ReturnKeyword: + if (hasKind(node.parent, SyntaxKind.ReturnStatement)) { + return getReturnOccurrences(node.parent); + } + break; + case SyntaxKind.ThrowKeyword: + if (hasKind(node.parent, SyntaxKind.ThrowStatement)) { + return getThrowOccurrences(node.parent); + } + break; + case SyntaxKind.CatchKeyword: + if (hasKind(parent(parent(node)), SyntaxKind.TryStatement)) { + return getTryCatchFinallyOccurrences(node.parent.parent); + } + break; + case SyntaxKind.TryKeyword: + case SyntaxKind.FinallyKeyword: + if (hasKind(parent(node), SyntaxKind.TryStatement)) { + return getTryCatchFinallyOccurrences(node.parent); + } + break; + case SyntaxKind.SwitchKeyword: + if (hasKind(node.parent, SyntaxKind.SwitchStatement)) { + return getSwitchCaseDefaultOccurrences(node.parent); + } + break; + case SyntaxKind.CaseKeyword: + case SyntaxKind.DefaultKeyword: + if (hasKind(parent(parent(parent(node))), SyntaxKind.SwitchStatement)) { + return getSwitchCaseDefaultOccurrences(node.parent.parent.parent); + } + break; + case SyntaxKind.BreakKeyword: + case SyntaxKind.ContinueKeyword: + if (hasKind(node.parent, SyntaxKind.BreakStatement) || hasKind(node.parent, SyntaxKind.ContinueStatement)) { + return getBreakOrContinueStatementOccurrences(node.parent); + } + break; + case SyntaxKind.ForKeyword: + if (hasKind(node.parent, SyntaxKind.ForStatement) || + hasKind(node.parent, SyntaxKind.ForInStatement) || + hasKind(node.parent, SyntaxKind.ForOfStatement)) { + return getLoopBreakContinueOccurrences(node.parent); + } + break; + case SyntaxKind.WhileKeyword: + case SyntaxKind.DoKeyword: + if (hasKind(node.parent, SyntaxKind.WhileStatement) || hasKind(node.parent, SyntaxKind.DoStatement)) { + return getLoopBreakContinueOccurrences(node.parent); + } + break; + case SyntaxKind.ConstructorKeyword: + if (hasKind(node.parent, SyntaxKind.Constructor)) { + return getConstructorOccurrences(node.parent); + } + break; + case SyntaxKind.GetKeyword: + case SyntaxKind.SetKeyword: + if (hasKind(node.parent, SyntaxKind.GetAccessor) || hasKind(node.parent, SyntaxKind.SetAccessor)) { + return getGetAndSetOccurrences(node.parent); + } + break; + default: + if (isModifierKind(node.kind) && node.parent && + (isDeclaration(node.parent) || node.parent.kind === SyntaxKind.VariableStatement)) { + return getModifierOccurrences(node.kind, node.parent); + } + } + } + + return undefined; + } + + /** + * Aggregates all throw-statements within this node *without* crossing + * into function boundaries and try-blocks with catch-clauses. + */ + function aggregateOwnedThrowStatements(node: Node): ThrowStatement[] { + const statementAccumulator: ThrowStatement[] = []; + aggregate(node); + return statementAccumulator; + + function aggregate(node: Node): void { + if (node.kind === SyntaxKind.ThrowStatement) { + statementAccumulator.push(node); + } + else if (node.kind === SyntaxKind.TryStatement) { + const tryStatement = node; + + if (tryStatement.catchClause) { + aggregate(tryStatement.catchClause); + } + else { + // Exceptions thrown within a try block lacking a catch clause + // are "owned" in the current context. + aggregate(tryStatement.tryBlock); + } + + if (tryStatement.finallyBlock) { + aggregate(tryStatement.finallyBlock); + } + } + // Do not cross function boundaries. + else if (!isFunctionLike(node)) { + forEachChild(node, aggregate); + } + } + } + + /** + * For lack of a better name, this function takes a throw statement and returns the + * nearest ancestor that is a try-block (whose try statement has a catch clause), + * function-block, or source file. + */ + function getThrowStatementOwner(throwStatement: ThrowStatement): Node { + let child: Node = throwStatement; + + while (child.parent) { + const parent = child.parent; + + if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { + return parent; + } + + // A throw-statement is only owned by a try-statement if the try-statement has + // a catch clause, and if the throw-statement occurs within the try block. + if (parent.kind === SyntaxKind.TryStatement) { + const tryStatement = parent; + + if (tryStatement.tryBlock === child && tryStatement.catchClause) { + return child; + } + } + + child = parent; + } + + return undefined; + } + + function aggregateAllBreakAndContinueStatements(node: Node): BreakOrContinueStatement[] { + const statementAccumulator: BreakOrContinueStatement[] = []; + aggregate(node); + return statementAccumulator; + + function aggregate(node: Node): void { + if (node.kind === SyntaxKind.BreakStatement || node.kind === SyntaxKind.ContinueStatement) { + statementAccumulator.push(node); + } + // Do not cross function boundaries. + else if (!isFunctionLike(node)) { + forEachChild(node, aggregate); + } + } + } + + function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { + const actualOwner = getBreakOrContinueOwner(statement); + + return actualOwner && actualOwner === owner; + } + + function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node { + for (let node = statement.parent; node; node = node.parent) { + switch (node.kind) { + case SyntaxKind.SwitchStatement: + if (statement.kind === SyntaxKind.ContinueStatement) { + continue; + } + // Fall through. + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.WhileStatement: + case SyntaxKind.DoStatement: + if (!statement.label || isLabeledBy(node, statement.label.text)) { + return node; + } + break; + default: + // Don't cross function boundaries. + if (isFunctionLike(node)) { + return undefined; + } + break; + } + } + + return undefined; + } + + function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): HighlightSpan[] { + const container = declaration.parent; + + // Make sure we only highlight the keyword when it makes sense to do so. + if (isAccessibilityModifier(modifier)) { + if (!(container.kind === SyntaxKind.ClassDeclaration || + container.kind === SyntaxKind.ClassExpression || + (declaration.kind === SyntaxKind.Parameter && hasKind(container, SyntaxKind.Constructor)))) { + return undefined; + } + } + else if (modifier === SyntaxKind.StaticKeyword) { + if (!(container.kind === SyntaxKind.ClassDeclaration || container.kind === SyntaxKind.ClassExpression)) { + return undefined; + } + } + else if (modifier === SyntaxKind.ExportKeyword || modifier === SyntaxKind.DeclareKeyword) { + if (!(container.kind === SyntaxKind.ModuleBlock || container.kind === SyntaxKind.SourceFile)) { + return undefined; + } + } + else if (modifier === SyntaxKind.AbstractKeyword) { + if (!(container.kind === SyntaxKind.ClassDeclaration || declaration.kind === SyntaxKind.ClassDeclaration)) { + return undefined; + } + } + else { + // unsupported modifier + return undefined; + } + + const keywords: Node[] = []; + const modifierFlag: ModifierFlags = getFlagFromModifier(modifier); + + let nodes: Node[]; + switch (container.kind) { + case SyntaxKind.ModuleBlock: + case SyntaxKind.SourceFile: + // Container is either a class declaration or the declaration is a classDeclaration + if (modifierFlag & ModifierFlags.Abstract) { + nodes = ((declaration).members).concat(declaration); + } + else { + nodes = (container).statements; + } + break; + case SyntaxKind.Constructor: + nodes = ((container).parameters).concat( + (container.parent).members); + break; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + nodes = (container).members; + + // If we're an accessibility modifier, we're in an instance member and should search + // the constructor's parameter list for instance members as well. + if (modifierFlag & ModifierFlags.AccessibilityModifier) { + const constructor = forEach((container).members, member => { + return member.kind === SyntaxKind.Constructor && member; + }); + + if (constructor) { + nodes = nodes.concat(constructor.parameters); + } + } + else if (modifierFlag & ModifierFlags.Abstract) { + nodes = nodes.concat(container); + } + break; + default: + Debug.fail("Invalid container kind."); + } + + forEach(nodes, node => { + if (getModifierFlags(node) & modifierFlag) { + forEach(node.modifiers, child => pushKeywordIf(keywords, child, modifier)); + } + }); + + return map(keywords, getHighlightSpanForNode); + + function getFlagFromModifier(modifier: SyntaxKind) { + switch (modifier) { + case SyntaxKind.PublicKeyword: + return ModifierFlags.Public; + case SyntaxKind.PrivateKeyword: + return ModifierFlags.Private; + case SyntaxKind.ProtectedKeyword: + return ModifierFlags.Protected; + case SyntaxKind.StaticKeyword: + return ModifierFlags.Static; + case SyntaxKind.ExportKeyword: + return ModifierFlags.Export; + case SyntaxKind.DeclareKeyword: + return ModifierFlags.Ambient; + case SyntaxKind.AbstractKeyword: + return ModifierFlags.Abstract; + default: + Debug.fail(); + } + } + } + + function pushKeywordIf(keywordList: Node[], token: Node, ...expected: SyntaxKind[]): boolean { + if (token && contains(expected, token.kind)) { + keywordList.push(token); + return true; + } + + return false; + } + + function getGetAndSetOccurrences(accessorDeclaration: AccessorDeclaration): HighlightSpan[] { + const keywords: Node[] = []; + + tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.GetAccessor); + tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.SetAccessor); + + return map(keywords, getHighlightSpanForNode); + + function tryPushAccessorKeyword(accessorSymbol: Symbol, accessorKind: SyntaxKind): void { + const accessor = getDeclarationOfKind(accessorSymbol, accessorKind); + + if (accessor) { + forEach(accessor.getChildren(), child => pushKeywordIf(keywords, child, SyntaxKind.GetKeyword, SyntaxKind.SetKeyword)); + } + } + } + + function getConstructorOccurrences(constructorDeclaration: ConstructorDeclaration): HighlightSpan[] { + const declarations = constructorDeclaration.symbol.getDeclarations(); + + const keywords: Node[] = []; + + forEach(declarations, declaration => { + forEach(declaration.getChildren(), token => { + return pushKeywordIf(keywords, token, SyntaxKind.ConstructorKeyword); + }); + }); + + return map(keywords, getHighlightSpanForNode); + } + + function getLoopBreakContinueOccurrences(loopNode: IterationStatement): HighlightSpan[] { + const keywords: Node[] = []; + + if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { + // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. + if (loopNode.kind === SyntaxKind.DoStatement) { + const loopTokens = loopNode.getChildren(); + + for (let i = loopTokens.length - 1; i >= 0; i--) { + if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { + break; + } + } + } + } + + const breaksAndContinues = aggregateAllBreakAndContinueStatements(loopNode.statement); + + forEach(breaksAndContinues, statement => { + if (ownsBreakOrContinueStatement(loopNode, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); + } + }); + + return map(keywords, getHighlightSpanForNode); + } + + function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): HighlightSpan[] { + const owner = getBreakOrContinueOwner(breakOrContinueStatement); + + if (owner) { + switch (owner.kind) { + case SyntaxKind.ForStatement: + case SyntaxKind.ForInStatement: + case SyntaxKind.ForOfStatement: + case SyntaxKind.DoStatement: + case SyntaxKind.WhileStatement: + return getLoopBreakContinueOccurrences(owner); + case SyntaxKind.SwitchStatement: + return getSwitchCaseDefaultOccurrences(owner); + + } + } + + return undefined; + } + + function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): HighlightSpan[] { + const keywords: Node[] = []; + + pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); + + // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. + forEach(switchStatement.caseBlock.clauses, clause => { + pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); + + const breaksAndContinues = aggregateAllBreakAndContinueStatements(clause); + + forEach(breaksAndContinues, statement => { + if (ownsBreakOrContinueStatement(switchStatement, statement)) { + pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); + } + }); + }); + + return map(keywords, getHighlightSpanForNode); + } + + function getTryCatchFinallyOccurrences(tryStatement: TryStatement): HighlightSpan[] { + const keywords: Node[] = []; + + pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); + + if (tryStatement.catchClause) { + pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); + } + + if (tryStatement.finallyBlock) { + const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); + pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); + } + + return map(keywords, getHighlightSpanForNode); + } + + function getThrowOccurrences(throwStatement: ThrowStatement): HighlightSpan[] { + const owner = getThrowStatementOwner(throwStatement); + + if (!owner) { + return undefined; + } + + const keywords: Node[] = []; + + forEach(aggregateOwnedThrowStatements(owner), throwStatement => { + pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); + }); + + // If the "owner" is a function, then we equate 'return' and 'throw' statements in their + // ability to "jump out" of the function, and include occurrences for both. + if (isFunctionBlock(owner)) { + forEachReturnStatement(owner, returnStatement => { + pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); + }); + } + + return map(keywords, getHighlightSpanForNode); + } + + function getReturnOccurrences(returnStatement: ReturnStatement): HighlightSpan[] { + 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))) { + return undefined; + } + + const keywords: Node[] = []; + forEachReturnStatement(func.body, returnStatement => { + pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); + }); + + // Include 'throw' statements that do not occur within a try block. + forEach(aggregateOwnedThrowStatements(func.body), throwStatement => { + pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); + }); + + return map(keywords, getHighlightSpanForNode); + } + + function getIfElseOccurrences(ifStatement: IfStatement): 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 result: HighlightSpan[] = []; + + // We'd like to highlight else/ifs together if they are only separated by whitespace + // (i.e. the keywords are separated by no comments, no newlines). + for (let i = 0; i < keywords.length; i++) { + if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { + const elseKeyword = keywords[i]; + const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. + + let shouldCombindElseAndIf = true; + + // Avoid recalculating getStart() by iterating backwards. + for (let j = ifKeyword.getStart() - 1; j >= elseKeyword.end; j--) { + if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { + shouldCombindElseAndIf = false; + break; + } + } + + if (shouldCombindElseAndIf) { + result.push({ + fileName: fileName, + textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), + kind: HighlightSpanKind.reference + }); + i++; // skip the next keyword + continue; + } + } + + // Ordinary case: just highlight the keyword. + result.push(getHighlightSpanForNode(keywords[i])); + } + + return result; + } + } + } + + /** + * Whether or not a 'node' is preceded by a label of the given string. + * Note: 'node' cannot be a SourceFile. + */ + function isLabeledBy(node: Node, labelName: string) { + for (let owner = node.parent; owner.kind === SyntaxKind.LabeledStatement; owner = owner.parent) { + if ((owner).label.text === labelName) { + return true; + } + } + + return false; + } +} diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts new file mode 100644 index 00000000000..bba70c47bc8 --- /dev/null +++ b/src/services/findAllReferences.ts @@ -0,0 +1,1117 @@ +/* @internal */ +namespace ts.FindAllReferences { + export function findReferencedSymbols(typeChecker: TypeChecker, cancellationToken: CancellationToken, sourceFiles: SourceFile[], sourceFile: SourceFile, position: number, findInStrings: boolean, findInComments: boolean): ReferencedSymbol[] { + const node = getTouchingPropertyName(sourceFile, position, /*includeJsDocComment*/ true); + if (node === sourceFile) { + return undefined; + } + + switch (node.kind) { + case SyntaxKind.NumericLiteral: + if (!isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { + break; + } + // Fallthrough + case SyntaxKind.Identifier: + case SyntaxKind.ThisKeyword: + // case SyntaxKind.SuperKeyword: TODO:GH#9268 + case SyntaxKind.ConstructorKeyword: + case SyntaxKind.StringLiteral: + return getReferencedSymbolsForNode(typeChecker, cancellationToken, node, sourceFiles, findInStrings, findInComments); + } + return undefined; + } + + export function getReferencedSymbolsForNode(typeChecker: TypeChecker, cancellationToken: CancellationToken, node: Node, sourceFiles: SourceFile[], findInStrings: boolean, findInComments: boolean): ReferencedSymbol[] { + // Labels + if (isLabelName(node)) { + if (isJumpStatementTarget(node)) { + const labelDefinition = getTargetLabel((node.parent), (node).text); + // if we have a label definition, look within its statement for references, if not, then + // the label is undefined and we have no results.. + return labelDefinition ? getLabelReferencesInNode(labelDefinition.parent, labelDefinition) : undefined; + } + else { + // it is a label definition and not a target, search within the parent labeledStatement + return getLabelReferencesInNode(node.parent, node); + } + } + + if (isThis(node)) { + return getReferencesForThisKeyword(node, sourceFiles); + } + + if (node.kind === SyntaxKind.SuperKeyword) { + return getReferencesForSuperKeyword(node); + } + + // `getSymbolAtLocation` normally returns the symbol of the class when given the constructor keyword, + // so we have to specify that we want the constructor symbol. + const symbol = typeChecker.getSymbolAtLocation(node); + + if (!symbol && node.kind === SyntaxKind.StringLiteral) { + return getReferencesForStringLiteral(node, sourceFiles); + } + + // Could not find a symbol e.g. unknown identifier + if (!symbol) { + // Can't have references to something that we have no symbol for. + return undefined; + } + + const declarations = symbol.declarations; + + // The symbol was an internal symbol and does not have a declaration e.g. undefined symbol + if (!declarations || !declarations.length) { + return undefined; + } + + let result: ReferencedSymbol[]; + + // Compute the meaning from the location and the symbol it references + const searchMeaning = getIntersectingMeaningFromDeclarations(Meaning.getMeaningFromLocation(node), declarations); + + // Get the text to search for. + // Note: if this is an external module symbol, the name doesn't include quotes. + const declaredName = stripQuotes(getDeclaredName(typeChecker, symbol, node)); + + // Try to get the smallest valid scope that we can limit our search to; + // otherwise we'll need to search globally (i.e. include each file). + const scope = getSymbolScope(symbol); + + // Maps from a symbol ID to the ReferencedSymbol entry in 'result'. + const symbolToIndex: number[] = []; + + if (scope) { + result = []; + getReferencesInNode(scope, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex); + } + else { + const internedName = getInternedName(symbol, node, declarations); + for (const sourceFile of sourceFiles) { + cancellationToken.throwIfCancellationRequested(); + + const nameTable = getNameTable(sourceFile); + + if (nameTable[internedName] !== undefined) { + result = result || []; + getReferencesInNode(sourceFile, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex); + } + } + } + + return result; + + function getDefinition(symbol: Symbol): ReferencedSymbolDefinitionInfo { + const info = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, node.getSourceFile(), getContainerNode(node), node); + const name = map(info.displayParts, p => p.text).join(""); + const declarations = symbol.declarations; + if (!declarations || declarations.length === 0) { + return undefined; + } + + return { + containerKind: "", + containerName: "", + name, + kind: info.symbolKind, + fileName: declarations[0].getSourceFile().fileName, + textSpan: createTextSpan(declarations[0].getStart(), 0), + displayParts: info.displayParts + }; + } + + function getAliasSymbolForPropertyNameSymbol(symbol: Symbol, location: Node): Symbol | undefined { + if (symbol.flags & SymbolFlags.Alias) { + // Default import get alias + const defaultImport = getDeclarationOfKind(symbol, SyntaxKind.ImportClause); + if (defaultImport) { + return typeChecker.getAliasedSymbol(symbol); + } + + const importOrExportSpecifier = forEach(symbol.declarations, + declaration => (declaration.kind === SyntaxKind.ImportSpecifier || + declaration.kind === SyntaxKind.ExportSpecifier) ? declaration : undefined); + if (importOrExportSpecifier && + // export { a } + (!importOrExportSpecifier.propertyName || + // export {a as class } where a is location + importOrExportSpecifier.propertyName === location)) { + // If Import specifier -> get alias + // else Export specifier -> get local target + return importOrExportSpecifier.kind === SyntaxKind.ImportSpecifier ? + typeChecker.getAliasedSymbol(symbol) : + typeChecker.getExportSpecifierLocalTargetSymbol(importOrExportSpecifier); + } + } + return undefined; + } + + function followAliasIfNecessary(symbol: Symbol, location: Node): Symbol { + return getAliasSymbolForPropertyNameSymbol(symbol, location) || symbol; + } + + function getPropertySymbolOfDestructuringAssignment(location: Node) { + return isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) && + typeChecker.getPropertySymbolOfDestructuringAssignment(location); + } + + function isObjectBindingPatternElementWithoutPropertyName(symbol: Symbol) { + const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); + return bindingElement && + bindingElement.parent.kind === SyntaxKind.ObjectBindingPattern && + !bindingElement.propertyName; + } + + function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol) { + if (isObjectBindingPatternElementWithoutPropertyName(symbol)) { + const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); + const typeOfPattern = typeChecker.getTypeAtLocation(bindingElement.parent); + return typeOfPattern && typeChecker.getPropertyOfType(typeOfPattern, (bindingElement.name).text); + } + return undefined; + } + + function getInternedName(symbol: Symbol, location: Node, declarations: Declaration[]): string { + // If this is an export or import specifier it could have been renamed using the 'as' syntax. + // If so we want to search for whatever under the cursor. + if (isImportOrExportSpecifierName(location)) { + return location.getText(); + } + + // Try to get the local symbol if we're dealing with an 'export default' + // since that symbol has the "true" name. + const localExportDefaultSymbol = getLocalSymbolForExportDefault(symbol); + symbol = localExportDefaultSymbol || symbol; + + return stripQuotes(symbol.name); + } + + /** + * Determines the smallest scope in which a symbol may have named references. + * Note that not every construct has been accounted for. This function can + * probably be improved. + * + * @returns undefined if the scope cannot be determined, implying that + * a reference to a symbol can occur anywhere. + */ + function getSymbolScope(symbol: Symbol): Node { + // If this is the symbol of a named function expression or named class expression, + // then named references are limited to its own scope. + const valueDeclaration = symbol.valueDeclaration; + if (valueDeclaration && (valueDeclaration.kind === SyntaxKind.FunctionExpression || valueDeclaration.kind === SyntaxKind.ClassExpression)) { + return valueDeclaration; + } + + // If this is private property or method, the scope is the containing class + if (symbol.flags & (SymbolFlags.Property | SymbolFlags.Method)) { + const privateDeclaration = forEach(symbol.getDeclarations(), d => (getModifierFlags(d) & ModifierFlags.Private) ? d : undefined); + if (privateDeclaration) { + return getAncestor(privateDeclaration, SyntaxKind.ClassDeclaration); + } + } + + // If the symbol is an import we would like to find it if we are looking for what it imports. + // So consider it visible outside its declaration scope. + if (symbol.flags & SymbolFlags.Alias) { + return undefined; + } + + // If symbol is of object binding pattern element without property name we would want to + // look for property too and that could be anywhere + if (isObjectBindingPatternElementWithoutPropertyName(symbol)) { + return undefined; + } + + // if this symbol is visible from its parent container, e.g. exported, then bail out + // if symbol correspond to the union property - bail out + if (symbol.parent || (symbol.flags & SymbolFlags.SyntheticProperty)) { + return undefined; + } + + let scope: Node; + + const declarations = symbol.getDeclarations(); + if (declarations) { + for (const declaration of declarations) { + const container = getContainerNode(declaration); + + if (!container) { + return undefined; + } + + if (scope && scope !== container) { + // Different declarations have different containers, bail out + return undefined; + } + + if (container.kind === SyntaxKind.SourceFile && !isExternalModule(container)) { + // This is a global variable and not an external module, any declaration defined + // within this scope is visible outside the file + return undefined; + } + + // The search scope is the container node + scope = container; + } + } + + return scope; + } + + function getPossibleSymbolReferencePositions(sourceFile: SourceFile, symbolName: string, start: number, end: number): number[] { + const positions: number[] = []; + + /// TODO: Cache symbol existence for files to save text search + // Also, need to make this work for unicode escapes. + + // Be resilient in the face of a symbol with no name or zero length name + if (!symbolName || !symbolName.length) { + return positions; + } + + const text = sourceFile.text; + const sourceLength = text.length; + const symbolNameLength = symbolName.length; + + let position = text.indexOf(symbolName, start); + while (position >= 0) { + cancellationToken.throwIfCancellationRequested(); + + // If we are past the end, stop looking + if (position > end) break; + + // We found a match. Make sure it's not part of a larger word (i.e. the char + // before and after it have to be a non-identifier char). + const endPosition = position + symbolNameLength; + + if ((position === 0 || !isIdentifierPart(text.charCodeAt(position - 1), ScriptTarget.Latest)) && + (endPosition === sourceLength || !isIdentifierPart(text.charCodeAt(endPosition), ScriptTarget.Latest))) { + // Found a real match. Keep searching. + positions.push(position); + } + position = text.indexOf(symbolName, position + symbolNameLength + 1); + } + + return positions; + } + + function getLabelReferencesInNode(container: Node, targetLabel: Identifier): ReferencedSymbol[] { + const references: ReferenceEntry[] = []; + const sourceFile = container.getSourceFile(); + const labelName = targetLabel.text; + const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, labelName, container.getStart(), container.getEnd()); + forEach(possiblePositions, position => { + cancellationToken.throwIfCancellationRequested(); + + const node = getTouchingWord(sourceFile, position); + if (!node || node.getWidth() !== labelName.length) { + return; + } + + // Only pick labels that are either the target label, or have a target that is the target label + if (node === targetLabel || + (isJumpStatementTarget(node) && getTargetLabel(node, labelName) === targetLabel)) { + references.push(getReferenceEntryFromNode(node)); + } + }); + + const definition: ReferencedSymbolDefinitionInfo = { + containerKind: "", + containerName: "", + fileName: targetLabel.getSourceFile().fileName, + kind: ScriptElementKind.label, + name: labelName, + textSpan: createTextSpanFromBounds(targetLabel.getStart(), targetLabel.getEnd()), + displayParts: [displayPart(labelName, SymbolDisplayPartKind.text)] + }; + + return [{ definition, references }]; + } + + function isValidReferencePosition(node: Node, searchSymbolName: string): boolean { + if (node) { + // Compare the length so we filter out strict superstrings of the symbol we are looking for + switch (node.kind) { + case SyntaxKind.Identifier: + return node.getWidth() === searchSymbolName.length; + + case SyntaxKind.StringLiteral: + if (isLiteralNameOfPropertyDeclarationOrIndexAccess(node) || + isNameOfExternalModuleImportOrDeclaration(node)) { + // For string literals we have two additional chars for the quotes + return node.getWidth() === searchSymbolName.length + 2; + } + break; + + case SyntaxKind.NumericLiteral: + if (isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { + return node.getWidth() === searchSymbolName.length; + } + break; + } + } + + return false; + } + + /** Search within node "container" for references for a search value, where the search value is defined as a + * tuple of(searchSymbol, searchText, searchLocation, and searchMeaning). + * searchLocation: a node where the search value + */ + function getReferencesInNode(container: Node, + searchSymbol: Symbol, + searchText: string, + searchLocation: Node, + searchMeaning: Meaning.SemanticMeaning, + findInStrings: boolean, + findInComments: boolean, + result: ReferencedSymbol[], + symbolToIndex: number[]): void { + + const sourceFile = container.getSourceFile(); + + const start = findInComments ? container.getFullStart() : container.getStart(); + const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, searchText, start, container.getEnd()); + + if (possiblePositions.length) { + // Build the set of symbols to search for, initially it has only the current symbol + const searchSymbols = populateSearchSymbolSet(searchSymbol, searchLocation); + + forEach(possiblePositions, position => { + cancellationToken.throwIfCancellationRequested(); + + const referenceLocation = getTouchingPropertyName(sourceFile, position); + if (!isValidReferencePosition(referenceLocation, searchText)) { + // This wasn't the start of a token. Check to see if it might be a + // match in a comment or string if that's what the caller is asking + // for. + if ((findInStrings && isInString(sourceFile, position)) || + (findInComments && isInNonReferenceComment(sourceFile, position))) { + + // In the case where we're looking inside comments/strings, we don't have + // an actual definition. So just use 'undefined' here. Features like + // 'Rename' won't care (as they ignore the definitions), and features like + // 'FindReferences' will just filter out these results. + result.push({ + definition: undefined, + references: [{ + fileName: sourceFile.fileName, + textSpan: createTextSpan(position, searchText.length), + isWriteAccess: false, + isDefinition: false + }] + }); + } + return; + } + + if (!(Meaning.getMeaningFromLocation(referenceLocation) & searchMeaning)) { + return; + } + + const referenceSymbol = typeChecker.getSymbolAtLocation(referenceLocation); + if (referenceSymbol) { + const referenceSymbolDeclaration = referenceSymbol.valueDeclaration; + const shorthandValueSymbol = typeChecker.getShorthandAssignmentValueSymbol(referenceSymbolDeclaration); + const relatedSymbol = getRelatedSymbol(searchSymbols, referenceSymbol, referenceLocation, + /*searchLocationIsConstructor*/ searchLocation.kind === SyntaxKind.ConstructorKeyword); + + if (relatedSymbol) { + const referencedSymbol = getReferencedSymbol(relatedSymbol); + referencedSymbol.references.push(getReferenceEntryFromNode(referenceLocation)); + } + /* Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment + * has two meaning : property name and property value. Therefore when we do findAllReference at the position where + * an identifier is declared, the language service should return the position of the variable declaration as well as + * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the + * position of property accessing, the referenceEntry of such position will be handled in the first case. + */ + else if (!(referenceSymbol.flags & SymbolFlags.Transient) && searchSymbols.indexOf(shorthandValueSymbol) >= 0) { + const referencedSymbol = getReferencedSymbol(shorthandValueSymbol); + referencedSymbol.references.push(getReferenceEntryFromNode(referenceSymbolDeclaration.name)); + } + else if (searchLocation.kind === SyntaxKind.ConstructorKeyword) { + findAdditionalConstructorReferences(referenceSymbol, referenceLocation); + } + } + }); + } + + return; + + /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ + function findAdditionalConstructorReferences(referenceSymbol: Symbol, referenceLocation: Node): void { + Debug.assert(isClassLike(searchSymbol.valueDeclaration)); + + const referenceClass = referenceLocation.parent; + if (referenceSymbol === searchSymbol && isClassLike(referenceClass)) { + Debug.assert(referenceClass.name === referenceLocation); + // This is the class declaration containing the constructor. + addReferences(findOwnConstructorCalls(searchSymbol)); + } + else { + // If this class appears in `extends C`, then the extending class' "super" calls are references. + const classExtending = tryGetClassByExtendingIdentifier(referenceLocation); + if (classExtending && isClassLike(classExtending) && followAliasIfNecessary(referenceSymbol, referenceLocation) === searchSymbol) { + addReferences(superConstructorAccesses(classExtending)); + } + } + } + + function addReferences(references: Node[]): void { + if (references.length) { + const referencedSymbol = getReferencedSymbol(searchSymbol); + addRange(referencedSymbol.references, map(references, getReferenceEntryFromNode)); + } + } + + /** `classSymbol` is the class where the constructor was defined. + * Reference the constructor and all calls to `new this()`. + */ + function findOwnConstructorCalls(classSymbol: Symbol): Node[] { + const result: Node[] = []; + + for (const decl of classSymbol.members["__constructor"].declarations) { + Debug.assert(decl.kind === SyntaxKind.Constructor); + const ctrKeyword = decl.getChildAt(0); + Debug.assert(ctrKeyword.kind === SyntaxKind.ConstructorKeyword); + result.push(ctrKeyword); + } + + forEachProperty(classSymbol.exports, member => { + const decl = member.valueDeclaration; + if (decl && decl.kind === SyntaxKind.MethodDeclaration) { + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => { + if (isNewExpressionTarget(thisKeyword)) { + result.push(thisKeyword); + } + }); + } + } + }); + + return result; + } + + /** Find references to `super` in the constructor of an extending class. */ + function superConstructorAccesses(cls: ClassLikeDeclaration): Node[] { + const symbol = cls.symbol; + const ctr = symbol.members["__constructor"]; + if (!ctr) { + return []; + } + + const result: Node[] = []; + for (const decl of ctr.declarations) { + Debug.assert(decl.kind === SyntaxKind.Constructor); + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.SuperKeyword, node => { + if (isCallExpressionTarget(node)) { + result.push(node); + } + }); + } + }; + return result; + } + + function getReferencedSymbol(symbol: Symbol): ReferencedSymbol { + const symbolId = getSymbolId(symbol); + let index = symbolToIndex[symbolId]; + if (index === undefined) { + index = result.length; + symbolToIndex[symbolId] = index; + + result.push({ + definition: getDefinition(symbol), + references: [] + }); + } + + return result[index]; + } + } + + function getReferencesForSuperKeyword(superKeyword: Node): ReferencedSymbol[] { + let searchSpaceNode = getSuperContainer(superKeyword, /*stopOnFunctions*/ false); + if (!searchSpaceNode) { + return undefined; + } + // Whether 'super' occurs in a static context within a class. + let staticFlag = ModifierFlags.Static; + + switch (searchSpaceNode.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + staticFlag &= getModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + default: + return undefined; + } + + const references: ReferenceEntry[] = []; + + const sourceFile = searchSpaceNode.getSourceFile(); + const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "super", searchSpaceNode.getStart(), searchSpaceNode.getEnd()); + forEach(possiblePositions, position => { + cancellationToken.throwIfCancellationRequested(); + + const node = getTouchingWord(sourceFile, position); + + if (!node || node.kind !== SyntaxKind.SuperKeyword) { + return; + } + + const container = getSuperContainer(node, /*stopOnFunctions*/ false); + + // If we have a 'super' container, we must have an enclosing class. + // Now make sure the owning class is the same as the search-space + // and has the same static qualifier as the original 'super's owner. + if (container && (ModifierFlags.Static & getModifierFlags(container)) === staticFlag && container.parent.symbol === searchSpaceNode.symbol) { + references.push(getReferenceEntryFromNode(node)); + } + }); + + const definition = getDefinition(searchSpaceNode.symbol); + return [{ definition, references }]; + } + + function getReferencesForThisKeyword(thisOrSuperKeyword: Node, sourceFiles: SourceFile[]): ReferencedSymbol[] { + let searchSpaceNode = getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); + + // Whether 'this' occurs in a static context within a class. + let staticFlag = ModifierFlags.Static; + + switch (searchSpaceNode.kind) { + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (isObjectLiteralMethod(searchSpaceNode)) { + break; + } + // fall through + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + staticFlag &= getModifierFlags(searchSpaceNode); + searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class + break; + case SyntaxKind.SourceFile: + if (isExternalModule(searchSpaceNode)) { + return undefined; + } + // Fall through + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + break; + // Computed properties in classes are not handled here because references to this are illegal, + // so there is no point finding references to them. + default: + return undefined; + } + + const references: ReferenceEntry[] = []; + + let possiblePositions: number[]; + if (searchSpaceNode.kind === SyntaxKind.SourceFile) { + forEach(sourceFiles, sourceFile => { + possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "this", sourceFile.getStart(), sourceFile.getEnd()); + getThisReferencesInFile(sourceFile, sourceFile, possiblePositions, references); + }); + } + else { + const sourceFile = searchSpaceNode.getSourceFile(); + possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "this", searchSpaceNode.getStart(), searchSpaceNode.getEnd()); + getThisReferencesInFile(sourceFile, searchSpaceNode, possiblePositions, references); + } + + const thisOrSuperSymbol = typeChecker.getSymbolAtLocation(thisOrSuperKeyword); + + const displayParts = thisOrSuperSymbol && SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind( + typeChecker, thisOrSuperSymbol, thisOrSuperKeyword.getSourceFile(), getContainerNode(thisOrSuperKeyword), thisOrSuperKeyword).displayParts; + + return [{ + definition: { + containerKind: "", + containerName: "", + fileName: node.getSourceFile().fileName, + kind: ScriptElementKind.variableElement, + name: "this", + textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), + displayParts + }, + references: references + }]; + + function getThisReferencesInFile(sourceFile: SourceFile, searchSpaceNode: Node, possiblePositions: number[], result: ReferenceEntry[]): void { + forEach(possiblePositions, position => { + cancellationToken.throwIfCancellationRequested(); + + const node = getTouchingWord(sourceFile, position); + if (!node || !isThis(node)) { + return; + } + + const container = getThisContainer(node, /* includeArrowFunctions */ false); + + switch (searchSpaceNode.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + if (searchSpaceNode.symbol === container.symbol) { + result.push(getReferenceEntryFromNode(node)); + } + break; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + if (isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol) { + result.push(getReferenceEntryFromNode(node)); + } + break; + case SyntaxKind.ClassExpression: + case SyntaxKind.ClassDeclaration: + // Make sure the container belongs to the same class + // and has the appropriate static modifier from the original container. + if (container.parent && searchSpaceNode.symbol === container.parent.symbol && (getModifierFlags(container) & ModifierFlags.Static) === staticFlag) { + result.push(getReferenceEntryFromNode(node)); + } + break; + case SyntaxKind.SourceFile: + if (container.kind === SyntaxKind.SourceFile && !isExternalModule(container)) { + result.push(getReferenceEntryFromNode(node)); + } + break; + } + }); + } + } + + function getReferencesForStringLiteral(node: StringLiteral, sourceFiles: SourceFile[]): ReferencedSymbol[] { + const type = getStringLiteralTypeForNode(node, typeChecker); + + if (!type) { + // nothing to do here. moving on + return undefined; + } + + const references: ReferenceEntry[] = []; + + for (const sourceFile of sourceFiles) { + const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, type.text, sourceFile.getStart(), sourceFile.getEnd()); + getReferencesForStringLiteralInFile(sourceFile, type, possiblePositions, references); + } + + return [{ + definition: { + containerKind: "", + containerName: "", + fileName: node.getSourceFile().fileName, + kind: ScriptElementKind.variableElement, + name: type.text, + textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), + displayParts: [displayPart(getTextOfNode(node), SymbolDisplayPartKind.stringLiteral)] + }, + references: references + }]; + + function getReferencesForStringLiteralInFile(sourceFile: SourceFile, searchType: Type, possiblePositions: number[], references: ReferenceEntry[]): void { + for (const position of possiblePositions) { + cancellationToken.throwIfCancellationRequested(); + + const node = getTouchingWord(sourceFile, position); + if (!node || node.kind !== SyntaxKind.StringLiteral) { + return; + } + + const type = getStringLiteralTypeForNode(node, typeChecker); + if (type === searchType) { + references.push(getReferenceEntryFromNode(node)); + } + } + } + } + + function populateSearchSymbolSet(symbol: Symbol, location: Node): Symbol[] { + // The search set contains at least the current symbol + let result = [symbol]; + + // If the location is name of property symbol from object literal destructuring pattern + // Search the property symbol + // for ( { property: p2 } of elems) { } + const containingObjectLiteralElement = getContainingObjectLiteralElement(location); + if (containingObjectLiteralElement && containingObjectLiteralElement.kind !== SyntaxKind.ShorthandPropertyAssignment) { + const propertySymbol = getPropertySymbolOfDestructuringAssignment(location); + if (propertySymbol) { + result.push(propertySymbol); + } + } + + // If the symbol is an alias, add what it aliases to the list + // import {a} from "mod"; + // export {a} + // If the symbol is an alias to default declaration, add what it aliases to the list + // declare "mod" { export default class B { } } + // import B from "mod"; + //// For export specifiers, the exported name can be referring to a local symbol, e.g.: + //// import {a} from "mod"; + //// export {a as somethingElse} + //// We want the *local* declaration of 'a' as declared in the import, + //// *not* as declared within "mod" (or farther) + const aliasSymbol = getAliasSymbolForPropertyNameSymbol(symbol, location); + if (aliasSymbol) { + result = result.concat(populateSearchSymbolSet(aliasSymbol, location)); + } + + // If the location is in a context sensitive location (i.e. in an object literal) try + // to get a contextual type for it, and add the property symbol from the contextual + // type to the search set + if (containingObjectLiteralElement) { + forEach(getPropertySymbolsFromContextualType(containingObjectLiteralElement), contextualSymbol => { + addRange(result, typeChecker.getRootSymbols(contextualSymbol)); + }); + + /* Because in short-hand property assignment, location has two meaning : property name and as value of the property + * When we do findAllReference at the position of the short-hand property assignment, we would want to have references to position of + * property name and variable declaration of the identifier. + * Like in below example, when querying for all references for an identifier 'name', of the property assignment, the language service + * should show both 'name' in 'obj' and 'name' in variable declaration + * const name = "Foo"; + * const obj = { name }; + * In order to do that, we will populate the search set with the value symbol of the identifier as a value of the property assignment + * so that when matching with potential reference symbol, both symbols from property declaration and variable declaration + * will be included correctly. + */ + const shorthandValueSymbol = typeChecker.getShorthandAssignmentValueSymbol(location.parent); + if (shorthandValueSymbol) { + result.push(shorthandValueSymbol); + } + } + + // If the symbol.valueDeclaration is a property parameter declaration, + // we should include both parameter declaration symbol and property declaration symbol + // Parameter Declaration symbol is only visible within function scope, so the symbol is stored in constructor.locals. + // Property Declaration symbol is a member of the class, so the symbol is stored in its class Declaration.symbol.members + if (symbol.valueDeclaration && symbol.valueDeclaration.kind === SyntaxKind.Parameter && + isParameterPropertyDeclaration(symbol.valueDeclaration)) { + result = result.concat(typeChecker.getSymbolsOfParameterPropertyDeclaration(symbol.valueDeclaration, symbol.name)); + } + + // If this is symbol of binding element without propertyName declaration in Object binding pattern + // Include the property in the search + const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol); + if (bindingElementPropertySymbol) { + result.push(bindingElementPropertySymbol); + } + + // If this is a union property, add all the symbols from all its source symbols in all unioned types. + // If the symbol is an instantiation from a another symbol (e.g. widened symbol) , add the root the list + forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { + if (rootSymbol !== symbol) { + result.push(rootSymbol); + } + + // Add symbol of properties/methods of the same name in base classes and implemented interfaces definitions + if (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.getName(), result, /*previousIterationSymbolsCache*/ createMap()); + } + }); + + return result; + } + + /** + * Find symbol of the given property-name and add the symbol to the given result array + * @param symbol a symbol to start searching for the given propertyName + * @param propertyName a name of property to search for + * @param result an array of symbol of found property symbols + * @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol. + * The value of previousIterationSymbol is undefined when the function is first called. + */ + function getPropertySymbolsFromBaseTypes(symbol: Symbol, propertyName: string, result: Symbol[], + previousIterationSymbolsCache: SymbolTable): void { + if (!symbol) { + return; + } + + // If the current symbol is the same as the previous-iteration symbol, we can just return the symbol that has already been visited + // This is particularly important for the following cases, so that we do not infinitely visit the same symbol. + // For example: + // interface C extends C { + // /*findRef*/propName: string; + // } + // The first time getPropertySymbolsFromBaseTypes is called when finding-all-references at propName, + // the symbol argument will be the symbol of an interface "C" and previousIterationSymbol is undefined, + // the function will add any found symbol of the property-name, then its sub-routine will call + // getPropertySymbolsFromBaseTypes again to walk up any base types to prevent revisiting already + // visited symbol, interface "C", the sub-routine will pass the current symbol as previousIterationSymbol. + if (symbol.name in previousIterationSymbolsCache) { + return; + } + + if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + forEach(symbol.getDeclarations(), declaration => { + if (isClassLike(declaration)) { + getPropertySymbolFromTypeReference(getClassExtendsHeritageClauseElement(declaration)); + forEach(getClassImplementsHeritageClauseElements(declaration), getPropertySymbolFromTypeReference); + } + else if (declaration.kind === SyntaxKind.InterfaceDeclaration) { + forEach(getInterfaceBaseTypeNodes(declaration), getPropertySymbolFromTypeReference); + } + }); + } + return; + + function getPropertySymbolFromTypeReference(typeReference: ExpressionWithTypeArguments) { + if (typeReference) { + const type = typeChecker.getTypeAtLocation(typeReference); + if (type) { + const propertySymbol = typeChecker.getPropertyOfType(type, propertyName); + if (propertySymbol) { + result.push(...typeChecker.getRootSymbols(propertySymbol)); + } + + // Visit the typeReference as well to see if it directly or indirectly use that property + previousIterationSymbolsCache[symbol.name] = symbol; + getPropertySymbolsFromBaseTypes(type.symbol, propertyName, result, previousIterationSymbolsCache); + } + } + } + } + + function getRelatedSymbol(searchSymbols: Symbol[], referenceSymbol: Symbol, referenceLocation: Node, searchLocationIsConstructor: boolean): Symbol | undefined { + if (contains(searchSymbols, referenceSymbol)) { + // If we are searching for constructor uses, they must be 'new' expressions. + return (!searchLocationIsConstructor || isNewExpressionTarget(referenceLocation)) && referenceSymbol; + } + + // If the reference symbol is an alias, check if what it is aliasing is one of the search + // symbols but by looking up for related symbol of this alias so it can handle multiple level of indirectness. + const aliasSymbol = getAliasSymbolForPropertyNameSymbol(referenceSymbol, referenceLocation); + if (aliasSymbol) { + return getRelatedSymbol(searchSymbols, aliasSymbol, referenceLocation, searchLocationIsConstructor); + } + + // If the reference location is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this symbol to + // compare to our searchSymbol + const containingObjectLiteralElement = getContainingObjectLiteralElement(referenceLocation); + if (containingObjectLiteralElement) { + const contextualSymbol = forEach(getPropertySymbolsFromContextualType(containingObjectLiteralElement), contextualSymbol => { + return forEach(typeChecker.getRootSymbols(contextualSymbol), s => searchSymbols.indexOf(s) >= 0 ? s : undefined); + }); + + if (contextualSymbol) { + return contextualSymbol; + } + + // If the reference location is the name of property from object literal destructuring pattern + // Get the property symbol from the object literal's type and look if thats the search symbol + // In below eg. get 'property' from type of elems iterating type + // for ( { property: p2 } of elems) { } + const propertySymbol = getPropertySymbolOfDestructuringAssignment(referenceLocation); + if (propertySymbol && searchSymbols.indexOf(propertySymbol) >= 0) { + return propertySymbol; + } + } + + // If the reference location is the binding element and doesn't have property name + // then include the binding element in the related symbols + // let { a } : { a }; + const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(referenceSymbol); + if (bindingElementPropertySymbol && searchSymbols.indexOf(bindingElementPropertySymbol) >= 0) { + return bindingElementPropertySymbol; + } + + // Unwrap symbols to get to the root (e.g. transient symbols as a result of widening) + // Or a union property, use its underlying unioned symbols + return forEach(typeChecker.getRootSymbols(referenceSymbol), rootSymbol => { + // if it is in the list, then we are done + if (searchSymbols.indexOf(rootSymbol) >= 0) { + return rootSymbol; + } + + // Finally, try all properties with the same name in any type the containing type extended or implemented, and + // see if any is in the list + if (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + const result: Symbol[] = []; + getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.getName(), result, /*previousIterationSymbolsCache*/ createMap()); + return forEach(result, s => searchSymbols.indexOf(s) >= 0 ? s : undefined); + } + + return undefined; + }); + } + + function getNameFromObjectLiteralElement(node: ObjectLiteralElement) { + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + const nameExpression = (node.name).expression; + // treat computed property names where expression is string/numeric literal as just string/numeric literal + if (isStringOrNumericLiteral(nameExpression.kind)) { + return (nameExpression).text; + } + return undefined; + } + return (node.name).text; + } + + function getPropertySymbolsFromContextualType(node: ObjectLiteralElement): Symbol[] { + const objectLiteral = node.parent; + const contextualType = typeChecker.getContextualType(objectLiteral); + const name = getNameFromObjectLiteralElement(node); + if (name && contextualType) { + const result: Symbol[] = []; + const symbol = contextualType.getProperty(name); + if (symbol) { + result.push(symbol); + } + + if (contextualType.flags & TypeFlags.Union) { + forEach((contextualType).types, t => { + const symbol = t.getProperty(name); + if (symbol) { + result.push(symbol); + } + }); + } + return result; + } + return undefined; + } + + /** Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations + * of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class + * then we need to widen the search to include type positions as well. + * On the contrary, if we are searching for "Bar" in type position and we trace bar to an interface, and an uninstantiated + * module, we want to keep the search limited to only types, as the two declarations (interface and uninstantiated module) + * do not intersect in any of the three spaces. + */ + function getIntersectingMeaningFromDeclarations(meaning: Meaning.SemanticMeaning, declarations: Declaration[]): Meaning.SemanticMeaning { + if (declarations) { + let lastIterationMeaning: Meaning.SemanticMeaning; + do { + // The result is order-sensitive, for instance if initialMeaning === Namespace, and declarations = [class, instantiated module] + // we need to consider both as they initialMeaning intersects with the module in the namespace space, and the module + // intersects with the class in the value space. + // To achieve that we will keep iterating until the result stabilizes. + + // Remember the last meaning + lastIterationMeaning = meaning; + + for (const declaration of declarations) { + const declarationMeaning = Meaning.getMeaningFromDeclaration(declaration); + + if (declarationMeaning & meaning) { + meaning |= declarationMeaning; + } + } + } + while (meaning !== lastIterationMeaning); + } + return meaning; + } + } + + export function convertReferences(referenceSymbols: ReferencedSymbol[]): ReferenceEntry[] { + if (!referenceSymbols) { + return undefined; + } + + const referenceEntries: ReferenceEntry[] = []; + + for (const referenceSymbol of referenceSymbols) { + addRange(referenceEntries, referenceSymbol.references); + } + + return referenceEntries; + } + + function getReferenceEntryFromNode(node: Node): ReferenceEntry { + let start = node.getStart(); + let end = node.getEnd(); + + if (node.kind === SyntaxKind.StringLiteral) { + start += 1; + end -= 1; + } + + return { + fileName: node.getSourceFile().fileName, + textSpan: createTextSpanFromBounds(start, end), + isWriteAccess: isWriteAccess(node), + isDefinition: isDeclarationName(node) || isLiteralComputedPropertyDeclarationName(node) + }; + } + + /** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */ + function isWriteAccess(node: Node): boolean { + if (node.kind === SyntaxKind.Identifier && isDeclarationName(node)) { + return true; + } + + const parent = node.parent; + if (parent) { + if (parent.kind === SyntaxKind.PostfixUnaryExpression || parent.kind === SyntaxKind.PrefixUnaryExpression) { + return true; + } + else if (parent.kind === SyntaxKind.BinaryExpression && (parent).left === node) { + const operator = (parent).operatorToken.kind; + return SyntaxKind.FirstAssignment <= operator && operator <= SyntaxKind.LastAssignment; + } + } + + return false; + } + + function forEachDescendantOfKind(node: Node, kind: SyntaxKind, action: (node: Node) => void) { + forEachChild(node, child => { + if (child.kind === kind) { + action(child); + } + forEachDescendantOfKind(child, kind, action); + }); + } + + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + function getContainingObjectLiteralElement(node: Node): ObjectLiteralElement { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + if (node.parent.kind === SyntaxKind.ComputedPropertyName) { + return isObjectLiteralPropertyDeclaration(node.parent.parent) ? node.parent.parent : undefined; + } + // intential fall through + case SyntaxKind.Identifier: + return isObjectLiteralPropertyDeclaration(node.parent) && node.parent.name === node ? node.parent : undefined; + } + return undefined; + } + + function isObjectLiteralPropertyDeclaration(node: Node): node is ObjectLiteralElement { + switch (node.kind) { + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return true; + } + return false; + } + + /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ + function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { + return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); + } +} diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts new file mode 100644 index 00000000000..4c005640d6a --- /dev/null +++ b/src/services/goToDefinition.ts @@ -0,0 +1,256 @@ +/* @internal */ +namespace ts.GoToDefinition { + export function getDefinitionAtPosition(program: Program, sourceFile: SourceFile, position: number): DefinitionInfo[] { + /// Triple slash reference comments + const comment = findReferenceInPosition(sourceFile.referencedFiles, position); + if (comment) { + const referenceFile = tryResolveScriptReference(program, sourceFile, comment); + if (referenceFile) { + return [getDefinitionInfoForFileReference(comment.fileName, referenceFile.fileName)]; + } + return undefined; + } + + // Type reference directives + const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); + if (typeReferenceDirective) { + const referenceFile = program.getResolvedTypeReferenceDirectives()[typeReferenceDirective.fileName]; + if (referenceFile && referenceFile.resolvedFileName) { + return [getDefinitionInfoForFileReference(typeReferenceDirective.fileName, referenceFile.resolvedFileName)]; + } + return undefined; + } + + const node = getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + return undefined; + } + + // Labels + if (isJumpStatementTarget(node)) { + const labelName = (node).text; + const label = getTargetLabel((node.parent), (node).text); + return label ? [createDefinitionInfo(label, ScriptElementKind.label, labelName, /*containerName*/ undefined)] : undefined; + } + + const typeChecker = program.getTypeChecker(); + + const calledDeclaration = tryGetSignatureDeclaration(typeChecker, node); + if (calledDeclaration) { + return [createDefinitionFromSignatureDeclaration(typeChecker, calledDeclaration)]; + } + + let symbol = typeChecker.getSymbolAtLocation(node); + + // Could not find a symbol e.g. node is string or number keyword, + // or the symbol was an internal symbol and does not have a declaration e.g. undefined symbol + if (!symbol) { + return undefined; + } + + // If this is an alias, and the request came at the declaration location + // get the aliased symbol instead. This allows for goto def on an import e.g. + // import {A, B} from "mod"; + // to jump to the implementation directly. + if (symbol.flags & SymbolFlags.Alias) { + const declaration = symbol.declarations[0]; + + // Go to the original declaration for cases: + // + // (1) when the aliased symbol was declared in the location(parent). + // (2) when the aliased symbol is originating from a named import. + // + if (node.kind === SyntaxKind.Identifier && + (node.parent === declaration || + (declaration.kind === SyntaxKind.ImportSpecifier && declaration.parent && declaration.parent.kind === SyntaxKind.NamedImports))) { + + symbol = typeChecker.getAliasedSymbol(symbol); + } + } + + // Because name in short-hand property assignment has two different meanings: property name and property value, + // using go-to-definition at such position should go to the variable declaration of the property value rather than + // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition + // is performed at the location of property access, we would like to go to definition of the property in the short-hand + // assignment. This case and others are handled by the following code. + if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { + const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); + if (!shorthandSymbol) { + return []; + } + + const shorthandDeclarations = shorthandSymbol.getDeclarations(); + const shorthandSymbolKind = SymbolDisplay.getSymbolKind(typeChecker, shorthandSymbol, node); + const shorthandSymbolName = typeChecker.symbolToString(shorthandSymbol); + const shorthandContainerName = typeChecker.symbolToString(symbol.parent, node); + return map(shorthandDeclarations, + declaration => createDefinitionInfo(declaration, shorthandSymbolKind, shorthandSymbolName, shorthandContainerName)); + } + + return getDefinitionFromSymbol(typeChecker, symbol, node); + } + + /// Goto type + export function getTypeDefinitionAtPosition(typeChecker: TypeChecker, sourceFile: SourceFile, position: number): DefinitionInfo[] { + const node = getTouchingPropertyName(sourceFile, position); + if (node === sourceFile) { + return undefined; + } + + const symbol = typeChecker.getSymbolAtLocation(node); + if (!symbol) { + return undefined; + } + + const type = typeChecker.getTypeOfSymbolAtLocation(symbol, node); + if (!type) { + return undefined; + } + + if (type.flags & TypeFlags.Union && !(type.flags & TypeFlags.Enum)) { + const result: DefinitionInfo[] = []; + forEach((type).types, t => { + if (t.symbol) { + addRange(/*to*/ result, /*from*/ getDefinitionFromSymbol(typeChecker, t.symbol, node)); + } + }); + return result; + } + + if (!type.symbol) { + return undefined; + } + + return getDefinitionFromSymbol(typeChecker, type.symbol, node); + } + + function getDefinitionFromSymbol(typeChecker: TypeChecker, symbol: Symbol, node: Node): DefinitionInfo[] { + const result: DefinitionInfo[] = []; + const declarations = symbol.getDeclarations(); + const { symbolName, symbolKind, containerName } = getSymbolInfo(typeChecker, symbol, node); + + if (!tryAddConstructSignature(symbol, node, symbolKind, symbolName, containerName, result) && + !tryAddCallSignature(symbol, node, symbolKind, symbolName, containerName, result)) { + // Just add all the declarations. + forEach(declarations, declaration => { + result.push(createDefinitionInfo(declaration, symbolKind, symbolName, containerName)); + }); + } + + return result; + + function tryAddConstructSignature(symbol: Symbol, location: Node, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { + // Applicable only if we are in a new expression, or we are on a constructor declaration + // and in either case the symbol has a construct signature definition, i.e. class + if (isNewExpressionTarget(location) || location.kind === SyntaxKind.ConstructorKeyword) { + if (symbol.flags & SymbolFlags.Class) { + // Find the first class-like declaration and try to get the construct signature. + for (const declaration of symbol.getDeclarations()) { + if (isClassLike(declaration)) { + return tryAddSignature(declaration.members, + /*selectConstructors*/ true, + symbolKind, + symbolName, + containerName, + result); + } + } + + Debug.fail("Expected declaration to have at least one class-like declaration"); + } + } + return false; + } + + function tryAddCallSignature(symbol: Symbol, location: Node, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { + if (isCallExpressionTarget(location) || isNewExpressionTarget(location) || isNameOfFunctionDeclaration(location)) { + return tryAddSignature(symbol.declarations, /*selectConstructors*/ false, symbolKind, symbolName, containerName, result); + } + return false; + } + + function tryAddSignature(signatureDeclarations: Declaration[], selectConstructors: boolean, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { + const declarations: Declaration[] = []; + let definition: Declaration; + + forEach(signatureDeclarations, d => { + if ((selectConstructors && d.kind === SyntaxKind.Constructor) || + (!selectConstructors && (d.kind === SyntaxKind.FunctionDeclaration || d.kind === SyntaxKind.MethodDeclaration || d.kind === SyntaxKind.MethodSignature))) { + declarations.push(d); + if ((d).body) definition = d; + } + }); + + if (definition) { + result.push(createDefinitionInfo(definition, symbolKind, symbolName, containerName)); + return true; + } + else if (declarations.length) { + result.push(createDefinitionInfo(lastOrUndefined(declarations), symbolKind, symbolName, containerName)); + return true; + } + + return false; + } + } + + function createDefinitionInfo(node: Node, symbolKind: string, symbolName: string, containerName: string): DefinitionInfo { + return { + fileName: node.getSourceFile().fileName, + textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), + kind: symbolKind, + name: symbolName, + containerKind: undefined, + containerName + }; + } + + function getSymbolInfo(typeChecker: TypeChecker, symbol: Symbol, node: Node) { + return { + symbolName: typeChecker.symbolToString(symbol), // Do not get scoped name, just the name of the symbol + symbolKind: SymbolDisplay.getSymbolKind(typeChecker, symbol, node), + containerName: symbol.parent ? typeChecker.symbolToString(symbol.parent, node) : "" + }; + } + + function createDefinitionFromSignatureDeclaration(typeChecker: TypeChecker, decl: SignatureDeclaration): DefinitionInfo { + const { symbolName, symbolKind, containerName } = getSymbolInfo(typeChecker, decl.symbol, decl); + return createDefinitionInfo(decl, symbolKind, symbolName, containerName); + } + + function findReferenceInPosition(refs: FileReference[], pos: number): FileReference { + for (const ref of refs) { + if (ref.pos <= pos && pos < ref.end) { + return ref; + } + } + return undefined; + } + + function getDefinitionInfoForFileReference(name: string, targetFileName: string): DefinitionInfo { + return { + fileName: targetFileName, + textSpan: createTextSpanFromBounds(0, 0), + kind: ScriptElementKind.scriptElement, + name: name, + containerName: undefined, + containerKind: undefined + }; + } + + /** Returns a CallLikeExpression where `node` is the target being invoked. */ + function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined { + const target = climbPastManyPropertyAccesses(node); + const callLike = target.parent; + return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target && callLike; + } + + function climbPastManyPropertyAccesses(node: Node): Node { + return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; + } + + function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { + const callLike = getAncestorCallLikeExpression(node); + return callLike && typeChecker.getResolvedSignature(callLike).declaration; + } +} diff --git a/src/services/jsDoc.ts b/src/services/jsDoc.ts new file mode 100644 index 00000000000..f40a8013426 --- /dev/null +++ b/src/services/jsDoc.ts @@ -0,0 +1,545 @@ +/* @internal */ +namespace ts.JsDoc { + const jsDocTagNames = [ + "augments", + "author", + "argument", + "borrows", + "class", + "constant", + "constructor", + "constructs", + "default", + "deprecated", + "description", + "event", + "example", + "extends", + "field", + "fileOverview", + "function", + "ignore", + "inner", + "lends", + "link", + "memberOf", + "name", + "namespace", + "param", + "private", + "property", + "public", + "requires", + "returns", + "see", + "since", + "static", + "throws", + "type", + "typedef", + "property", + "prop", + "version" + ]; + let jsDocCompletionEntries: CompletionEntry[]; + + export function getJsDocCommentsFromDeclarations(declarations: Declaration[], name: string, canUseParsedParamTagComments: boolean) { + const documentationComment = []; + const docComments = getJsDocCommentsSeparatedByNewLines(); + ts.forEach(docComments, docComment => { + if (documentationComment.length) { + documentationComment.push(lineBreakPart()); + } + documentationComment.push(docComment); + }); + + return documentationComment; + + function getJsDocCommentsSeparatedByNewLines() { + const paramTag = "@param"; + const jsDocCommentParts: SymbolDisplayPart[] = []; + + ts.forEach(declarations, (declaration, indexOfDeclaration) => { + // Make sure we are collecting doc comment from declaration once, + // In case of union property there might be same declaration multiple times + // which only varies in type parameter + // Eg. const a: Array | Array; a.length + // The property length will have two declarations of property length coming + // from Array - Array and Array + if (indexOf(declarations, declaration) === indexOfDeclaration) { + const sourceFileOfDeclaration = getSourceFileOfNode(declaration); + // If it is parameter - try and get the jsDoc comment with @param tag from function declaration's jsDoc comments + if (canUseParsedParamTagComments && declaration.kind === SyntaxKind.Parameter) { + if ((declaration.parent.kind === SyntaxKind.FunctionExpression || declaration.parent.kind === SyntaxKind.ArrowFunction) && + declaration.parent.parent.kind === SyntaxKind.VariableDeclaration) { + addCommentParts(declaration.parent.parent.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); + } + addCommentParts(declaration.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); + } + + // If this is left side of dotted module declaration, there is no doc comments associated with this node + if (declaration.kind === SyntaxKind.ModuleDeclaration && (declaration).body && (declaration).body.kind === SyntaxKind.ModuleDeclaration) { + return; + } + + if ((declaration.kind === SyntaxKind.FunctionExpression || declaration.kind === SyntaxKind.ArrowFunction) && + declaration.parent.kind === SyntaxKind.VariableDeclaration) { + addCommentParts(declaration.parent.parent, sourceFileOfDeclaration, getCleanedJsDocComment); + } + + // If this is dotted module name, get the doc comments from the parent + while (declaration.kind === SyntaxKind.ModuleDeclaration && declaration.parent.kind === SyntaxKind.ModuleDeclaration) { + declaration = declaration.parent; + } + addCommentParts(declaration.kind === SyntaxKind.VariableDeclaration ? declaration.parent.parent : declaration, + sourceFileOfDeclaration, + getCleanedJsDocComment); + + if (declaration.kind === SyntaxKind.VariableDeclaration) { + const init = (declaration as VariableDeclaration).initializer; + if (init && (init.kind === SyntaxKind.FunctionExpression || init.kind === SyntaxKind.ArrowFunction)) { + // Get the cleaned js doc comment text from the initializer + addCommentParts(init, sourceFileOfDeclaration, getCleanedJsDocComment); + } + } + } + }); + + return jsDocCommentParts; + + function addCommentParts(commented: Node, + sourceFileOfDeclaration: SourceFile, + getCommentPart: (pos: number, end: number, file: SourceFile) => SymbolDisplayPart[]): void { + const ranges = getJsDocCommentTextRange(commented, sourceFileOfDeclaration); + // Get the cleaned js doc comment text from the declaration + ts.forEach(ranges, jsDocCommentTextRange => { + const cleanedComment = getCommentPart(jsDocCommentTextRange.pos, jsDocCommentTextRange.end, sourceFileOfDeclaration); + if (cleanedComment) { + addRange(jsDocCommentParts, cleanedComment); + } + }); + } + + function getJsDocCommentTextRange(node: Node, sourceFile: SourceFile): TextRange[] { + return ts.map(getJsDocComments(node, sourceFile), + jsDocComment => { + return { + pos: jsDocComment.pos + "/*".length, // Consume /* from the comment + end: jsDocComment.end - "*/".length // Trim off comment end indicator + }; + }); + } + + function consumeWhiteSpacesOnTheLine(pos: number, end: number, sourceFile: SourceFile, maxSpacesToRemove?: number) { + if (maxSpacesToRemove !== undefined) { + end = Math.min(end, pos + maxSpacesToRemove); + } + + for (; pos < end; pos++) { + const ch = sourceFile.text.charCodeAt(pos); + if (!isWhiteSpaceSingleLine(ch)) { + return pos; + } + } + + return end; + } + + function consumeLineBreaks(pos: number, end: number, sourceFile: SourceFile) { + while (pos < end && isLineBreak(sourceFile.text.charCodeAt(pos))) { + pos++; + } + + return pos; + } + + function isName(pos: number, end: number, sourceFile: SourceFile, name: string) { + return pos + name.length < end && + sourceFile.text.substr(pos, name.length) === name && + isWhiteSpace(sourceFile.text.charCodeAt(pos + name.length)); + } + + function isParamTag(pos: number, end: number, sourceFile: SourceFile) { + // If it is @param tag + return isName(pos, end, sourceFile, paramTag); + } + + function pushDocCommentLineText(docComments: SymbolDisplayPart[], text: string, blankLineCount: number) { + // Add the empty lines in between texts + while (blankLineCount) { + blankLineCount--; + docComments.push(textPart("")); + } + + docComments.push(textPart(text)); + } + + function getCleanedJsDocComment(pos: number, end: number, sourceFile: SourceFile) { + let spacesToRemoveAfterAsterisk: number; + const docComments: SymbolDisplayPart[] = []; + let blankLineCount = 0; + let isInParamTag = false; + + while (pos < end) { + let docCommentTextOfLine = ""; + // First consume leading white space + pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile); + + // If the comment starts with '*' consume the spaces on this line + if (pos < end && sourceFile.text.charCodeAt(pos) === CharacterCodes.asterisk) { + const lineStartPos = pos + 1; + pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, spacesToRemoveAfterAsterisk); + + // Set the spaces to remove after asterisk as margin if not already set + if (spacesToRemoveAfterAsterisk === undefined && pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { + spacesToRemoveAfterAsterisk = pos - lineStartPos; + } + } + else if (spacesToRemoveAfterAsterisk === undefined) { + spacesToRemoveAfterAsterisk = 0; + } + + // Analyze text on this line + while (pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { + const ch = sourceFile.text.charAt(pos); + if (ch === "@") { + // If it is @param tag + if (isParamTag(pos, end, sourceFile)) { + isInParamTag = true; + pos += paramTag.length; + continue; + } + else { + isInParamTag = false; + } + } + + // Add the ch to doc text if we arent in param tag + if (!isInParamTag) { + docCommentTextOfLine += ch; + } + + // Scan next character + pos++; + } + + // Continue with next line + pos = consumeLineBreaks(pos, end, sourceFile); + if (docCommentTextOfLine) { + pushDocCommentLineText(docComments, docCommentTextOfLine, blankLineCount); + blankLineCount = 0; + } + else if (!isInParamTag && docComments.length) { + // This is blank line when there is text already parsed + blankLineCount++; + } + } + + return docComments; + } + + function getCleanedParamJsDocComment(pos: number, end: number, sourceFile: SourceFile) { + let paramHelpStringMargin: number; + const paramDocComments: SymbolDisplayPart[] = []; + while (pos < end) { + if (isParamTag(pos, end, sourceFile)) { + let blankLineCount = 0; + let recordedParamTag = false; + // Consume leading spaces + pos = consumeWhiteSpaces(pos + paramTag.length); + if (pos >= end) { + break; + } + + // Ignore type expression + if (sourceFile.text.charCodeAt(pos) === CharacterCodes.openBrace) { + pos++; + for (let curlies = 1; pos < end; pos++) { + const charCode = sourceFile.text.charCodeAt(pos); + + // { character means we need to find another } to match the found one + if (charCode === CharacterCodes.openBrace) { + curlies++; + continue; + } + + // } char + if (charCode === CharacterCodes.closeBrace) { + curlies--; + if (curlies === 0) { + // We do not have any more } to match the type expression is ignored completely + pos++; + break; + } + else { + // there are more { to be matched with } + continue; + } + } + + // Found start of another tag + if (charCode === CharacterCodes.at) { + break; + } + } + + // Consume white spaces + pos = consumeWhiteSpaces(pos); + if (pos >= end) { + break; + } + } + + // Parameter name + if (isName(pos, end, sourceFile, name)) { + // Found the parameter we are looking for consume white spaces + pos = consumeWhiteSpaces(pos + name.length); + if (pos >= end) { + break; + } + + let paramHelpString = ""; + const firstLineParamHelpStringPos = pos; + while (pos < end) { + const ch = sourceFile.text.charCodeAt(pos); + + // at line break, set this comment line text and go to next line + if (isLineBreak(ch)) { + if (paramHelpString) { + pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); + paramHelpString = ""; + blankLineCount = 0; + recordedParamTag = true; + } + else if (recordedParamTag) { + blankLineCount++; + } + + // Get the pos after cleaning start of the line + setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos); + continue; + } + + // Done scanning param help string - next tag found + if (ch === CharacterCodes.at) { + break; + } + + paramHelpString += sourceFile.text.charAt(pos); + + // Go to next character + pos++; + } + + // If there is param help text, add it top the doc comments + if (paramHelpString) { + pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); + } + paramHelpStringMargin = undefined; + } + + // If this is the start of another tag, continue with the loop in search of param tag with symbol name + if (sourceFile.text.charCodeAt(pos) === CharacterCodes.at) { + continue; + } + } + + // Next character + pos++; + } + + return paramDocComments; + + function consumeWhiteSpaces(pos: number) { + while (pos < end && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { + pos++; + } + + return pos; + } + + function setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos: number) { + // Get the pos after consuming line breaks + pos = consumeLineBreaks(pos, end, sourceFile); + if (pos >= end) { + return; + } + + if (paramHelpStringMargin === undefined) { + paramHelpStringMargin = sourceFile.getLineAndCharacterOfPosition(firstLineParamHelpStringPos).character; + } + + // Now consume white spaces max + const startOfLinePos = pos; + pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile, paramHelpStringMargin); + if (pos >= end) { + return; + } + + const consumedSpaces = pos - startOfLinePos; + if (consumedSpaces < paramHelpStringMargin) { + const ch = sourceFile.text.charCodeAt(pos); + if (ch === CharacterCodes.asterisk) { + // Consume more spaces after asterisk + pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, paramHelpStringMargin - consumedSpaces - 1); + } + } + } + } + } + } + + export function getAllJsDocCompletionEntries(): CompletionEntry[] { + return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => { + return { + name: tagName, + kind: ScriptElementKind.keyword, + kindModifiers: "", + sortText: "0", + }; + })); + } + + /** + * Checks if position points to a valid position to add JSDoc comments, and if so, + * returns the appropriate template. Otherwise returns an empty string. + * Valid positions are + * - outside of comments, statements, and expressions, and + * - preceding a: + * - function/constructor/method declaration + * - class declarations + * - variable statements + * - namespace declarations + * + * Hosts should ideally check that: + * - The line is all whitespace up to 'position' before performing the insertion. + * - If the keystroke sequence "/\*\*" induced the call, we also check that the next + * non-whitespace character is '*', which (approximately) indicates whether we added + * the second '*' to complete an existing (JSDoc) comment. + * @param fileName The file in which to perform the check. + * @param position The (character-indexed) position in the file where the check should + * be performed. + */ + export function getDocCommentTemplateAtPosition(newLine: string, sourceFile: SourceFile, position: number): TextInsertion { + // Check if in a context where we don't want to perform any insertion + if (isInString(sourceFile, position) || isInComment(sourceFile, position) || hasDocComment(sourceFile, position)) { + return undefined; + } + + const tokenAtPos = getTokenAtPosition(sourceFile, position); + const tokenStart = tokenAtPos.getStart(); + if (!tokenAtPos || tokenStart < position) { + return undefined; + } + + // TODO: add support for: + // - enums/enum members + // - interfaces + // - property declarations + // - potentially property assignments + let commentOwner: Node; + findOwner: for (commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) { + switch (commentOwner.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.VariableStatement: + break findOwner; + case SyntaxKind.SourceFile: + return undefined; + case SyntaxKind.ModuleDeclaration: + // If in walking up the tree, we hit a a nested namespace declaration, + // then we must be somewhere within a dotted namespace name; however we don't + // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. + if (commentOwner.parent.kind === SyntaxKind.ModuleDeclaration) { + return undefined; + } + break findOwner; + } + } + + if (!commentOwner || commentOwner.getStart() < position) { + return undefined; + } + + const parameters = getParametersForJsDocOwningNode(commentOwner); + const posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position); + const lineStart = sourceFile.getLineStarts()[posLineAndChar.line]; + + const indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character); + + let docParams = ""; + for (let i = 0, numParams = parameters.length; i < numParams; i++) { + const currentName = parameters[i].name; + const paramName = currentName.kind === SyntaxKind.Identifier ? + (currentName).text : + "param" + i; + + docParams += `${indentationStr} * @param ${paramName}${newLine}`; + } + + // A doc comment consists of the following + // * The opening comment line + // * the first line (without a param) for the object's untagged info (this is also where the caret ends up) + // * the '@param'-tagged lines + // * TODO: other tags. + // * the closing comment line + // * if the caret was directly in front of the object, then we add an extra line and indentation. + const preamble = "/**" + newLine + + indentationStr + " * "; + const result = + preamble + newLine + + docParams + + indentationStr + " */" + + (tokenStart === position ? newLine + indentationStr : ""); + + return { newText: result, caretOffset: preamble.length }; + } + + function getParametersForJsDocOwningNode(commentOwner: Node): ParameterDeclaration[] { + if (isFunctionLike(commentOwner)) { + return commentOwner.parameters; + } + + if (commentOwner.kind === SyntaxKind.VariableStatement) { + const varStatement = commentOwner; + const varDeclarations = varStatement.declarationList.declarations; + + if (varDeclarations.length === 1 && varDeclarations[0].initializer) { + return getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer); + } + } + + return emptyArray; + } + + /** + * Digs into an an initializer or RHS operand of an assignment operation + * to get the parameters of an apt signature corresponding to a + * function expression or a class expression. + * + * @param rightHandSide the expression which may contain an appropriate set of parameters + * @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'. + */ + function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): ParameterDeclaration[] { + while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) { + rightHandSide = (rightHandSide).expression; + } + + switch (rightHandSide.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + return (rightHandSide).parameters; + case SyntaxKind.ClassExpression: + for (const member of (rightHandSide).members) { + if (member.kind === SyntaxKind.Constructor) { + return (member).parameters; + } + } + break; + } + + return emptyArray; + } +} diff --git a/src/services/meaning.ts b/src/services/meaning.ts new file mode 100644 index 00000000000..da77ed4bfda --- /dev/null +++ b/src/services/meaning.ts @@ -0,0 +1,160 @@ +/* @internal */ +namespace ts.Meaning { + export const enum SemanticMeaning { + None = 0x0, + Value = 0x1, + Type = 0x2, + Namespace = 0x4, + All = Value | Type | Namespace + } + + export function getMeaningFromDeclaration(node: Node): SemanticMeaning { + switch (node.kind) { + case SyntaxKind.Parameter: + case SyntaxKind.VariableDeclaration: + case SyntaxKind.BindingElement: + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.EnumMember: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.Constructor: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.CatchClause: + return SemanticMeaning.Value; + + case SyntaxKind.TypeParameter: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.TypeAliasDeclaration: + case SyntaxKind.TypeLiteral: + return SemanticMeaning.Type; + + case SyntaxKind.ClassDeclaration: + case SyntaxKind.EnumDeclaration: + return SemanticMeaning.Value | SemanticMeaning.Type; + + case SyntaxKind.ModuleDeclaration: + if (isAmbientModule(node)) { + return SemanticMeaning.Namespace | SemanticMeaning.Value; + } + else if (getModuleInstanceState(node) === ModuleInstanceState.Instantiated) { + return SemanticMeaning.Namespace | SemanticMeaning.Value; + } + else { + return SemanticMeaning.Namespace; + } + + case SyntaxKind.NamedImports: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExportAssignment: + case SyntaxKind.ExportDeclaration: + return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + + // An external module can be a Value + case SyntaxKind.SourceFile: + return SemanticMeaning.Namespace | SemanticMeaning.Value; + } + + return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + } + + export function getMeaningFromLocation(node: Node): SemanticMeaning { + if (node.parent.kind === SyntaxKind.ExportAssignment) { + return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + } + else if (isInRightSideOfImport(node)) { + return getMeaningFromRightHandSideOfImportEquals(node); + } + else if (isDeclarationName(node)) { + return getMeaningFromDeclaration(node.parent); + } + else if (isTypeReference(node)) { + return SemanticMeaning.Type; + } + else if (isNamespaceReference(node)) { + return SemanticMeaning.Namespace; + } + else { + return SemanticMeaning.Value; + } + } + + function getMeaningFromRightHandSideOfImportEquals(node: Node) { + Debug.assert(node.kind === SyntaxKind.Identifier); + + // import a = |b|; // Namespace + // import a = |b.c|; // Value, type, namespace + // import a = |b.c|.d; // Namespace + + if (node.parent.kind === SyntaxKind.QualifiedName && + (node.parent).right === node && + node.parent.parent.kind === SyntaxKind.ImportEqualsDeclaration) { + return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; + } + return SemanticMeaning.Namespace; + } + + function isInRightSideOfImport(node: Node) { + while (node.parent.kind === SyntaxKind.QualifiedName) { + node = node.parent; + } + return isInternalModuleImportEqualsDeclaration(node.parent) && (node.parent).moduleReference === node; + } + + function isNamespaceReference(node: Node): boolean { + return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node); + } + + function isQualifiedNameNamespaceReference(node: Node): boolean { + let root = node; + let isLastClause = true; + if (root.parent.kind === SyntaxKind.QualifiedName) { + while (root.parent && root.parent.kind === SyntaxKind.QualifiedName) { + root = root.parent; + } + + isLastClause = (root).right === node; + } + + return root.parent.kind === SyntaxKind.TypeReference && !isLastClause; + } + + function isPropertyAccessNamespaceReference(node: Node): boolean { + let root = node; + let isLastClause = true; + if (root.parent.kind === SyntaxKind.PropertyAccessExpression) { + while (root.parent && root.parent.kind === SyntaxKind.PropertyAccessExpression) { + root = root.parent; + } + + isLastClause = (root).name === node; + } + + if (!isLastClause && root.parent.kind === SyntaxKind.ExpressionWithTypeArguments && root.parent.parent.kind === SyntaxKind.HeritageClause) { + const decl = root.parent.parent.parent; + return (decl.kind === SyntaxKind.ClassDeclaration && (root.parent.parent).token === SyntaxKind.ImplementsKeyword) || + (decl.kind === SyntaxKind.InterfaceDeclaration && (root.parent.parent).token === SyntaxKind.ExtendsKeyword); + } + + return false; + } + + function isTypeReference(node: Node): boolean { + if (isRightSideOfQualifiedNameOrPropertyAccess(node)) { + node = node.parent; + } + + return node.parent.kind === SyntaxKind.TypeReference || + (node.parent.kind === SyntaxKind.ExpressionWithTypeArguments && !isExpressionWithTypeArgumentsInClassExtendsClause(node.parent)) || + (node.kind === SyntaxKind.ThisKeyword && !isPartOfExpression(node)) || + node.kind === SyntaxKind.ThisType; + } +} diff --git a/src/services/preProcess.ts b/src/services/preProcess.ts new file mode 100644 index 00000000000..8b3d42af328 --- /dev/null +++ b/src/services/preProcess.ts @@ -0,0 +1,360 @@ +/* @internal */ +namespace ts.PreProcess { + export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { + const referencedFiles: FileReference[] = []; + const typeReferenceDirectives: FileReference[] = []; + const importedFiles: FileReference[] = []; + let ambientExternalModules: { ref: FileReference, depth: number }[]; + let isNoDefaultLib = false; + let braceNesting = 0; + // assume that text represent an external module if it contains at least one top level import/export + // ambient modules that are found inside external modules are interpreted as module augmentations + let externalModule = false; + + function nextToken() { + const token = scanner.scan(); + if (token === SyntaxKind.OpenBraceToken) { + braceNesting++; + } + else if (token === SyntaxKind.CloseBraceToken) { + braceNesting--; + } + return token; + } + + function processTripleSlashDirectives(): void { + const commentRanges = getLeadingCommentRanges(sourceText, 0); + forEach(commentRanges, commentRange => { + const comment = sourceText.substring(commentRange.pos, commentRange.end); + const referencePathMatchResult = getFileReferenceFromReferencePath(comment, commentRange); + if (referencePathMatchResult) { + isNoDefaultLib = referencePathMatchResult.isNoDefaultLib; + const fileReference = referencePathMatchResult.fileReference; + if (fileReference) { + const collection = referencePathMatchResult.isTypeReferenceDirective + ? typeReferenceDirectives + : referencedFiles; + + collection.push(fileReference); + } + } + }); + } + + function getFileReference() { + const file = scanner.getTokenValue(); + const pos = scanner.getTokenPos(); + return { + fileName: file, + pos: pos, + end: pos + file.length + }; + } + + function recordAmbientExternalModule(): void { + if (!ambientExternalModules) { + ambientExternalModules = []; + } + ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); + } + + function recordModuleName() { + importedFiles.push(getFileReference()); + + markAsExternalModuleIfTopLevel(); + } + + function markAsExternalModuleIfTopLevel() { + if (braceNesting === 0) { + externalModule = true; + } + } + + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeDeclare(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.DeclareKeyword) { + // declare module "mod" + token = nextToken(); + if (token === SyntaxKind.ModuleKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + recordAmbientExternalModule(); + } + } + return true; + } + + return false; + } + + /** + * Returns true if at least one token was consumed from the stream + */ + function tryConsumeImport(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.ImportKeyword) { + + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // import "mod"; + recordModuleName(); + return true; + } + else { + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // import d from "mod"; + recordModuleName(); + return true; + } + } + else if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; + } + } + else if (token === SyntaxKind.CommaToken) { + // consume comma and keep going + token = nextToken(); + } + else { + // unknown syntax + return true; + } + } + + if (token === SyntaxKind.OpenBraceToken) { + token = nextToken(); + // consume "{ a as B, c, d as D}" clauses + // make sure that it stops on EOF + while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { + token = nextToken(); + } + + if (token === SyntaxKind.CloseBraceToken) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // import {a as A} from "mod"; + // import d, {a, b as B} from "mod" + recordModuleName(); + } + } + } + } + else if (token === SyntaxKind.AsteriskToken) { + token = nextToken(); + if (token === SyntaxKind.AsKeyword) { + token = nextToken(); + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // import * as NS from "mod" + // import d, * as NS from "mod" + recordModuleName(); + } + } + } + } + } + } + + return true; + } + + return false; + } + + function tryConsumeExport(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.ExportKeyword) { + markAsExternalModuleIfTopLevel(); + token = nextToken(); + if (token === SyntaxKind.OpenBraceToken) { + token = nextToken(); + // consume "{ a as B, c, d as D}" clauses + // make sure it stops on EOF + while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { + token = nextToken(); + } + + if (token === SyntaxKind.CloseBraceToken) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // export {a as A} from "mod"; + // export {a, b as B} from "mod" + recordModuleName(); + } + } + } + } + else if (token === SyntaxKind.AsteriskToken) { + token = nextToken(); + if (token === SyntaxKind.FromKeyword) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // export * from "mod" + recordModuleName(); + } + } + } + else if (token === SyntaxKind.ImportKeyword) { + token = nextToken(); + if (token === SyntaxKind.Identifier || isKeyword(token)) { + token = nextToken(); + if (token === SyntaxKind.EqualsToken) { + if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { + return true; + } + } + } + } + + return true; + } + + return false; + } + + function tryConsumeRequireCall(skipCurrentToken: boolean): boolean { + let token = skipCurrentToken ? nextToken() : scanner.getToken(); + if (token === SyntaxKind.RequireKeyword) { + token = nextToken(); + if (token === SyntaxKind.OpenParenToken) { + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // require("mod"); + recordModuleName(); + } + } + return true; + } + return false; + } + + function tryConsumeDefine(): boolean { + let token = scanner.getToken(); + if (token === SyntaxKind.Identifier && scanner.getTokenValue() === "define") { + token = nextToken(); + if (token !== SyntaxKind.OpenParenToken) { + return true; + } + + token = nextToken(); + if (token === SyntaxKind.StringLiteral) { + // looks like define ("modname", ... - skip string literal and comma + token = nextToken(); + if (token === SyntaxKind.CommaToken) { + token = nextToken(); + } + else { + // unexpected token + return true; + } + } + + // should be start of dependency list + if (token !== SyntaxKind.OpenBracketToken) { + return true; + } + + // skip open bracket + token = nextToken(); + let i = 0; + // scan until ']' or EOF + while (token !== SyntaxKind.CloseBracketToken && token !== SyntaxKind.EndOfFileToken) { + // record string literals as module names + if (token === SyntaxKind.StringLiteral) { + recordModuleName(); + i++; + } + + token = nextToken(); + } + return true; + + } + return false; + } + + function processImports(): void { + scanner.setText(sourceText); + nextToken(); + // Look for: + // import "mod"; + // import d from "mod" + // import {a as A } from "mod"; + // import * as NS from "mod" + // import d, {a, b as B} from "mod" + // import i = require("mod"); + // + // export * from "mod" + // export {a as b} from "mod" + // export import i = require("mod") + // (for JavaScript files) require("mod") + + while (true) { + if (scanner.getToken() === SyntaxKind.EndOfFileToken) { + break; + } + + // check if at least one of alternative have moved scanner forward + if (tryConsumeDeclare() || + tryConsumeImport() || + tryConsumeExport() || + (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false) || tryConsumeDefine()))) { + continue; + } + else { + nextToken(); + } + } + + scanner.setText(undefined); + } + + if (readImportFiles) { + processImports(); + } + processTripleSlashDirectives(); + if (externalModule) { + // for external modules module all nested ambient modules are augmentations + if (ambientExternalModules) { + // move all detected ambient modules to imported files since they need to be resolved + for (const decl of ambientExternalModules) { + importedFiles.push(decl.ref); + } + } + return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: undefined }; + } + else { + // for global scripts ambient modules still can have augmentations - look for ambient modules with depth > 0 + let ambientModuleNames: string[]; + if (ambientExternalModules) { + for (const decl of ambientExternalModules) { + if (decl.depth === 0) { + if (!ambientModuleNames) { + ambientModuleNames = []; + } + ambientModuleNames.push(decl.ref.fileName); + } + else { + importedFiles.push(decl.ref); + } + } + } + return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: ambientModuleNames }; + } + } +} diff --git a/src/services/services.ts b/src/services/services.ts index eaba4779a52..51976cfdc61 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1,15 +1,25 @@ /// /// -/// -/// -/// -/// -/// -/// /// /// +/// +/// +/// +/// +/// +/// +/// +/// /// +/// +/// +/// +/// +/// +/// +/// +/// /// /// @@ -17,1018 +27,6 @@ namespace ts { /** The version of the language service API */ export const servicesVersion = "0.5"; - const scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); - - const emptyArray: any[] = []; - - const jsDocTagNames = [ - "augments", - "author", - "argument", - "borrows", - "class", - "constant", - "constructor", - "constructs", - "default", - "deprecated", - "description", - "event", - "example", - "extends", - "field", - "fileOverview", - "function", - "ignore", - "inner", - "lends", - "link", - "memberOf", - "name", - "namespace", - "param", - "private", - "property", - "public", - "requires", - "returns", - "see", - "since", - "static", - "throws", - "type", - "typedef", - "property", - "prop", - "version" - ]; - let jsDocCompletionEntries: CompletionEntry[]; - - function createNode(kind: SyntaxKind, pos: number, end: number, parent?: Node): NodeObject | TokenObject | IdentifierObject { - const node = kind >= SyntaxKind.FirstNode ? new NodeObject(kind, pos, end) : - kind === SyntaxKind.Identifier ? new IdentifierObject(kind, pos, end) : - new TokenObject(kind, pos, end); - node.parent = parent; - return node; - } - - class NodeObject implements Node { - public kind: SyntaxKind; - public pos: number; - public end: number; - public flags: NodeFlags; - public parent: Node; - public jsDocComments: JSDocComment[]; - public original: Node; - public transformFlags: TransformFlags; - public excludeTransformFlags: TransformFlags; - private _children: Node[]; - - constructor(kind: SyntaxKind, pos: number, end: number) { - this.pos = pos; - this.end = end; - this.flags = NodeFlags.None; - this.transformFlags = undefined; - this.excludeTransformFlags = undefined; - this.parent = undefined; - this.kind = kind; - } - - public getSourceFile(): SourceFile { - return getSourceFileOfNode(this); - } - - public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { - return getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } - - public getFullStart(): number { - return this.pos; - } - - public getEnd(): number { - return this.end; - } - - public getWidth(sourceFile?: SourceFile): number { - return this.getEnd() - this.getStart(sourceFile); - } - - public getFullWidth(): number { - return this.end - this.pos; - } - - public getLeadingTriviaWidth(sourceFile?: SourceFile): number { - return this.getStart(sourceFile) - this.pos; - } - - public getFullText(sourceFile?: SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } - - public getText(sourceFile?: SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.getStart(), this.getEnd()); - } - - private addSyntheticNodes(nodes: Node[], pos: number, end: number, useJSDocScanner?: boolean): number { - scanner.setTextPos(pos); - while (pos < end) { - const token = useJSDocScanner ? scanner.scanJSDocToken() : scanner.scan(); - const textPos = scanner.getTextPos(); - if (textPos <= end) { - nodes.push(createNode(token, pos, textPos, this)); - } - pos = textPos; - } - return pos; - } - - private createSyntaxList(nodes: NodeArray): Node { - const list = createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, this); - list._children = []; - let pos = nodes.pos; - - for (const node of nodes) { - if (pos < node.pos) { - pos = this.addSyntheticNodes(list._children, pos, node.pos); - } - list._children.push(node); - pos = node.end; - } - if (pos < nodes.end) { - this.addSyntheticNodes(list._children, pos, nodes.end); - } - return list; - } - - private createChildren(sourceFile?: SourceFile) { - let children: Node[]; - if (this.kind >= SyntaxKind.FirstNode) { - scanner.setText((sourceFile || this.getSourceFile()).text); - children = []; - let pos = this.pos; - const useJSDocScanner = this.kind >= SyntaxKind.FirstJSDocTagNode && this.kind <= SyntaxKind.LastJSDocTagNode; - const processNode = (node: Node) => { - const isJSDocTagNode = isJSDocTag(node); - if (!isJSDocTagNode && pos < node.pos) { - pos = this.addSyntheticNodes(children, pos, node.pos, useJSDocScanner); - } - children.push(node); - if (!isJSDocTagNode) { - pos = node.end; - } - }; - const processNodes = (nodes: NodeArray) => { - if (pos < nodes.pos) { - pos = this.addSyntheticNodes(children, pos, nodes.pos, useJSDocScanner); - } - children.push(this.createSyntaxList(>nodes)); - pos = nodes.end; - }; - // jsDocComments need to be the first children - if (this.jsDocComments) { - for (const jsDocComment of this.jsDocComments) { - processNode(jsDocComment); - } - } - // For syntactic classifications, all trivia are classcified together, including jsdoc comments. - // For that to work, the jsdoc comments should still be the leading trivia of the first child. - // Restoring the scanner position ensures that. - pos = this.pos; - forEachChild(this, processNode, processNodes); - if (pos < this.end) { - this.addSyntheticNodes(children, pos, this.end); - } - scanner.setText(undefined); - } - this._children = children || emptyArray; - } - - public getChildCount(sourceFile?: SourceFile): number { - if (!this._children) this.createChildren(sourceFile); - return this._children.length; - } - - public getChildAt(index: number, sourceFile?: SourceFile): Node { - if (!this._children) this.createChildren(sourceFile); - return this._children[index]; - } - - public getChildren(sourceFile?: SourceFile): Node[] { - if (!this._children) this.createChildren(sourceFile); - return this._children; - } - - public getFirstToken(sourceFile?: SourceFile): Node { - const children = this.getChildren(sourceFile); - if (!children.length) { - return undefined; - } - - const child = children[0]; - - return child.kind < SyntaxKind.FirstNode ? child : child.getFirstToken(sourceFile); - } - - public getLastToken(sourceFile?: SourceFile): Node { - const children = this.getChildren(sourceFile); - - const child = lastOrUndefined(children); - if (!child) { - return undefined; - } - - return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile); - } - } - - class TokenOrIdentifierObject implements Token { - public kind: SyntaxKind; - public pos: number; - public end: number; - public flags: NodeFlags; - public parent: Node; - public jsDocComments: JSDocComment[]; - public __tokenTag: any; - - constructor(pos: number, end: number) { - // Set properties in same order as NodeObject - this.pos = pos; - this.end = end; - this.flags = NodeFlags.None; - this.parent = undefined; - } - - public getSourceFile(): SourceFile { - return getSourceFileOfNode(this); - } - - public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { - return getTokenPosOfNode(this, sourceFile, includeJsDocComment); - } - - public getFullStart(): number { - return this.pos; - } - - public getEnd(): number { - return this.end; - } - - public getWidth(sourceFile?: SourceFile): number { - return this.getEnd() - this.getStart(sourceFile); - } - - public getFullWidth(): number { - return this.end - this.pos; - } - - public getLeadingTriviaWidth(sourceFile?: SourceFile): number { - return this.getStart(sourceFile) - this.pos; - } - - public getFullText(sourceFile?: SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end); - } - - public getText(sourceFile?: SourceFile): string { - return (sourceFile || this.getSourceFile()).text.substring(this.getStart(), this.getEnd()); - } - - public getChildCount(sourceFile?: SourceFile): number { - return 0; - } - - public getChildAt(index: number, sourceFile?: SourceFile): Node { - return undefined; - } - - public getChildren(sourceFile?: SourceFile): Node[] { - return emptyArray; - } - - public getFirstToken(sourceFile?: SourceFile): Node { - return undefined; - } - - public getLastToken(sourceFile?: SourceFile): Node { - return undefined; - } - } - - class TokenObject extends TokenOrIdentifierObject { - public kind: SyntaxKind; - constructor(kind: SyntaxKind, pos: number, end: number) { - super(pos, end); - this.kind = kind; - } - } - - class IdentifierObject extends TokenOrIdentifierObject { - constructor(kind: SyntaxKind, pos: number, end: number) { - super(pos, end); - } - } - IdentifierObject.prototype.kind = SyntaxKind.Identifier; - - class SymbolObject implements Symbol { - flags: SymbolFlags; - name: string; - declarations: Declaration[]; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no doc comment, then the empty string will be returned. - documentationComment: SymbolDisplayPart[]; - - constructor(flags: SymbolFlags, name: string) { - this.flags = flags; - this.name = name; - } - - getFlags(): SymbolFlags { - return this.flags; - } - - getName(): string { - return this.name; - } - - getDeclarations(): Declaration[] { - return this.declarations; - } - - getDocumentationComment(): SymbolDisplayPart[] { - if (this.documentationComment === undefined) { - this.documentationComment = getJsDocCommentsFromDeclarations(this.declarations, this.name, !(this.flags & SymbolFlags.Property)); - } - - return this.documentationComment; - } - } - - function getJsDocCommentsFromDeclarations(declarations: Declaration[], name: string, canUseParsedParamTagComments: boolean) { - const documentationComment = []; - const docComments = getJsDocCommentsSeparatedByNewLines(); - ts.forEach(docComments, docComment => { - if (documentationComment.length) { - documentationComment.push(lineBreakPart()); - } - documentationComment.push(docComment); - }); - - return documentationComment; - - function getJsDocCommentsSeparatedByNewLines() { - const paramTag = "@param"; - const jsDocCommentParts: SymbolDisplayPart[] = []; - - ts.forEach(declarations, (declaration, indexOfDeclaration) => { - // Make sure we are collecting doc comment from declaration once, - // In case of union property there might be same declaration multiple times - // which only varies in type parameter - // Eg. const a: Array | Array; a.length - // The property length will have two declarations of property length coming - // from Array - Array and Array - if (indexOf(declarations, declaration) === indexOfDeclaration) { - const sourceFileOfDeclaration = getSourceFileOfNode(declaration); - // If it is parameter - try and get the jsDoc comment with @param tag from function declaration's jsDoc comments - if (canUseParsedParamTagComments && declaration.kind === SyntaxKind.Parameter) { - if ((declaration.parent.kind === SyntaxKind.FunctionExpression || declaration.parent.kind === SyntaxKind.ArrowFunction) && - declaration.parent.parent.kind === SyntaxKind.VariableDeclaration) { - addCommentParts(declaration.parent.parent.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); - } - addCommentParts(declaration.parent, sourceFileOfDeclaration, getCleanedParamJsDocComment); - } - - // If this is left side of dotted module declaration, there is no doc comments associated with this node - if (declaration.kind === SyntaxKind.ModuleDeclaration && (declaration).body && (declaration).body.kind === SyntaxKind.ModuleDeclaration) { - return; - } - - if ((declaration.kind === SyntaxKind.FunctionExpression || declaration.kind === SyntaxKind.ArrowFunction) && - declaration.parent.kind === SyntaxKind.VariableDeclaration) { - addCommentParts(declaration.parent.parent, sourceFileOfDeclaration, getCleanedJsDocComment); - } - - // If this is dotted module name, get the doc comments from the parent - while (declaration.kind === SyntaxKind.ModuleDeclaration && declaration.parent.kind === SyntaxKind.ModuleDeclaration) { - declaration = declaration.parent; - } - addCommentParts(declaration.kind === SyntaxKind.VariableDeclaration ? declaration.parent.parent : declaration, - sourceFileOfDeclaration, - getCleanedJsDocComment); - - if (declaration.kind === SyntaxKind.VariableDeclaration) { - const init = (declaration as VariableDeclaration).initializer; - if (init && (init.kind === SyntaxKind.FunctionExpression || init.kind === SyntaxKind.ArrowFunction)) { - // Get the cleaned js doc comment text from the initializer - addCommentParts(init, sourceFileOfDeclaration, getCleanedJsDocComment); - } - } - } - }); - - return jsDocCommentParts; - - function addCommentParts(commented: Node, - sourceFileOfDeclaration: SourceFile, - getCommentPart: (pos: number, end: number, file: SourceFile) => SymbolDisplayPart[]): void { - const ranges = getJsDocCommentTextRange(commented, sourceFileOfDeclaration); - // Get the cleaned js doc comment text from the declaration - ts.forEach(ranges, jsDocCommentTextRange => { - const cleanedComment = getCommentPart(jsDocCommentTextRange.pos, jsDocCommentTextRange.end, sourceFileOfDeclaration); - if (cleanedComment) { - addRange(jsDocCommentParts, cleanedComment); - } - }); - } - - function getJsDocCommentTextRange(node: Node, sourceFile: SourceFile): TextRange[] { - return ts.map(getJsDocComments(node, sourceFile), - jsDocComment => { - return { - pos: jsDocComment.pos + "/*".length, // Consume /* from the comment - end: jsDocComment.end - "*/".length // Trim off comment end indicator - }; - }); - } - - function consumeWhiteSpacesOnTheLine(pos: number, end: number, sourceFile: SourceFile, maxSpacesToRemove?: number) { - if (maxSpacesToRemove !== undefined) { - end = Math.min(end, pos + maxSpacesToRemove); - } - - for (; pos < end; pos++) { - const ch = sourceFile.text.charCodeAt(pos); - if (!isWhiteSpaceSingleLine(ch)) { - return pos; - } - } - - return end; - } - - function consumeLineBreaks(pos: number, end: number, sourceFile: SourceFile) { - while (pos < end && isLineBreak(sourceFile.text.charCodeAt(pos))) { - pos++; - } - - return pos; - } - - function isName(pos: number, end: number, sourceFile: SourceFile, name: string) { - return pos + name.length < end && - sourceFile.text.substr(pos, name.length) === name && - isWhiteSpace(sourceFile.text.charCodeAt(pos + name.length)); - } - - function isParamTag(pos: number, end: number, sourceFile: SourceFile) { - // If it is @param tag - return isName(pos, end, sourceFile, paramTag); - } - - function pushDocCommentLineText(docComments: SymbolDisplayPart[], text: string, blankLineCount: number) { - // Add the empty lines in between texts - while (blankLineCount) { - blankLineCount--; - docComments.push(textPart("")); - } - - docComments.push(textPart(text)); - } - - function getCleanedJsDocComment(pos: number, end: number, sourceFile: SourceFile) { - let spacesToRemoveAfterAsterisk: number; - const docComments: SymbolDisplayPart[] = []; - let blankLineCount = 0; - let isInParamTag = false; - - while (pos < end) { - let docCommentTextOfLine = ""; - // First consume leading white space - pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile); - - // If the comment starts with '*' consume the spaces on this line - if (pos < end && sourceFile.text.charCodeAt(pos) === CharacterCodes.asterisk) { - const lineStartPos = pos + 1; - pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, spacesToRemoveAfterAsterisk); - - // Set the spaces to remove after asterisk as margin if not already set - if (spacesToRemoveAfterAsterisk === undefined && pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { - spacesToRemoveAfterAsterisk = pos - lineStartPos; - } - } - else if (spacesToRemoveAfterAsterisk === undefined) { - spacesToRemoveAfterAsterisk = 0; - } - - // Analyze text on this line - while (pos < end && !isLineBreak(sourceFile.text.charCodeAt(pos))) { - const ch = sourceFile.text.charAt(pos); - if (ch === "@") { - // If it is @param tag - if (isParamTag(pos, end, sourceFile)) { - isInParamTag = true; - pos += paramTag.length; - continue; - } - else { - isInParamTag = false; - } - } - - // Add the ch to doc text if we arent in param tag - if (!isInParamTag) { - docCommentTextOfLine += ch; - } - - // Scan next character - pos++; - } - - // Continue with next line - pos = consumeLineBreaks(pos, end, sourceFile); - if (docCommentTextOfLine) { - pushDocCommentLineText(docComments, docCommentTextOfLine, blankLineCount); - blankLineCount = 0; - } - else if (!isInParamTag && docComments.length) { - // This is blank line when there is text already parsed - blankLineCount++; - } - } - - return docComments; - } - - function getCleanedParamJsDocComment(pos: number, end: number, sourceFile: SourceFile) { - let paramHelpStringMargin: number; - const paramDocComments: SymbolDisplayPart[] = []; - while (pos < end) { - if (isParamTag(pos, end, sourceFile)) { - let blankLineCount = 0; - let recordedParamTag = false; - // Consume leading spaces - pos = consumeWhiteSpaces(pos + paramTag.length); - if (pos >= end) { - break; - } - - // Ignore type expression - if (sourceFile.text.charCodeAt(pos) === CharacterCodes.openBrace) { - pos++; - for (let curlies = 1; pos < end; pos++) { - const charCode = sourceFile.text.charCodeAt(pos); - - // { character means we need to find another } to match the found one - if (charCode === CharacterCodes.openBrace) { - curlies++; - continue; - } - - // } char - if (charCode === CharacterCodes.closeBrace) { - curlies--; - if (curlies === 0) { - // We do not have any more } to match the type expression is ignored completely - pos++; - break; - } - else { - // there are more { to be matched with } - continue; - } - } - - // Found start of another tag - if (charCode === CharacterCodes.at) { - break; - } - } - - // Consume white spaces - pos = consumeWhiteSpaces(pos); - if (pos >= end) { - break; - } - } - - // Parameter name - if (isName(pos, end, sourceFile, name)) { - // Found the parameter we are looking for consume white spaces - pos = consumeWhiteSpaces(pos + name.length); - if (pos >= end) { - break; - } - - let paramHelpString = ""; - const firstLineParamHelpStringPos = pos; - while (pos < end) { - const ch = sourceFile.text.charCodeAt(pos); - - // at line break, set this comment line text and go to next line - if (isLineBreak(ch)) { - if (paramHelpString) { - pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); - paramHelpString = ""; - blankLineCount = 0; - recordedParamTag = true; - } - else if (recordedParamTag) { - blankLineCount++; - } - - // Get the pos after cleaning start of the line - setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos); - continue; - } - - // Done scanning param help string - next tag found - if (ch === CharacterCodes.at) { - break; - } - - paramHelpString += sourceFile.text.charAt(pos); - - // Go to next character - pos++; - } - - // If there is param help text, add it top the doc comments - if (paramHelpString) { - pushDocCommentLineText(paramDocComments, paramHelpString, blankLineCount); - } - paramHelpStringMargin = undefined; - } - - // If this is the start of another tag, continue with the loop in search of param tag with symbol name - if (sourceFile.text.charCodeAt(pos) === CharacterCodes.at) { - continue; - } - } - - // Next character - pos++; - } - - return paramDocComments; - - function consumeWhiteSpaces(pos: number) { - while (pos < end && isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(pos))) { - pos++; - } - - return pos; - } - - function setPosForParamHelpStringOnNextLine(firstLineParamHelpStringPos: number) { - // Get the pos after consuming line breaks - pos = consumeLineBreaks(pos, end, sourceFile); - if (pos >= end) { - return; - } - - if (paramHelpStringMargin === undefined) { - paramHelpStringMargin = sourceFile.getLineAndCharacterOfPosition(firstLineParamHelpStringPos).character; - } - - // Now consume white spaces max - const startOfLinePos = pos; - pos = consumeWhiteSpacesOnTheLine(pos, end, sourceFile, paramHelpStringMargin); - if (pos >= end) { - return; - } - - const consumedSpaces = pos - startOfLinePos; - if (consumedSpaces < paramHelpStringMargin) { - const ch = sourceFile.text.charCodeAt(pos); - if (ch === CharacterCodes.asterisk) { - // Consume more spaces after asterisk - pos = consumeWhiteSpacesOnTheLine(pos + 1, end, sourceFile, paramHelpStringMargin - consumedSpaces - 1); - } - } - } - } - } - } - - class TypeObject implements Type { - checker: TypeChecker; - flags: TypeFlags; - id: number; - symbol: Symbol; - constructor(checker: TypeChecker, flags: TypeFlags) { - this.checker = checker; - this.flags = flags; - } - getFlags(): TypeFlags { - return this.flags; - } - getSymbol(): Symbol { - return this.symbol; - } - getProperties(): Symbol[] { - return this.checker.getPropertiesOfType(this); - } - getProperty(propertyName: string): Symbol { - return this.checker.getPropertyOfType(this, propertyName); - } - getApparentProperties(): Symbol[] { - return this.checker.getAugmentedPropertiesOfType(this); - } - getCallSignatures(): Signature[] { - return this.checker.getSignaturesOfType(this, SignatureKind.Call); - } - getConstructSignatures(): Signature[] { - return this.checker.getSignaturesOfType(this, SignatureKind.Construct); - } - getStringIndexType(): Type { - return this.checker.getIndexTypeOfType(this, IndexKind.String); - } - getNumberIndexType(): Type { - return this.checker.getIndexTypeOfType(this, IndexKind.Number); - } - getBaseTypes(): ObjectType[] { - return this.flags & (TypeFlags.Class | TypeFlags.Interface) - ? this.checker.getBaseTypes(this) - : undefined; - } - getNonNullableType(): Type { - return this.checker.getNonNullableType(this); - } - } - - class SignatureObject implements Signature { - checker: TypeChecker; - declaration: SignatureDeclaration; - typeParameters: TypeParameter[]; - parameters: Symbol[]; - thisParameter: Symbol; - resolvedReturnType: Type; - minArgumentCount: number; - hasRestParameter: boolean; - hasLiteralTypes: boolean; - - // Undefined is used to indicate the value has not been computed. If, after computing, the - // symbol has no doc comment, then the empty string will be returned. - documentationComment: SymbolDisplayPart[]; - - constructor(checker: TypeChecker) { - this.checker = checker; - } - getDeclaration(): SignatureDeclaration { - return this.declaration; - } - getTypeParameters(): Type[] { - return this.typeParameters; - } - getParameters(): Symbol[] { - return this.parameters; - } - getReturnType(): Type { - return this.checker.getReturnTypeOfSignature(this); - } - - getDocumentationComment(): SymbolDisplayPart[] { - if (this.documentationComment === undefined) { - this.documentationComment = this.declaration ? getJsDocCommentsFromDeclarations( - [this.declaration], - /*name*/ undefined, - /*canUseParsedParamTagComments*/ false) : []; - } - - return this.documentationComment; - } - } - - class SourceFileObject extends NodeObject implements SourceFile { - public _declarationBrand: any; - public fileName: string; - public path: Path; - public text: string; - public scriptSnapshot: IScriptSnapshot; - public lineMap: number[]; - - public statements: NodeArray; - public endOfFileToken: Node; - - public amdDependencies: { name: string; path: string }[]; - public moduleName: string; - public referencedFiles: FileReference[]; - public typeReferenceDirectives: FileReference[]; - - public syntacticDiagnostics: Diagnostic[]; - public referenceDiagnostics: Diagnostic[]; - public parseDiagnostics: Diagnostic[]; - public bindDiagnostics: Diagnostic[]; - - public isDeclarationFile: boolean; - public isDefaultLib: boolean; - public hasNoDefaultLib: boolean; - public externalModuleIndicator: Node; // The first node that causes this file to be an external module - public commonJsModuleIndicator: Node; // The first node that causes this file to be a CommonJS module - public nodeCount: number; - public identifierCount: number; - public symbolCount: number; - public version: string; - public scriptKind: ScriptKind; - public languageVersion: ScriptTarget; - public languageVariant: LanguageVariant; - public identifiers: Map; - public nameTable: Map; - public resolvedModules: Map; - public resolvedTypeReferenceDirectiveNames: Map; - public imports: LiteralExpression[]; - public moduleAugmentations: LiteralExpression[]; - private namedDeclarations: Map; - - constructor(kind: SyntaxKind, pos: number, end: number) { - super(kind, pos, end); - } - - public update(newText: string, textChangeRange: TextChangeRange): SourceFile { - return updateSourceFile(this, newText, textChangeRange); - } - - public getLineAndCharacterOfPosition(position: number): LineAndCharacter { - return ts.getLineAndCharacterOfPosition(this, position); - } - - public getLineStarts(): number[] { - return getLineStarts(this); - } - - public getPositionOfLineAndCharacter(line: number, character: number): number { - return ts.getPositionOfLineAndCharacter(this, line, character); - } - - public getNamedDeclarations(): Map { - if (!this.namedDeclarations) { - this.namedDeclarations = this.computeNamedDeclarations(); - } - - return this.namedDeclarations; - } - - private computeNamedDeclarations(): Map { - const result = createMap(); - - forEachChild(this, visit); - - return result; - - function addDeclaration(declaration: Declaration) { - const name = getDeclarationName(declaration); - if (name) { - multiMapAdd(result, name, declaration); - } - } - - function getDeclarations(name: string) { - return result[name] || (result[name] = []); - } - - function getDeclarationName(declaration: Declaration) { - if (declaration.name) { - const result = getTextOfIdentifierOrLiteral(declaration.name); - if (result !== undefined) { - return result; - } - - if (declaration.name.kind === SyntaxKind.ComputedPropertyName) { - const expr = (declaration.name).expression; - if (expr.kind === SyntaxKind.PropertyAccessExpression) { - return (expr).name.text; - } - - return getTextOfIdentifierOrLiteral(expr); - } - } - - return undefined; - } - - function getTextOfIdentifierOrLiteral(node: Node) { - if (node) { - if (node.kind === SyntaxKind.Identifier || - node.kind === SyntaxKind.StringLiteral || - node.kind === SyntaxKind.NumericLiteral) { - - return (node).text; - } - } - - return undefined; - } - - function visit(node: Node): void { - switch (node.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - const functionDeclaration = node; - const declarationName = getDeclarationName(functionDeclaration); - - if (declarationName) { - const declarations = getDeclarations(declarationName); - const lastDeclaration = lastOrUndefined(declarations); - - // Check whether this declaration belongs to an "overload group". - if (lastDeclaration && functionDeclaration.parent === lastDeclaration.parent && functionDeclaration.symbol === lastDeclaration.symbol) { - // Overwrite the last declaration if it was an overload - // and this one is an implementation. - if (functionDeclaration.body && !(lastDeclaration).body) { - declarations[declarations.length - 1] = functionDeclaration; - } - } - else { - declarations.push(functionDeclaration); - } - - forEachChild(node, visit); - } - break; - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportClause: - case SyntaxKind.NamespaceImport: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.TypeLiteral: - addDeclaration(node); - forEachChild(node, visit); - break; - - case SyntaxKind.Parameter: - // Only consider parameter properties - if (!hasModifier(node, ModifierFlags.ParameterPropertyModifier)) { - break; - } - // fall through - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: { - const decl = node; - if (isBindingPattern(decl.name)) { - forEachChild(decl.name, visit); - break; - } - if (decl.initializer) - visit(decl.initializer); - } - case SyntaxKind.EnumMember: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - addDeclaration(node); - break; - - case SyntaxKind.ExportDeclaration: - // Handle named exports case e.g.: - // export {a, b as B} from "mod"; - if ((node).exportClause) { - forEach((node).exportClause.elements, visit); - } - break; - - case SyntaxKind.ImportDeclaration: - const importClause = (node).importClause; - if (importClause) { - // Handle default import case e.g.: - // import d from "mod"; - if (importClause.name) { - addDeclaration(importClause); - } - - // Handle named bindings in imports e.g.: - // import * as NS from "mod"; - // import {a, b as B} from "mod"; - if (importClause.namedBindings) { - if (importClause.namedBindings.kind === SyntaxKind.NamespaceImport) { - addDeclaration(importClause.namedBindings); - } - else { - forEach((importClause.namedBindings).elements, visit); - } - } - } - break; - - default: - forEachChild(node, visit); - } - } - } - } - /// Language Service // Information about a specific host file. @@ -1049,11 +47,6 @@ namespace ts { owners: string[]; } - interface VisibleModuleInfo { - moduleName: string; - moduleDir: string; - } - export interface DisplayPartsSymbolWriter extends SymbolWriter { displayParts(): SymbolDisplayPart[]; } @@ -1066,34 +59,6 @@ namespace ts { return ""; } - function isLocalVariableOrFunction(symbol: Symbol) { - if (symbol.parent) { - return false; // This is exported symbol - } - - return ts.forEach(symbol.declarations, declaration => { - // Function expressions are local - if (declaration.kind === SyntaxKind.FunctionExpression) { - return true; - } - - if (declaration.kind !== SyntaxKind.VariableDeclaration && declaration.kind !== SyntaxKind.FunctionDeclaration) { - return false; - } - - // If the parent is not sourceFile or module block it is local variable - for (let parent = declaration.parent; !isFunctionBlock(parent); parent = parent.parent) { - // Reached source file or module block - if (parent.kind === SyntaxKind.SourceFile || parent.kind === SyntaxKind.ModuleBlock) { - return false; - } - } - - // parent is in function block - return true; - }); - } - export function getDefaultCompilerOptions(): CompilerOptions { // Always default to "ScriptTarget.ES5" for the language service return { @@ -1250,16 +215,6 @@ namespace ts { sourceMapText?: string; } - /** - * Matches a triple slash reference directive with an incomplete string literal for its path. Used - * to determine if the caret is currently within the string literal and capture the literal fragment - * for completions. - * For example, this matches /// { - const comment = sourceText.substring(commentRange.pos, commentRange.end); - const referencePathMatchResult = getFileReferenceFromReferencePath(comment, commentRange); - if (referencePathMatchResult) { - isNoDefaultLib = referencePathMatchResult.isNoDefaultLib; - const fileReference = referencePathMatchResult.fileReference; - if (fileReference) { - const collection = referencePathMatchResult.isTypeReferenceDirective - ? typeReferenceDirectives - : referencedFiles; - - collection.push(fileReference); - } - } - }); - } - - function getFileReference() { - const file = scanner.getTokenValue(); - const pos = scanner.getTokenPos(); - return { - fileName: file, - pos: pos, - end: pos + file.length - }; - } - - function recordAmbientExternalModule(): void { - if (!ambientExternalModules) { - ambientExternalModules = []; - } - ambientExternalModules.push({ ref: getFileReference(), depth: braceNesting }); - } - - function recordModuleName() { - importedFiles.push(getFileReference()); - - markAsExternalModuleIfTopLevel(); - } - - function markAsExternalModuleIfTopLevel() { - if (braceNesting === 0) { - externalModule = true; - } - } - - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeDeclare(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.DeclareKeyword) { - // declare module "mod" - token = nextToken(); - if (token === SyntaxKind.ModuleKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - recordAmbientExternalModule(); - } - } - return true; - } - - return false; - } - - /** - * Returns true if at least one token was consumed from the stream - */ - function tryConsumeImport(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.ImportKeyword) { - - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import "mod"; - recordModuleName(); - return true; - } - else { - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import d from "mod"; - recordModuleName(); - return true; - } - } - else if (token === SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } - } - else if (token === SyntaxKind.CommaToken) { - // consume comma and keep going - token = nextToken(); - } - else { - // unknown syntax - return true; - } - } - - if (token === SyntaxKind.OpenBraceToken) { - token = nextToken(); - // consume "{ a as B, c, d as D}" clauses - // make sure that it stops on EOF - while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { - token = nextToken(); - } - - if (token === SyntaxKind.CloseBraceToken) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import {a as A} from "mod"; - // import d, {a, b as B} from "mod" - recordModuleName(); - } - } - } - } - else if (token === SyntaxKind.AsteriskToken) { - token = nextToken(); - if (token === SyntaxKind.AsKeyword) { - token = nextToken(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // import * as NS from "mod" - // import d, * as NS from "mod" - recordModuleName(); - } - } - } - } - } - } - - return true; - } - - return false; - } - - function tryConsumeExport(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.ExportKeyword) { - markAsExternalModuleIfTopLevel(); - token = nextToken(); - if (token === SyntaxKind.OpenBraceToken) { - token = nextToken(); - // consume "{ a as B, c, d as D}" clauses - // make sure it stops on EOF - while (token !== SyntaxKind.CloseBraceToken && token !== SyntaxKind.EndOfFileToken) { - token = nextToken(); - } - - if (token === SyntaxKind.CloseBraceToken) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // export {a as A} from "mod"; - // export {a, b as B} from "mod" - recordModuleName(); - } - } - } - } - else if (token === SyntaxKind.AsteriskToken) { - token = nextToken(); - if (token === SyntaxKind.FromKeyword) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // export * from "mod" - recordModuleName(); - } - } - } - else if (token === SyntaxKind.ImportKeyword) { - token = nextToken(); - if (token === SyntaxKind.Identifier || isKeyword(token)) { - token = nextToken(); - if (token === SyntaxKind.EqualsToken) { - if (tryConsumeRequireCall(/*skipCurrentToken*/ true)) { - return true; - } - } - } - } - - return true; - } - - return false; - } - - function tryConsumeRequireCall(skipCurrentToken: boolean): boolean { - let token = skipCurrentToken ? nextToken() : scanner.getToken(); - if (token === SyntaxKind.RequireKeyword) { - token = nextToken(); - if (token === SyntaxKind.OpenParenToken) { - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // require("mod"); - recordModuleName(); - } - } - return true; - } - return false; - } - - function tryConsumeDefine(): boolean { - let token = scanner.getToken(); - if (token === SyntaxKind.Identifier && scanner.getTokenValue() === "define") { - token = nextToken(); - if (token !== SyntaxKind.OpenParenToken) { - return true; - } - - token = nextToken(); - if (token === SyntaxKind.StringLiteral) { - // looks like define ("modname", ... - skip string literal and comma - token = nextToken(); - if (token === SyntaxKind.CommaToken) { - token = nextToken(); - } - else { - // unexpected token - return true; - } - } - - // should be start of dependency list - if (token !== SyntaxKind.OpenBracketToken) { - return true; - } - - // skip open bracket - token = nextToken(); - let i = 0; - // scan until ']' or EOF - while (token !== SyntaxKind.CloseBracketToken && token !== SyntaxKind.EndOfFileToken) { - // record string literals as module names - if (token === SyntaxKind.StringLiteral) { - recordModuleName(); - i++; - } - - token = nextToken(); - } - return true; - - } - return false; - } - - function processImports(): void { - scanner.setText(sourceText); - nextToken(); - // Look for: - // import "mod"; - // import d from "mod" - // import {a as A } from "mod"; - // import * as NS from "mod" - // import d, {a, b as B} from "mod" - // import i = require("mod"); - // - // export * from "mod" - // export {a as b} from "mod" - // export import i = require("mod") - // (for JavaScript files) require("mod") - - while (true) { - if (scanner.getToken() === SyntaxKind.EndOfFileToken) { - break; - } - - // check if at least one of alternative have moved scanner forward - if (tryConsumeDeclare() || - tryConsumeImport() || - tryConsumeExport() || - (detectJavaScriptImports && (tryConsumeRequireCall(/*skipCurrentToken*/ false) || tryConsumeDefine()))) { - continue; - } - else { - nextToken(); - } - } - - scanner.setText(undefined); - } - - if (readImportFiles) { - processImports(); - } - processTripleSlashDirectives(); - if (externalModule) { - // for external modules module all nested ambient modules are augmentations - if (ambientExternalModules) { - // move all detected ambient modules to imported files since they need to be resolved - for (const decl of ambientExternalModules) { - importedFiles.push(decl.ref); - } - } - return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: undefined }; - } - else { - // for global scripts ambient modules still can have augmentations - look for ambient modules with depth > 0 - let ambientModuleNames: string[]; - if (ambientExternalModules) { - for (const decl of ambientExternalModules) { - if (decl.depth === 0) { - if (!ambientModuleNames) { - ambientModuleNames = []; - } - ambientModuleNames.push(decl.ref.fileName); - } - else { - importedFiles.push(decl.ref); - } - } - } - return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: ambientModuleNames }; - } - } - - /// Helpers - function getTargetLabel(referenceNode: Node, labelName: string): Identifier { - while (referenceNode) { - if (referenceNode.kind === SyntaxKind.LabeledStatement && (referenceNode).label.text === labelName) { - return (referenceNode).label; - } - referenceNode = referenceNode.parent; - } - return undefined; - } - - function isJumpStatementTarget(node: Node): boolean { - return node.kind === SyntaxKind.Identifier && - (node.parent.kind === SyntaxKind.BreakStatement || node.parent.kind === SyntaxKind.ContinueStatement) && - (node.parent).label === node; - } - - function isLabelOfLabeledStatement(node: Node): boolean { - return node.kind === SyntaxKind.Identifier && - node.parent.kind === SyntaxKind.LabeledStatement && - (node.parent).label === node; - } - - /** - * Whether or not a 'node' is preceded by a label of the given string. - * Note: 'node' cannot be a SourceFile. - */ - function isLabeledBy(node: Node, labelName: string) { - for (let owner = node.parent; owner.kind === SyntaxKind.LabeledStatement; owner = owner.parent) { - if ((owner).label.text === labelName) { - return true; - } - } - - return false; - } - - function isLabelName(node: Node): boolean { - return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node); - } - - function isRightSideOfQualifiedName(node: Node) { - return node.parent.kind === SyntaxKind.QualifiedName && (node.parent).right === node; - } - - function isRightSideOfPropertyAccess(node: Node) { - return node && node.parent && node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).name === node; - } - - function climbPastPropertyAccess(node: Node) { - return isRightSideOfPropertyAccess(node) ? node.parent : node; - } - - /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ - function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { - return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); - } - - function isCallExpressionTarget(node: Node): boolean { - return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpression); - } - - function isNewExpressionTarget(node: Node): boolean { - return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpression); - } - - function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) { - const target = climbPastPropertyAccess(node); - return target && target.parent && target.parent.kind === kind && (target.parent).expression === target; - } - - function climbPastManyPropertyAccesses(node: Node): Node { - return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; - } - - /** Returns a CallLikeExpression where `node` is the target being invoked. */ - function getAncestorCallLikeExpression(node: Node): CallLikeExpression | undefined { - const target = climbPastManyPropertyAccesses(node); - const callLike = target.parent; - return callLike && isCallLikeExpression(callLike) && getInvokedExpression(callLike) === target && callLike; - } - - function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { - const callLike = getAncestorCallLikeExpression(node); - return callLike && typeChecker.getResolvedSignature(callLike).declaration; - } - - function isNameOfModuleDeclaration(node: Node) { - return node.parent.kind === SyntaxKind.ModuleDeclaration && (node.parent).name === node; - } - - function isNameOfFunctionDeclaration(node: Node): boolean { - return node.kind === SyntaxKind.Identifier && - isFunctionLike(node.parent) && (node.parent).name === node; - } - - function isObjectLiteralPropertyDeclaration(node: Node): node is ObjectLiteralElement { - switch (node.kind) { - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return true; - } - return false; - } - - /** - * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } - */ - function getContainingObjectLiteralElement(node: Node): ObjectLiteralElement { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - if (node.parent.kind === SyntaxKind.ComputedPropertyName) { - return isObjectLiteralPropertyDeclaration(node.parent.parent) ? node.parent.parent : undefined; - } - // intential fall through - case SyntaxKind.Identifier: - return isObjectLiteralPropertyDeclaration(node.parent) && node.parent.name === node ? node.parent : undefined; - } - return undefined; - } - - function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: Node): boolean { - if (node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NumericLiteral) { - switch (node.parent.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.PropertyAssignment: - case SyntaxKind.EnumMember: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.ModuleDeclaration: - return (node.parent).name === node; - case SyntaxKind.ElementAccessExpression: - return (node.parent).argumentExpression === node; - case SyntaxKind.ComputedPropertyName: - return true; - } - } - - return false; - } - - function isNameOfExternalModuleImportOrDeclaration(node: Node): boolean { - if (node.kind === SyntaxKind.StringLiteral) { - return isNameOfModuleDeclaration(node) || isExpressionOfExternalModuleImportEqualsDeclaration(node); - } - - return false; - } - - function isExpressionOfExternalModuleImportEqualsDeclaration(node: Node) { - return isExternalModuleImportEqualsDeclaration(node.parent.parent) && - getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node; - } - - /** Returns true if the position is within a comment */ - function isInsideComment(sourceFile: SourceFile, token: Node, position: number): boolean { - // The position has to be: 1. in the leading trivia (before token.getStart()), and 2. within a comment - return position <= token.getStart(sourceFile) && - (isInsideCommentRange(getTrailingCommentRanges(sourceFile.text, token.getFullStart())) || - isInsideCommentRange(getLeadingCommentRanges(sourceFile.text, token.getFullStart()))); - - function isInsideCommentRange(comments: CommentRange[]): boolean { - return forEach(comments, comment => { - // either we are 1. completely inside the comment, or 2. at the end of the comment - if (comment.pos < position && position < comment.end) { - return true; - } - else if (position === comment.end) { - const text = sourceFile.text; - const width = comment.end - comment.pos; - // is single line comment or just /* - if (width <= 2 || text.charCodeAt(comment.pos + 1) === CharacterCodes.slash) { - return true; - } - else { - // is unterminated multi-line comment - return !(text.charCodeAt(comment.end - 1) === CharacterCodes.slash && - text.charCodeAt(comment.end - 2) === CharacterCodes.asterisk); - } - } - return false; - }); - } - } - - const enum SemanticMeaning { - None = 0x0, - Value = 0x1, - Type = 0x2, - Namespace = 0x4, - All = Value | Type | Namespace - } - - const enum BreakContinueSearchType { - None = 0x0, - Unlabeled = 0x1, - Labeled = 0x2, - All = Unlabeled | Labeled - } - - // A cache of completion entries for keywords, these do not change between sessions - const keywordCompletions: CompletionEntry[] = []; - for (let i = SyntaxKind.FirstKeyword; i <= SyntaxKind.LastKeyword; i++) { - keywordCompletions.push({ - name: tokenToString(i), - kind: ScriptElementKind.keyword, - kindModifiers: ScriptElementKindModifier.none, - sortText: "0" - }); - } - - /* @internal */ export function getContainerNode(node: Node): Declaration { - while (true) { - node = node.parent; - if (!node) { - return undefined; - } - switch (node.kind) { - case SyntaxKind.SourceFile: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.EnumDeclaration: - case SyntaxKind.ModuleDeclaration: - return node; - } - } - } - - /* @internal */ export function getNodeKind(node: Node): string { - switch (node.kind) { - case SyntaxKind.SourceFile: - return isExternalModule(node) ? ScriptElementKind.moduleElement : ScriptElementKind.scriptElement; - case SyntaxKind.ModuleDeclaration: - return ScriptElementKind.moduleElement; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - return ScriptElementKind.classElement; - case SyntaxKind.InterfaceDeclaration: return ScriptElementKind.interfaceElement; - case SyntaxKind.TypeAliasDeclaration: return ScriptElementKind.typeElement; - case SyntaxKind.EnumDeclaration: return ScriptElementKind.enumElement; - case SyntaxKind.VariableDeclaration: - return getKindOfVariableDeclaration(node); - case SyntaxKind.BindingElement: - return getKindOfVariableDeclaration(getRootDeclaration(node)); - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - return ScriptElementKind.functionElement; - case SyntaxKind.GetAccessor: return ScriptElementKind.memberGetAccessorElement; - case SyntaxKind.SetAccessor: return ScriptElementKind.memberSetAccessorElement; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - return ScriptElementKind.memberFunctionElement; - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - return ScriptElementKind.memberVariableElement; - case SyntaxKind.IndexSignature: return ScriptElementKind.indexSignatureElement; - case SyntaxKind.ConstructSignature: return ScriptElementKind.constructSignatureElement; - case SyntaxKind.CallSignature: return ScriptElementKind.callSignatureElement; - case SyntaxKind.Constructor: return ScriptElementKind.constructorImplementationElement; - case SyntaxKind.TypeParameter: return ScriptElementKind.typeParameterElement; - case SyntaxKind.EnumMember: return ScriptElementKind.enumMemberElement; - case SyntaxKind.Parameter: return hasModifier(node, ModifierFlags.ParameterPropertyModifier) ? ScriptElementKind.memberVariableElement : ScriptElementKind.parameterElement; - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportClause: - case SyntaxKind.ExportSpecifier: - case SyntaxKind.NamespaceImport: - return ScriptElementKind.alias; - case SyntaxKind.JSDocTypedefTag: - return ScriptElementKind.typeElement; - default: - return ScriptElementKind.unknown; - } - - function getKindOfVariableDeclaration(v: VariableDeclaration): string { - return isConst(v) - ? ScriptElementKind.constElement - : isLet(v) - ? ScriptElementKind.letElement - : ScriptElementKind.variableElement; - } - } - class CancellationTokenObject implements CancellationToken { constructor(private cancellationToken: HostCancellationToken) { } @@ -2563,2147 +863,14 @@ namespace ts { program.getGlobalDiagnostics(cancellationToken)); } - /** - * Get the name to be display in completion from a given symbol. - * - * @return undefined if the name is of external module otherwise a name with striped of any quote - */ - function getCompletionEntryDisplayNameForSymbol(symbol: Symbol, target: ScriptTarget, performCharacterChecks: boolean, location: Node): string { - const displayName: string = getDeclaredName(program.getTypeChecker(), symbol, location); - - if (displayName) { - const firstCharCode = displayName.charCodeAt(0); - // First check of the displayName is not external module; if it is an external module, it is not valid entry - if ((symbol.flags & SymbolFlags.Namespace) && (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote)) { - // If the symbol is external module, don't show it in the completion list - // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) - return undefined; - } - } - - return getCompletionEntryDisplayName(displayName, target, performCharacterChecks); - } - - /** - * Get a displayName from a given for completion list, performing any necessary quotes stripping - * and checking whether the name is valid identifier name. - */ - function getCompletionEntryDisplayName(name: string, target: ScriptTarget, performCharacterChecks: boolean): string { - if (!name) { - return undefined; - } - - name = stripQuotes(name); - - if (!name) { - return undefined; - } - - // If the user entered name for the symbol was quoted, removing the quotes is not enough, as the name could be an - // invalid identifier name. We need to check if whatever was inside the quotes is actually a valid identifier name. - // e.g "b a" is valid quoted name but when we strip off the quotes, it is invalid. - // We, thus, need to check if whatever was inside the quotes is actually a valid identifier name. - if (performCharacterChecks) { - if (!isIdentifierText(name, target)) { - return undefined; - } - } - - return name; - } - - function getCompletionData(fileName: string, position: number) { - const typeChecker = program.getTypeChecker(); - const sourceFile = getValidSourceFile(fileName); - const isJavaScriptFile = isSourceFileJavaScript(sourceFile); - - let isJsDocTagName = false; - - let start = timestamp(); - const currentToken = getTokenAtPosition(sourceFile, position); - log("getCompletionData: Get current token: " + (timestamp() - start)); - - start = timestamp(); - // Completion not allowed inside comments, bail out if this is the case - const insideComment = isInsideComment(sourceFile, currentToken, position); - log("getCompletionData: Is inside comment: " + (timestamp() - start)); - - if (insideComment) { - // The current position is next to the '@' sign, when no tag name being provided yet. - // Provide a full list of tag names - if (hasDocComment(sourceFile, position) && sourceFile.text.charCodeAt(position - 1) === CharacterCodes.at) { - isJsDocTagName = true; - } - - // Completion should work inside certain JsDoc tags. For example: - // /** @type {number | string} */ - // Completion should work in the brackets - let insideJsDocTagExpression = false; - const tag = getJsDocTagAtPosition(sourceFile, position); - if (tag) { - if (tag.tagName.pos <= position && position <= tag.tagName.end) { - isJsDocTagName = true; - } - - switch (tag.kind) { - case SyntaxKind.JSDocTypeTag: - case SyntaxKind.JSDocParameterTag: - case SyntaxKind.JSDocReturnTag: - const tagWithExpression = tag; - if (tagWithExpression.typeExpression) { - insideJsDocTagExpression = tagWithExpression.typeExpression.pos < position && position < tagWithExpression.typeExpression.end; - } - break; - } - } - - if (isJsDocTagName) { - return { symbols: undefined, isMemberCompletion: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, isJsDocTagName }; - } - - if (!insideJsDocTagExpression) { - // Proceed if the current position is in jsDoc tag expression; otherwise it is a normal - // comment or the plain text part of a jsDoc comment, so no completion should be available - log("Returning an empty list because completion was inside a regular comment or plain text part of a JsDoc comment."); - return undefined; - } - } - - start = timestamp(); - const previousToken = findPrecedingToken(position, sourceFile); - log("getCompletionData: Get previous token 1: " + (timestamp() - start)); - - // The decision to provide completion depends on the contextToken, which is determined through the previousToken. - // Note: 'previousToken' (and thus 'contextToken') can be undefined if we are the beginning of the file - let contextToken = previousToken; - - // Check if the caret is at the end of an identifier; this is a partial identifier that we want to complete: e.g. a.toS| - // Skip this partial identifier and adjust the contextToken to the token that precedes it. - if (contextToken && position <= contextToken.end && isWord(contextToken.kind)) { - const start = timestamp(); - contextToken = findPrecedingToken(contextToken.getFullStart(), sourceFile); - log("getCompletionData: Get previous token 2: " + (timestamp() - start)); - } - - // Find the node where completion is requested on. - // Also determine whether we are trying to complete with members of that node - // or attributes of a JSX tag. - let node = currentToken; - let isRightOfDot = false; - let isRightOfOpenTag = false; - let isStartingCloseTag = false; - - let location = getTouchingPropertyName(sourceFile, position); - if (contextToken) { - // Bail out if this is a known invalid completion location - if (isCompletionListBlocker(contextToken)) { - log("Returning an empty list because completion was requested in an invalid position."); - return undefined; - } - - const { parent, kind } = contextToken; - if (kind === SyntaxKind.DotToken) { - if (parent.kind === SyntaxKind.PropertyAccessExpression) { - node = (contextToken.parent).expression; - isRightOfDot = true; - } - else if (parent.kind === SyntaxKind.QualifiedName) { - node = (contextToken.parent).left; - isRightOfDot = true; - } - else { - // There is nothing that precedes the dot, so this likely just a stray character - // or leading into a '...' token. Just bail out instead. - return undefined; - } - } - else if (sourceFile.languageVariant === LanguageVariant.JSX) { - if (kind === SyntaxKind.LessThanToken) { - isRightOfOpenTag = true; - location = contextToken; - } - else if (kind === SyntaxKind.SlashToken && contextToken.parent.kind === SyntaxKind.JsxClosingElement) { - isStartingCloseTag = true; - location = contextToken; - } - } - } - - const semanticStart = timestamp(); - let isMemberCompletion: boolean; - let isNewIdentifierLocation: boolean; - let symbols: Symbol[] = []; - - if (isRightOfDot) { - getTypeScriptMemberSymbols(); - } - else if (isRightOfOpenTag) { - const tagSymbols = typeChecker.getJsxIntrinsicTagNames(); - if (tryGetGlobalSymbols()) { - symbols = tagSymbols.concat(symbols.filter(s => !!(s.flags & (SymbolFlags.Value | SymbolFlags.Alias)))); - } - else { - symbols = tagSymbols; - } - isMemberCompletion = true; - isNewIdentifierLocation = false; - } - else if (isStartingCloseTag) { - const tagName = (contextToken.parent.parent).openingElement.tagName; - const tagSymbol = typeChecker.getSymbolAtLocation(tagName); - - if (!typeChecker.isUnknownSymbol(tagSymbol)) { - symbols = [tagSymbol]; - } - isMemberCompletion = true; - isNewIdentifierLocation = false; - } - else { - // For JavaScript or TypeScript, if we're not after a dot, then just try to get the - // global symbols in scope. These results should be valid for either language as - // the set of symbols that can be referenced from this location. - if (!tryGetGlobalSymbols()) { - return undefined; - } - } - - log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - - return { symbols, isMemberCompletion, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), isJsDocTagName }; - - function getTypeScriptMemberSymbols(): void { - // Right of dot member completion list - isMemberCompletion = true; - isNewIdentifierLocation = false; - - if (node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.QualifiedName || node.kind === SyntaxKind.PropertyAccessExpression) { - let symbol = typeChecker.getSymbolAtLocation(node); - - // This is an alias, follow what it aliases - if (symbol && symbol.flags & SymbolFlags.Alias) { - symbol = typeChecker.getAliasedSymbol(symbol); - } - - if (symbol && symbol.flags & SymbolFlags.HasExports) { - // Extract module or enum members - const exportedSymbols = typeChecker.getExportsOfModule(symbol); - forEach(exportedSymbols, symbol => { - if (typeChecker.isValidPropertyAccess((node.parent), symbol.name)) { - symbols.push(symbol); - } - }); - } - } - - const type = typeChecker.getTypeAtLocation(node); - addTypeProperties(type); - } - - function addTypeProperties(type: Type) { - if (type) { - // Filter private properties - for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccess((node.parent), symbol.name)) { - symbols.push(symbol); - } - } - - if (isJavaScriptFile && type.flags & TypeFlags.Union) { - // In javascript files, for union types, we don't just get the members that - // the individual types have in common, we also include all the members that - // each individual type has. This is because we're going to add all identifiers - // anyways. So we might as well elevate the members that were at least part - // of the individual types to a higher status since we know what they are. - const unionType = type; - for (const elementType of unionType.types) { - addTypeProperties(elementType); - } - } - } - } - - function tryGetGlobalSymbols(): boolean { - let objectLikeContainer: ObjectLiteralExpression | BindingPattern; - let namedImportsOrExports: NamedImportsOrExports; - let jsxContainer: JsxOpeningLikeElement; - - if (objectLikeContainer = tryGetObjectLikeCompletionContainer(contextToken)) { - return tryGetObjectLikeCompletionSymbols(objectLikeContainer); - } - - if (namedImportsOrExports = tryGetNamedImportsOrExportsForCompletion(contextToken)) { - // cursor is in an import clause - // try to show exported member for imported module - return tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports); - } - - if (jsxContainer = tryGetContainingJsxElement(contextToken)) { - let attrsType: Type; - if ((jsxContainer.kind === SyntaxKind.JsxSelfClosingElement) || (jsxContainer.kind === SyntaxKind.JsxOpeningElement)) { - // Cursor is inside a JSX self-closing element or opening element - attrsType = typeChecker.getJsxElementAttributesType(jsxContainer); - - if (attrsType) { - symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (jsxContainer).attributes); - isMemberCompletion = true; - isNewIdentifierLocation = false; - return true; - } - - } - } - - // Get all entities in the current scope. - isMemberCompletion = false; - isNewIdentifierLocation = isNewIdentifierDefinitionLocation(contextToken); - - if (previousToken !== contextToken) { - Debug.assert(!!previousToken, "Expected 'contextToken' to be defined when different from 'previousToken'."); - } - // We need to find the node that will give us an appropriate scope to begin - // aggregating completion candidates. This is achieved in 'getScopeNode' - // by finding the first node that encompasses a position, accounting for whether a node - // is "complete" to decide whether a position belongs to the node. - // - // However, at the end of an identifier, we are interested in the scope of the identifier - // itself, but fall outside of the identifier. For instance: - // - // xyz => x$ - // - // the cursor is outside of both the 'x' and the arrow function 'xyz => x', - // so 'xyz' is not returned in our results. - // - // We define 'adjustedPosition' so that we may appropriately account for - // being at the end of an identifier. The intention is that if requesting completion - // at the end of an identifier, it should be effectively equivalent to requesting completion - // anywhere inside/at the beginning of the identifier. So in the previous case, the - // 'adjustedPosition' will work as if requesting completion in the following: - // - // xyz => $x - // - // If previousToken !== contextToken, then - // - 'contextToken' was adjusted to the token prior to 'previousToken' - // because we were at the end of an identifier. - // - 'previousToken' is defined. - const adjustedPosition = previousToken !== contextToken ? - previousToken.getStart() : - position; - - const scopeNode = getScopeNode(contextToken, adjustedPosition, sourceFile) || sourceFile; - - /// TODO filter meaning based on the current context - const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias; - symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings); - - return true; - } - - /** - * Finds the first node that "embraces" the position, so that one may - * accurately aggregate locals from the closest containing scope. - */ - function getScopeNode(initialToken: Node, position: number, sourceFile: SourceFile) { - let scope = initialToken; - while (scope && !positionBelongsToNode(scope, position, sourceFile)) { - scope = scope.parent; - } - return scope; - } - - function isCompletionListBlocker(contextToken: Node): boolean { - const start = timestamp(); - const result = isInStringOrRegularExpressionOrTemplateLiteral(contextToken) || - isSolelyIdentifierDefinitionLocation(contextToken) || - isDotOfNumericLiteral(contextToken) || - isInJsxText(contextToken); - log("getCompletionsAtPosition: isCompletionListBlocker: " + (timestamp() - start)); - return result; - } - - function isInJsxText(contextToken: Node): boolean { - if (contextToken.kind === SyntaxKind.JsxText) { - return true; - } - - if (contextToken.kind === SyntaxKind.GreaterThanToken && contextToken.parent) { - if (contextToken.parent.kind === SyntaxKind.JsxOpeningElement) { - return true; - } - - if (contextToken.parent.kind === SyntaxKind.JsxClosingElement || contextToken.parent.kind === SyntaxKind.JsxSelfClosingElement) { - return contextToken.parent.parent && contextToken.parent.parent.kind === SyntaxKind.JsxElement; - } - } - return false; - } - - function isNewIdentifierDefinitionLocation(previousToken: Node): boolean { - if (previousToken) { - const containingNodeKind = previousToken.parent.kind; - switch (previousToken.kind) { - case SyntaxKind.CommaToken: - return containingNodeKind === SyntaxKind.CallExpression // func( a, | - || containingNodeKind === SyntaxKind.Constructor // constructor( a, | /* public, protected, private keywords are allowed here, so show completion */ - || containingNodeKind === SyntaxKind.NewExpression // new C(a, | - || containingNodeKind === SyntaxKind.ArrayLiteralExpression // [a, | - || containingNodeKind === SyntaxKind.BinaryExpression // const x = (a, | - || containingNodeKind === SyntaxKind.FunctionType; // var x: (s: string, list| - - case SyntaxKind.OpenParenToken: - return containingNodeKind === SyntaxKind.CallExpression // func( | - || containingNodeKind === SyntaxKind.Constructor // constructor( | - || containingNodeKind === SyntaxKind.NewExpression // new C(a| - || containingNodeKind === SyntaxKind.ParenthesizedExpression // const x = (a| - || containingNodeKind === SyntaxKind.ParenthesizedType; // function F(pred: (a| /* this can become an arrow function, where 'a' is the argument */ - - case SyntaxKind.OpenBracketToken: - return containingNodeKind === SyntaxKind.ArrayLiteralExpression // [ | - || containingNodeKind === SyntaxKind.IndexSignature // [ | : string ] - || containingNodeKind === SyntaxKind.ComputedPropertyName; // [ | /* this can become an index signature */ - - case SyntaxKind.ModuleKeyword: // module | - case SyntaxKind.NamespaceKeyword: // namespace | - return true; - - case SyntaxKind.DotToken: - return containingNodeKind === SyntaxKind.ModuleDeclaration; // module A.| - - case SyntaxKind.OpenBraceToken: - return containingNodeKind === SyntaxKind.ClassDeclaration; // class A{ | - - case SyntaxKind.EqualsToken: - return containingNodeKind === SyntaxKind.VariableDeclaration // const x = a| - || containingNodeKind === SyntaxKind.BinaryExpression; // x = a| - - case SyntaxKind.TemplateHead: - return containingNodeKind === SyntaxKind.TemplateExpression; // `aa ${| - - case SyntaxKind.TemplateMiddle: - return containingNodeKind === SyntaxKind.TemplateSpan; // `aa ${10} dd ${| - - case SyntaxKind.PublicKeyword: - case SyntaxKind.PrivateKeyword: - case SyntaxKind.ProtectedKeyword: - return containingNodeKind === SyntaxKind.PropertyDeclaration; // class A{ public | - } - - // Previous token may have been a keyword that was converted to an identifier. - switch (previousToken.getText()) { - case "public": - case "protected": - case "private": - return true; - } - } - - return false; - } - - function isInStringOrRegularExpressionOrTemplateLiteral(contextToken: Node): boolean { - if (contextToken.kind === SyntaxKind.StringLiteral - || contextToken.kind === SyntaxKind.RegularExpressionLiteral - || isTemplateLiteralKind(contextToken.kind)) { - const start = contextToken.getStart(); - const end = contextToken.getEnd(); - - // To be "in" one of these literals, the position has to be: - // 1. entirely within the token text. - // 2. at the end position of an unterminated token. - // 3. at the end of a regular expression (due to trailing flags like '/foo/g'). - if (start < position && position < end) { - return true; - } - - if (position === end) { - return !!(contextToken).isUnterminated - || contextToken.kind === SyntaxKind.RegularExpressionLiteral; - } - } - - return false; - } - - /** - * Aggregates relevant symbols for completion in object literals and object binding patterns. - * Relevant symbols are stored in the captured 'symbols' variable. - * - * @returns true if 'symbols' was successfully populated; false otherwise. - */ - function tryGetObjectLikeCompletionSymbols(objectLikeContainer: ObjectLiteralExpression | BindingPattern): boolean { - // We're looking up possible property names from contextual/inferred/declared type. - isMemberCompletion = true; - - let typeForObject: Type; - let existingMembers: Declaration[]; - - if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { - // We are completing on contextual types, but may also include properties - // other than those within the declared type. - isNewIdentifierLocation = true; - - // If the object literal is being assigned to something of type 'null | { hello: string }', - // it clearly isn't trying to satisfy the 'null' type. So we grab the non-nullable type if possible. - typeForObject = typeChecker.getContextualType(objectLikeContainer); - typeForObject = typeForObject && typeForObject.getNonNullableType(); - - existingMembers = (objectLikeContainer).properties; - } - else if (objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern) { - // We are *only* completing on properties from the type being destructured. - isNewIdentifierLocation = false; - - const rootDeclaration = getRootDeclaration(objectLikeContainer.parent); - if (isVariableLike(rootDeclaration)) { - // We don't want to complete using the type acquired by the shape - // of the binding pattern; we are only interested in types acquired - // through type declaration or inference. - // Also proceed if rootDeclaration is a parameter and if its containing function expression/arrow function is contextually typed - - // type of parameter will flow in from the contextual type of the function - let canGetType = !!(rootDeclaration.initializer || rootDeclaration.type); - if (!canGetType && rootDeclaration.kind === SyntaxKind.Parameter) { - if (isExpression(rootDeclaration.parent)) { - canGetType = !!typeChecker.getContextualType(rootDeclaration.parent); - } - else if (rootDeclaration.parent.kind === SyntaxKind.MethodDeclaration || rootDeclaration.parent.kind === SyntaxKind.SetAccessor) { - canGetType = isExpression(rootDeclaration.parent.parent) && !!typeChecker.getContextualType(rootDeclaration.parent.parent); - } - } - if (canGetType) { - typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); - existingMembers = (objectLikeContainer).elements; - } - } - else { - Debug.fail("Root declaration is not variable-like."); - } - } - else { - Debug.fail("Expected object literal or binding pattern, got " + objectLikeContainer.kind); - } - - if (!typeForObject) { - return false; - } - - const typeMembers = typeChecker.getPropertiesOfType(typeForObject); - if (typeMembers && typeMembers.length > 0) { - // Add filtered items to the completion list - symbols = filterObjectMembersList(typeMembers, existingMembers); - } - return true; - } - - /** - * Aggregates relevant symbols for completion in import clauses and export clauses - * whose declarations have a module specifier; for instance, symbols will be aggregated for - * - * import { | } from "moduleName"; - * export { a as foo, | } from "moduleName"; - * - * but not for - * - * export { | }; - * - * Relevant symbols are stored in the captured 'symbols' variable. - * - * @returns true if 'symbols' was successfully populated; false otherwise. - */ - function tryGetImportOrExportClauseCompletionSymbols(namedImportsOrExports: NamedImportsOrExports): boolean { - const declarationKind = namedImportsOrExports.kind === SyntaxKind.NamedImports ? - SyntaxKind.ImportDeclaration : - SyntaxKind.ExportDeclaration; - const importOrExportDeclaration = getAncestor(namedImportsOrExports, declarationKind); - const moduleSpecifier = importOrExportDeclaration.moduleSpecifier; - - if (!moduleSpecifier) { - return false; - } - - isMemberCompletion = true; - isNewIdentifierLocation = false; - - let exports: Symbol[]; - const moduleSpecifierSymbol = typeChecker.getSymbolAtLocation(importOrExportDeclaration.moduleSpecifier); - if (moduleSpecifierSymbol) { - exports = typeChecker.getExportsOfModule(moduleSpecifierSymbol); - } - - symbols = exports ? filterNamedImportOrExportCompletionItems(exports, namedImportsOrExports.elements) : emptyArray; - - return true; - } - - /** - * Returns the immediate owning object literal or binding pattern of a context token, - * on the condition that one exists and that the context implies completion should be given. - */ - function tryGetObjectLikeCompletionContainer(contextToken: Node): ObjectLiteralExpression | BindingPattern { - if (contextToken) { - switch (contextToken.kind) { - case SyntaxKind.OpenBraceToken: // const x = { | - case SyntaxKind.CommaToken: // const x = { a: 0, | - const parent = contextToken.parent; - if (parent && (parent.kind === SyntaxKind.ObjectLiteralExpression || parent.kind === SyntaxKind.ObjectBindingPattern)) { - return parent; - } - break; - } - } - - return undefined; - } - - /** - * Returns the containing list of named imports or exports of a context token, - * on the condition that one exists and that the context implies completion should be given. - */ - function tryGetNamedImportsOrExportsForCompletion(contextToken: Node): NamedImportsOrExports { - if (contextToken) { - switch (contextToken.kind) { - case SyntaxKind.OpenBraceToken: // import { | - case SyntaxKind.CommaToken: // import { a as 0, | - switch (contextToken.parent.kind) { - case SyntaxKind.NamedImports: - case SyntaxKind.NamedExports: - return contextToken.parent; - } - } - } - - return undefined; - } - - function tryGetContainingJsxElement(contextToken: Node): JsxOpeningLikeElement { - if (contextToken) { - const parent = contextToken.parent; - switch (contextToken.kind) { - case SyntaxKind.LessThanSlashToken: - case SyntaxKind.SlashToken: - case SyntaxKind.Identifier: - case SyntaxKind.JsxAttribute: - case SyntaxKind.JsxSpreadAttribute: - if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) { - return parent; - } - else if (parent.kind === SyntaxKind.JsxAttribute) { - return parent.parent; - } - break; - - // The context token is the closing } or " of an attribute, which means - // its parent is a JsxExpression, whose parent is a JsxAttribute, - // whose parent is a JsxOpeningLikeElement - case SyntaxKind.StringLiteral: - if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) { - return parent.parent; - } - - break; - - case SyntaxKind.CloseBraceToken: - if (parent && - parent.kind === SyntaxKind.JsxExpression && - parent.parent && - (parent.parent.kind === SyntaxKind.JsxAttribute)) { - return parent.parent.parent; - } - - if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) { - return parent.parent; - } - - break; - } - } - return undefined; - } - - function isFunction(kind: SyntaxKind): boolean { - switch (kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.CallSignature: - case SyntaxKind.ConstructSignature: - case SyntaxKind.IndexSignature: - return true; - } - return false; - } - - /** - * @returns true if we are certain that the currently edited location must define a new location; false otherwise. - */ - function isSolelyIdentifierDefinitionLocation(contextToken: Node): boolean { - const containingNodeKind = contextToken.parent.kind; - switch (contextToken.kind) { - case SyntaxKind.CommaToken: - return containingNodeKind === SyntaxKind.VariableDeclaration || - containingNodeKind === SyntaxKind.VariableDeclarationList || - containingNodeKind === SyntaxKind.VariableStatement || - containingNodeKind === SyntaxKind.EnumDeclaration || // enum a { foo, | - isFunction(containingNodeKind) || - containingNodeKind === SyntaxKind.ClassDeclaration || // class A(); - - for (const element of namedImportsOrExports) { - // If this is the current item we are editing right now, do not filter it out - if (element.getStart() <= position && position <= element.getEnd()) { - continue; - } - - const name = element.propertyName || element.name; - existingImportsOrExports[name.text] = true; - } - - if (!someProperties(existingImportsOrExports)) { - return filter(exportsOfModule, e => e.name !== "default"); - } - - return filter(exportsOfModule, e => e.name !== "default" && !existingImportsOrExports[e.name]); - } - - /** - * Filters out completion suggestions for named imports or exports. - * - * @returns Symbols to be suggested in an object binding pattern or object literal expression, barring those whose declarations - * do not occur at the current position and have not otherwise been typed. - */ - function filterObjectMembersList(contextualMemberSymbols: Symbol[], existingMembers: Declaration[]): Symbol[] { - if (!existingMembers || existingMembers.length === 0) { - return contextualMemberSymbols; - } - - const existingMemberNames = createMap(); - for (const m of existingMembers) { - // Ignore omitted expressions for missing members - if (m.kind !== SyntaxKind.PropertyAssignment && - m.kind !== SyntaxKind.ShorthandPropertyAssignment && - m.kind !== SyntaxKind.BindingElement && - m.kind !== SyntaxKind.MethodDeclaration) { - continue; - } - - // If this is the current item we are editing right now, do not filter it out - if (m.getStart() <= position && position <= m.getEnd()) { - continue; - } - - let existingName: string; - - if (m.kind === SyntaxKind.BindingElement && (m).propertyName) { - // include only identifiers in completion list - if ((m).propertyName.kind === SyntaxKind.Identifier) { - existingName = ((m).propertyName).text; - } - } - else { - // TODO(jfreeman): Account for computed property name - // NOTE: if one only performs this step when m.name is an identifier, - // things like '__proto__' are not filtered out. - existingName = (m.name).text; - } - - existingMemberNames[existingName] = true; - } - - return filter(contextualMemberSymbols, m => !existingMemberNames[m.name]); - } - - /** - * Filters out completion suggestions from 'symbols' according to existing JSX attributes. - * - * @returns Symbols to be suggested in a JSX element, barring those whose attributes - * do not occur at the current position and have not otherwise been typed. - */ - function filterJsxAttributes(symbols: Symbol[], attributes: NodeArray): Symbol[] { - const seenNames = createMap(); - for (const attr of attributes) { - // If this is the current item we are editing right now, do not filter it out - if (attr.getStart() <= position && position <= attr.getEnd()) { - continue; - } - - if (attr.kind === SyntaxKind.JsxAttribute) { - seenNames[(attr).name.text] = true; - } - } - - return filter(symbols, a => !seenNames[a.name]); - } - } - function getCompletionsAtPosition(fileName: string, position: number): CompletionInfo { synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - - if (isInReferenceComment(sourceFile, position)) { - return getTripleSlashReferenceCompletion(sourceFile, position); - } - - if (isInString(sourceFile, position)) { - return getStringLiteralCompletionEntries(sourceFile, position); - } - - const completionData = getCompletionData(fileName, position); - if (!completionData) { - return undefined; - } - - const { symbols, isMemberCompletion, isNewIdentifierLocation, location, isJsDocTagName } = completionData; - - if (isJsDocTagName) { - // If the current position is a jsDoc tag name, only tag names should be provided for completion - return { isMemberCompletion: false, isNewIdentifierLocation: false, entries: getAllJsDocCompletionEntries() }; - } - - const entries: CompletionEntry[] = []; - - if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ false); - addRange(entries, getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames)); - } - else { - if (!symbols || symbols.length === 0) { - if (sourceFile.languageVariant === LanguageVariant.JSX && - location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) { - // In the TypeScript JSX element, if such element is not defined. When users query for completion at closing tag, - // instead of simply giving unknown value, the completion will return the tag-name of an associated opening-element. - // For example: - // var x =
completion list at "1" will contain "div" with type any - const tagName = (location.parent.parent).openingElement.tagName; - entries.push({ - name: (tagName).text, - kind: undefined, - kindModifiers: undefined, - sortText: "0", - }); - } - else { - return undefined; - } - } - - getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true); - } - - // Add keywords if this is not a member completion list - if (!isMemberCompletion && !isJsDocTagName) { - addRange(entries, keywordCompletions); - } - - return { isMemberCompletion, isNewIdentifierLocation: isNewIdentifierLocation || isSourceFileJavaScript(sourceFile), entries }; - - function getJavaScriptCompletionEntries(sourceFile: SourceFile, position: number, uniqueNames: Map): CompletionEntry[] { - const entries: CompletionEntry[] = []; - const target = program.getCompilerOptions().target; - - const nameTable = getNameTable(sourceFile); - for (const name in nameTable) { - // Skip identifiers produced only from the current location - if (nameTable[name] === position) { - continue; - } - - if (!uniqueNames[name]) { - uniqueNames[name] = name; - const displayName = getCompletionEntryDisplayName(unescapeIdentifier(name), target, /*performCharacterChecks*/ true); - if (displayName) { - const entry = { - name: displayName, - kind: ScriptElementKind.warning, - kindModifiers: "", - sortText: "1" - }; - entries.push(entry); - } - } - } - - return entries; - } - - function getAllJsDocCompletionEntries(): CompletionEntry[] { - return jsDocCompletionEntries || (jsDocCompletionEntries = ts.map(jsDocTagNames, tagName => { - return { - name: tagName, - kind: ScriptElementKind.keyword, - kindModifiers: "", - sortText: "0", - }; - })); - } - - function createCompletionEntry(symbol: Symbol, location: Node, performCharacterChecks: boolean): CompletionEntry { - // Try to get a valid display name for this symbol, if we could not find one, then ignore it. - // We would like to only show things that can be added after a dot, so for instance numeric properties can - // not be accessed with a dot (a.1 <- invalid) - const displayName = getCompletionEntryDisplayNameForSymbol(symbol, program.getCompilerOptions().target, performCharacterChecks, location); - if (!displayName) { - return undefined; - } - - // TODO(drosen): Right now we just permit *all* semantic meanings when calling - // 'getSymbolKind' which is permissible given that it is backwards compatible; but - // really we should consider passing the meaning for the node so that we don't report - // that a suggestion for a value is an interface. We COULD also just do what - // 'getSymbolModifiers' does, which is to use the first declaration. - - // Use a 'sortText' of 0' so that all symbol completion entries come before any other - // entries (like JavaScript identifier entries). - return { - name: displayName, - kind: getSymbolKind(symbol, location), - kindModifiers: getSymbolModifiers(symbol), - sortText: "0", - }; - - } - - function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: CompletionEntry[], location: Node, performCharacterChecks: boolean): Map { - const start = timestamp(); - const uniqueNames = createMap(); - if (symbols) { - for (const symbol of symbols) { - const entry = createCompletionEntry(symbol, location, performCharacterChecks); - if (entry) { - const id = escapeIdentifier(entry.name); - if (!uniqueNames[id]) { - entries.push(entry); - uniqueNames[id] = id; - } - } - } - } - - log("getCompletionsAtPosition: getCompletionEntriesFromSymbols: " + (timestamp() - start)); - return uniqueNames; - } - - function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number) { - const node = findPrecedingToken(position, sourceFile); - if (!node || node.kind !== SyntaxKind.StringLiteral) { - return undefined; - } - - if (node.parent.kind === SyntaxKind.PropertyAssignment && node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression) { - // Get quoted name of properties of the object literal expression - // i.e. interface ConfigFiles { - // 'jspm:dev': string - // } - // let files: ConfigFiles = { - // '/*completion position*/' - // } - // - // function foo(c: ConfigFiles) {} - // foo({ - // '/*completion position*/' - // }); - return getStringLiteralCompletionEntriesFromPropertyAssignment(node.parent); - } - else if (isElementAccessExpression(node.parent) && node.parent.argumentExpression === node) { - // Get all names of properties on the expression - // i.e. interface A { - // 'prop1': string - // } - // let a: A; - // a['/*completion position*/'] - return getStringLiteralCompletionEntriesFromElementAccess(node.parent); - } - else if (node.parent.kind === SyntaxKind.ImportDeclaration || isExpressionOfExternalModuleImportEqualsDeclaration(node) || isRequireCall(node.parent, false)) { - // Get all known external module names or complete a path to a module - // i.e. import * as ns from "/*completion position*/"; - // import x = require("/*completion position*/"); - // var y = require("/*completion position*/"); - return getStringLiteralCompletionEntriesFromModuleNames(node); - } - else { - const argumentInfo = SignatureHelp.getContainingArgumentInfo(node, position, sourceFile); - if (argumentInfo) { - // Get string literal completions from specialized signatures of the target - // i.e. declare function f(a: 'A'); - // f("/*completion position*/") - return getStringLiteralCompletionEntriesFromCallExpression(argumentInfo, node); - } - - // Get completion for string literal from string literal type - // i.e. var x: "hi" | "hello" = "/*completion position*/" - return getStringLiteralCompletionEntriesFromContextualType(node); - } - } - - function getStringLiteralCompletionEntriesFromPropertyAssignment(element: ObjectLiteralElement) { - const typeChecker = program.getTypeChecker(); - const type = typeChecker.getContextualType((element.parent)); - const entries: CompletionEntry[] = []; - if (type) { - getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, element, /*performCharacterChecks*/false); - if (entries.length) { - return { isMemberCompletion: true, isNewIdentifierLocation: true, entries }; - } - } - } - - function getStringLiteralCompletionEntriesFromCallExpression(argumentInfo: SignatureHelp.ArgumentListInfo, location: Node) { - const typeChecker = program.getTypeChecker(); - const candidates: Signature[] = []; - const entries: CompletionEntry[] = []; - - typeChecker.getResolvedSignature(argumentInfo.invocation, candidates); - - for (const candidate of candidates) { - if (candidate.parameters.length > argumentInfo.argumentIndex) { - const parameter = candidate.parameters[argumentInfo.argumentIndex]; - addStringLiteralCompletionsFromType(typeChecker.getTypeAtLocation(parameter.valueDeclaration), entries); - } - } - - if (entries.length) { - return { isMemberCompletion: false, isNewIdentifierLocation: true, entries }; - } - - return undefined; - } - - function getStringLiteralCompletionEntriesFromElementAccess(node: ElementAccessExpression) { - const typeChecker = program.getTypeChecker(); - const type = typeChecker.getTypeAtLocation(node.expression); - const entries: CompletionEntry[] = []; - if (type) { - getCompletionEntriesFromSymbols(type.getApparentProperties(), entries, node, /*performCharacterChecks*/false); - if (entries.length) { - return { isMemberCompletion: true, isNewIdentifierLocation: true, entries }; - } - } - return undefined; - } - - function getStringLiteralCompletionEntriesFromContextualType(node: StringLiteral) { - const typeChecker = program.getTypeChecker(); - const type = typeChecker.getContextualType(node); - if (type) { - const entries: CompletionEntry[] = []; - addStringLiteralCompletionsFromType(type, entries); - if (entries.length) { - return { isMemberCompletion: false, isNewIdentifierLocation: false, entries }; - } - } - return undefined; - } - - function addStringLiteralCompletionsFromType(type: Type, result: CompletionEntry[]): void { - if (!type) { - return; - } - if (type.flags & TypeFlags.Union) { - forEach((type).types, t => addStringLiteralCompletionsFromType(t, result)); - } - else { - if (type.flags & TypeFlags.StringLiteral) { - result.push({ - name: (type).text, - kindModifiers: ScriptElementKindModifier.none, - kind: ScriptElementKind.variableElement, - sortText: "0" - }); - } - } - } - - function getStringLiteralCompletionEntriesFromModuleNames(node: StringLiteral): CompletionInfo { - const literalValue = normalizeSlashes(node.text); - - const scriptPath = node.getSourceFile().path; - const scriptDirectory = getDirectoryPath(scriptPath); - - const span = getDirectoryFragmentTextSpan((node).text, node.getStart() + 1); - let entries: CompletionEntry[]; - if (isPathRelativeToScript(literalValue) || isRootedDiskPath(literalValue)) { - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions.rootDirs) { - entries = getCompletionEntriesForDirectoryFragmentWithRootDirs( - compilerOptions.rootDirs, literalValue, scriptDirectory, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/false, span, scriptPath); - } - else { - entries = getCompletionEntriesForDirectoryFragment( - literalValue, scriptDirectory, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/false, span, scriptPath); - } - } - else { - // Check for node modules - entries = getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, span); - } - return { - isMemberCompletion: false, - isNewIdentifierLocation: true, - entries - }; - } - - /** - * Takes a script path and returns paths for all potential folders that could be merged with its - * containing folder via the "rootDirs" compiler option - */ - function getBaseDirectoriesFromRootDirs(rootDirs: string[], basePath: string, scriptPath: string, ignoreCase: boolean): string[] { - // Make all paths absolute/normalized if they are not already - rootDirs = map(rootDirs, rootDirectory => normalizePath(isRootedDiskPath(rootDirectory) ? rootDirectory : combinePaths(basePath, rootDirectory))); - - // Determine the path to the directory containing the script relative to the root directory it is contained within - let relativeDirectory: string; - for (const rootDirectory of rootDirs) { - if (containsPath(rootDirectory, scriptPath, basePath, ignoreCase)) { - relativeDirectory = scriptPath.substr(rootDirectory.length); - break; - } - } - - // Now find a path for each potential directory that is to be merged with the one containing the script - return deduplicate(map(rootDirs, rootDirectory => combinePaths(rootDirectory, relativeDirectory))); - } - - function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[], fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, exclude?: string): CompletionEntry[] { - const basePath = program.getCompilerOptions().project || host.getCurrentDirectory(); - const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); - const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptPath, ignoreCase); - - const result: CompletionEntry[] = []; - - for (const baseDirectory of baseDirectories) { - getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensions, includeExtensions, span, exclude, result); - } - - return result; - } - - function getCompletionEntriesForDirectoryFragment(fragment: string, scriptPath: string, extensions: string[], includeExtensions: boolean, span: TextSpan, exclude?: string, result: CompletionEntry[] = []): CompletionEntry[] { - fragment = getDirectoryPath(fragment); - if (!fragment) { - fragment = "./"; - } - else { - fragment = ensureTrailingDirectorySeparator(fragment); - } - - const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); - const baseDirectory = getDirectoryPath(absolutePath); - const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames()); - - if (directoryProbablyExists(baseDirectory, host)) { - if (host.readDirectory) { - // Enumerate the available files if possible - const files = host.readDirectory(baseDirectory, extensions, /*exclude*/undefined, /*include*/["./*"]); - const foundFiles = createMap(); - for (let filePath of files) { - filePath = normalizePath(filePath); - if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) { - continue; - } - - const foundFileName = includeExtensions ? getBaseFileName(filePath) : removeFileExtension(getBaseFileName(filePath)); - - if (!foundFiles[foundFileName]) { - foundFiles[foundFileName] = true; - } - } - - for (const foundFile in foundFiles) { - result.push(createCompletionEntryForModule(foundFile, ScriptElementKind.scriptElement, span)); - } - } - - // If possible, get folder completion as well - if (host.getDirectories) { - const directories = host.getDirectories(baseDirectory); - for (const directory of directories) { - const directoryName = getBaseFileName(normalizePath(directory)); - - result.push(createCompletionEntryForModule(directoryName, ScriptElementKind.directory, span)); - } - } - } - - return result; - } - - /** - * Check all of the declared modules and those in node modules. Possible sources of modules: - * Modules that are found by the type checker - * Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option) - * Modules from node_modules (i.e. those listed in package.json) - * This includes all files that are found in node_modules/moduleName/ with acceptable file extensions - */ - function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string, span: TextSpan): CompletionEntry[] { - const options = program.getCompilerOptions(); - const { baseUrl, paths } = options; - - let result: CompletionEntry[]; - - if (baseUrl) { - const fileExtensions = getSupportedExtensions(options); - const projectDir = options.project || host.getCurrentDirectory(); - const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(projectDir, baseUrl); - result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false, span); - - if (paths) { - for (const path in paths) { - if (paths.hasOwnProperty(path)) { - if (path === "*") { - if (paths[path]) { - for (const pattern of paths[path]) { - for (const match of getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions)) { - result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName, span)); - } - } - } - } - else if (startsWith(path, fragment)) { - const entry = paths[path] && paths[path].length === 1 && paths[path][0]; - if (entry) { - result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName, span)); - } - } - } - } - } - } - else { - result = []; - } - - getCompletionEntriesFromTypings(host, options, scriptPath, span, result); - - for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, options)) { - result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); - } - - return result; - } - - function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: string[]): string[] { - if (host.readDirectory) { - const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined; - if (parsed) { - // The prefix has two effective parts: the directory path and the base component after the filepath that is not a - // full directory component. For example: directory/path/of/prefix/base* - const normalizedPrefix = normalizeAndPreserveTrailingSlash(parsed.prefix); - const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); - const normalizedPrefixBase = getBaseFileName(normalizedPrefix); - - const fragmentHasPath = fragment.indexOf(directorySeparator) !== -1; - - // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call - const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory; - - const normalizedSuffix = normalizePath(parsed.suffix); - const baseDirectory = combinePaths(baseUrl, expandedPrefixDirectory); - const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase; - - // If we have a suffix, then we need to read the directory all the way down. We could create a glob - // that encodes the suffix, but we would have to escape the character "?" which readDirectory - // doesn't support. For now, this is safer but slower - const includeGlob = normalizedSuffix ? "**/*" : "./*"; - - const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]); - const result: string[] = []; - - // Trim away prefix and suffix - for (const match of matches) { - const normalizedMatch = normalizePath(match); - if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) { - continue; - } - - const start = completePrefix.length; - const length = normalizedMatch.length - start - normalizedSuffix.length; - - result.push(removeFileExtension(normalizedMatch.substr(start, length))); - } - return result; - } - } - - return undefined; - } - - function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions): string[] { - // Check If this is a nested module - const isNestedModule = fragment.indexOf(directorySeparator) !== -1; - const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined; - - // Get modules that the type checker picked up - const ambientModules = map(program.getTypeChecker().getAmbientModules(), sym => stripQuotes(sym.name)); - let nonRelativeModules = filter(ambientModules, moduleName => startsWith(moduleName, fragment)); - - // Nested modules of the form "module-name/sub" need to be adjusted to only return the string - // after the last '/' that appears in the fragment because that's where the replacement span - // starts - if (isNestedModule) { - const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment); - nonRelativeModules = map(nonRelativeModules, moduleName => { - if (startsWith(fragment, moduleNameWithSeperator)) { - return moduleName.substr(moduleNameWithSeperator.length); - } - return moduleName; - }); - } - - - if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) { - for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) { - if (!isNestedModule) { - nonRelativeModules.push(visibleModule.moduleName); - } - else if (host.readDirectory && startsWith(visibleModule.moduleName, moduleNameFragment)) { - const nestedFiles = host.readDirectory(visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/undefined, /*include*/["./*"]); - - for (let f of nestedFiles) { - f = normalizePath(f); - const nestedModule = removeFileExtension(getBaseFileName(f)); - nonRelativeModules.push(nestedModule); - } - } - } - } - - return deduplicate(nonRelativeModules); - } - - function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number): CompletionInfo { - const token = getTokenAtPosition(sourceFile, position); - if (!token) { - return undefined; - } - const commentRanges: CommentRange[] = getLeadingCommentRanges(sourceFile.text, token.pos); - - if (!commentRanges || !commentRanges.length) { - return undefined; - } - - const range = forEach(commentRanges, commentRange => position >= commentRange.pos && position <= commentRange.end && commentRange); - - if (!range) { - return undefined; - } - - const text = sourceFile.text.substr(range.pos, position - range.pos); - - const match = tripleSlashDirectiveFragmentRegex.exec(text); - if (match) { - const prefix = match[1]; - const kind = match[2]; - const toComplete = match[3]; - - const scriptPath = getDirectoryPath(sourceFile.path); - let entries: CompletionEntry[]; - if (kind === "path") { - // Give completions for a relative path - const span: TextSpan = getDirectoryFragmentTextSpan(toComplete, range.pos + prefix.length); - entries = getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getSupportedExtensions(program.getCompilerOptions()), /*includeExtensions*/true, span, sourceFile.path); - } - else { - // Give completions based on the typings available - const span: TextSpan = { start: range.pos + prefix.length, length: match[0].length - prefix.length }; - entries = getCompletionEntriesFromTypings(host, program.getCompilerOptions(), scriptPath, span); - } - - return { - isMemberCompletion: false, - isNewIdentifierLocation: true, - entries - }; - } - - return undefined; - } - - function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: CompilerOptions, scriptPath: string, span: TextSpan, result: CompletionEntry[] = []): CompletionEntry[] { - // Check for typings specified in compiler options - if (options.types) { - for (const moduleName of options.types) { - result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName, span)); - } - } - else if (host.getDirectories) { - const typeRoots = getEffectiveTypeRoots(options, host); - for (const root of typeRoots) { - getCompletionEntriesFromDirectories(host, options, root, span, result); - } - } - - if (host.getDirectories) { - // Also get all @types typings installed in visible node_modules directories - for (const package of findPackageJsons(scriptPath)) { - const typesDir = combinePaths(getDirectoryPath(package), "node_modules/@types"); - getCompletionEntriesFromDirectories(host, options, typesDir, span, result); - } - } - - return result; - } - - function getCompletionEntriesFromDirectories(host: LanguageServiceHost, options: CompilerOptions, directory: string, span: TextSpan, result: CompletionEntry[]) { - if (host.getDirectories && directoryProbablyExists(directory, host)) { - for (let typeDirectory of host.getDirectories(directory)) { - typeDirectory = normalizePath(typeDirectory); - result.push(createCompletionEntryForModule(getBaseFileName(typeDirectory), ScriptElementKind.externalModuleName, span)); - } - } - } - - function findPackageJsons(currentDir: string): string[] { - const paths: string[] = []; - let currentConfigPath: string; - while (true) { - currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json"); - if (currentConfigPath) { - paths.push(currentConfigPath); - - currentDir = getDirectoryPath(currentConfigPath); - const parent = getDirectoryPath(currentDir); - if (currentDir === parent) { - break; - } - currentDir = parent; - } - else { - break; - } - } - - return paths; - } - - - function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) { - const result: VisibleModuleInfo[] = []; - - if (host.readFile && host.fileExists) { - for (const packageJson of findPackageJsons(scriptPath)) { - const package = tryReadingPackageJson(packageJson); - if (!package) { - return; - } - - const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); - const foundModuleNames: string[] = []; - - // Provide completions for all non @types dependencies - for (const key of nodeModulesDependencyKeys) { - addPotentialPackageNames(package[key], foundModuleNames); - } - - for (const moduleName of foundModuleNames) { - const moduleDir = combinePaths(nodeModulesDir, moduleName); - result.push({ - moduleName, - moduleDir - }); - } - } - } - - return result; - - function tryReadingPackageJson(filePath: string) { - try { - const fileText = host.readFile(filePath); - return JSON.parse(fileText); - } - catch (e) { - return undefined; - } - } - - function addPotentialPackageNames(dependencies: any, result: string[]) { - if (dependencies) { - for (const dep in dependencies) { - if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) { - result.push(dep); - } - } - } - } - } - - function createCompletionEntryForModule(name: string, kind: string, replacementSpan: TextSpan): CompletionEntry { - return { name, kind, kindModifiers: ScriptElementKindModifier.none, sortText: name, replacementSpan }; - } - - // Replace everything after the last directory seperator that appears - function getDirectoryFragmentTextSpan(text: string, textStart: number): TextSpan { - const index = text.lastIndexOf(directorySeparator); - const offset = index !== -1 ? index + 1 : 0; - return { start: textStart + offset, length: text.length - offset }; - } - - // Returns true if the path is explicitly relative to the script (i.e. relative to . or ..) - function isPathRelativeToScript(path: string) { - if (path && path.length >= 2 && path.charCodeAt(0) === CharacterCodes.dot) { - const slashIndex = path.length >= 3 && path.charCodeAt(1) === CharacterCodes.dot ? 2 : 1; - const slashCharCode = path.charCodeAt(slashIndex); - return slashCharCode === CharacterCodes.slash || slashCharCode === CharacterCodes.backslash; - } - return false; - } - - function normalizeAndPreserveTrailingSlash(path: string) { - return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(normalizePath(path)) : normalizePath(path); - } + return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position); } function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { synchronizeHostData(); - - // Compute all the completion symbols again. - const completionData = getCompletionData(fileName, position); - if (completionData) { - const { symbols, location } = completionData; - - // Find the symbol with the matching entry name. - const target = program.getCompilerOptions().target; - // We don't need to perform character checks here because we're only comparing the - // name against 'entryName' (which is known to be good), not building a new - // completion entry. - const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, target, /*performCharacterChecks*/ false, location) === entryName ? s : undefined); - - if (symbol) { - const { displayParts, documentation, symbolKind } = getSymbolDisplayPartsDocumentationAndSymbolKind(symbol, getValidSourceFile(fileName), location, location, SemanticMeaning.All); - return { - name: entryName, - kindModifiers: getSymbolModifiers(symbol), - kind: symbolKind, - displayParts, - documentation - }; - } - } - - // Didn't find a symbol with this name. See if we can find a keyword instead. - const keywordCompletion = forEach(keywordCompletions, c => c.name === entryName); - if (keywordCompletion) { - return { - name: entryName, - kind: ScriptElementKind.keyword, - kindModifiers: ScriptElementKindModifier.none, - displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)], - documentation: undefined - }; - } - - return undefined; - } - - // TODO(drosen): use contextual SemanticMeaning. - function getSymbolKind(symbol: Symbol, location: Node): string { - const flags = symbol.getFlags(); - - if (flags & SymbolFlags.Class) return getDeclarationOfKind(symbol, SyntaxKind.ClassExpression) ? - ScriptElementKind.localClassElement : ScriptElementKind.classElement; - if (flags & SymbolFlags.Enum) return ScriptElementKind.enumElement; - if (flags & SymbolFlags.TypeAlias) return ScriptElementKind.typeElement; - if (flags & SymbolFlags.Interface) return ScriptElementKind.interfaceElement; - if (flags & SymbolFlags.TypeParameter) return ScriptElementKind.typeParameterElement; - - const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(symbol, flags, location); - if (result === ScriptElementKind.unknown) { - if (flags & SymbolFlags.TypeParameter) return ScriptElementKind.typeParameterElement; - if (flags & SymbolFlags.EnumMember) return ScriptElementKind.variableElement; - if (flags & SymbolFlags.Alias) return ScriptElementKind.alias; - if (flags & SymbolFlags.Module) return ScriptElementKind.moduleElement; - } - - return result; - } - - function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(symbol: Symbol, flags: SymbolFlags, location: Node) { - const typeChecker = program.getTypeChecker(); - - if (typeChecker.isUndefinedSymbol(symbol)) { - return ScriptElementKind.variableElement; - } - if (typeChecker.isArgumentsSymbol(symbol)) { - return ScriptElementKind.localVariableElement; - } - if (location.kind === SyntaxKind.ThisKeyword && isExpression(location)) { - return ScriptElementKind.parameterElement; - } - if (flags & SymbolFlags.Variable) { - if (isFirstDeclarationOfSymbolParameter(symbol)) { - return ScriptElementKind.parameterElement; - } - else if (symbol.valueDeclaration && isConst(symbol.valueDeclaration)) { - return ScriptElementKind.constElement; - } - else if (forEach(symbol.declarations, isLet)) { - return ScriptElementKind.letElement; - } - return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localVariableElement : ScriptElementKind.variableElement; - } - if (flags & SymbolFlags.Function) return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localFunctionElement : ScriptElementKind.functionElement; - if (flags & SymbolFlags.GetAccessor) return ScriptElementKind.memberGetAccessorElement; - if (flags & SymbolFlags.SetAccessor) return ScriptElementKind.memberSetAccessorElement; - if (flags & SymbolFlags.Method) return ScriptElementKind.memberFunctionElement; - if (flags & SymbolFlags.Constructor) return ScriptElementKind.constructorImplementationElement; - - if (flags & SymbolFlags.Property) { - if (flags & SymbolFlags.SyntheticProperty) { - // If union property is result of union of non method (property/accessors/variables), it is labeled as property - const unionPropertyKind = forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { - const rootSymbolFlags = rootSymbol.getFlags(); - if (rootSymbolFlags & (SymbolFlags.PropertyOrAccessor | SymbolFlags.Variable)) { - return ScriptElementKind.memberVariableElement; - } - Debug.assert(!!(rootSymbolFlags & SymbolFlags.Method)); - }); - if (!unionPropertyKind) { - // If this was union of all methods, - // make sure it has call signatures before we can label it as method - const typeOfUnionProperty = typeChecker.getTypeOfSymbolAtLocation(symbol, location); - if (typeOfUnionProperty.getCallSignatures().length) { - return ScriptElementKind.memberFunctionElement; - } - return ScriptElementKind.memberVariableElement; - } - return unionPropertyKind; - } - return ScriptElementKind.memberVariableElement; - } - - return ScriptElementKind.unknown; - } - - function getSymbolModifiers(symbol: Symbol): string { - return symbol && symbol.declarations && symbol.declarations.length > 0 - ? getNodeModifiers(symbol.declarations[0]) - : ScriptElementKindModifier.none; - } - - // TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location - function getSymbolDisplayPartsDocumentationAndSymbolKind(symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node, - location: Node, semanticMeaning = getMeaningFromLocation(location)) { - - const typeChecker = program.getTypeChecker(); - - const displayParts: SymbolDisplayPart[] = []; - let documentation: SymbolDisplayPart[]; - const symbolFlags = symbol.flags; - let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(symbol, symbolFlags, location); - let hasAddedSymbolInfo: boolean; - const isThisExpression = location.kind === SyntaxKind.ThisKeyword && isExpression(location); - let type: Type; - - // Class at constructor site need to be shown as constructor apart from property,method, vars - if (symbolKind !== ScriptElementKind.unknown || symbolFlags & SymbolFlags.Class || symbolFlags & SymbolFlags.Alias) { - // If it is accessor they are allowed only if location is at name of the accessor - if (symbolKind === ScriptElementKind.memberGetAccessorElement || symbolKind === ScriptElementKind.memberSetAccessorElement) { - symbolKind = ScriptElementKind.memberVariableElement; - } - - let signature: Signature; - type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol, location); - if (type) { - if (location.parent && location.parent.kind === SyntaxKind.PropertyAccessExpression) { - const right = (location.parent).name; - // Either the location is on the right of a property access, or on the left and the right is missing - if (right === location || (right && right.getFullWidth() === 0)) { - location = location.parent; - } - } - - // try get the call/construct signature from the type if it matches - let callExpression: CallExpression; - if (location.kind === SyntaxKind.CallExpression || location.kind === SyntaxKind.NewExpression) { - callExpression = location; - } - else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { - callExpression = location.parent; - } - - if (callExpression) { - const candidateSignatures: Signature[] = []; - signature = typeChecker.getResolvedSignature(callExpression, candidateSignatures); - if (!signature && candidateSignatures.length) { - // Use the first candidate: - signature = candidateSignatures[0]; - } - - const useConstructSignatures = callExpression.kind === SyntaxKind.NewExpression || callExpression.expression.kind === SyntaxKind.SuperKeyword; - const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); - - if (!contains(allSignatures, signature.target) && !contains(allSignatures, signature)) { - // Get the first signature if there is one -- allSignatures may contain - // either the original signature or its target, so check for either - signature = allSignatures.length ? allSignatures[0] : undefined; - } - - if (signature) { - if (useConstructSignatures && (symbolFlags & SymbolFlags.Class)) { - // Constructor - symbolKind = ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); - } - else if (symbolFlags & SymbolFlags.Alias) { - symbolKind = ScriptElementKind.alias; - pushTypePart(symbolKind); - displayParts.push(spacePart()); - if (useConstructSignatures) { - displayParts.push(keywordPart(SyntaxKind.NewKeyword)); - displayParts.push(spacePart()); - } - addFullSymbolName(symbol); - } - else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); - } - - switch (symbolKind) { - case ScriptElementKind.memberVariableElement: - case ScriptElementKind.variableElement: - case ScriptElementKind.constElement: - case ScriptElementKind.letElement: - case ScriptElementKind.parameterElement: - case ScriptElementKind.localVariableElement: - // If it is call or construct signature of lambda's write type name - displayParts.push(punctuationPart(SyntaxKind.ColonToken)); - displayParts.push(spacePart()); - if (useConstructSignatures) { - displayParts.push(keywordPart(SyntaxKind.NewKeyword)); - displayParts.push(spacePart()); - } - if (!(type.flags & TypeFlags.Anonymous) && type.symbol) { - addRange(displayParts, symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, SymbolFormatFlags.WriteTypeParametersOrArguments)); - } - addSignatureDisplayParts(signature, allSignatures, TypeFormatFlags.WriteArrowStyleSignature); - break; - - default: - // Just signature - addSignatureDisplayParts(signature, allSignatures); - } - hasAddedSymbolInfo = true; - } - } - else if ((isNameOfFunctionDeclaration(location) && !(symbol.flags & SymbolFlags.Accessor)) || // name of function declaration - (location.kind === SyntaxKind.ConstructorKeyword && location.parent.kind === SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration - // get the signature from the declaration and write it - const functionDeclaration = location.parent; - const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures(); - if (!typeChecker.isImplementationOfOverload(functionDeclaration)) { - signature = typeChecker.getSignatureFromDeclaration(functionDeclaration); - } - else { - signature = allSignatures[0]; - } - - if (functionDeclaration.kind === SyntaxKind.Constructor) { - // show (constructor) Type(...) signature - symbolKind = ScriptElementKind.constructorImplementationElement; - addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); - } - else { - // (function/method) symbol(..signature) - addPrefixForAnyFunctionOrVar(functionDeclaration.kind === SyntaxKind.CallSignature && - !(type.symbol.flags & SymbolFlags.TypeLiteral || type.symbol.flags & SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); - } - - addSignatureDisplayParts(signature, allSignatures); - hasAddedSymbolInfo = true; - } - } - } - if (symbolFlags & SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { - if (getDeclarationOfKind(symbol, SyntaxKind.ClassExpression)) { - // Special case for class expressions because we would like to indicate that - // the class name is local to the class body (similar to function expression) - // (local class) class - pushTypePart(ScriptElementKind.localClassElement); - } - else { - // Class declaration has name which is not local. - displayParts.push(keywordPart(SyntaxKind.ClassKeyword)); - } - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if ((symbolFlags & SymbolFlags.Interface) && (semanticMeaning & SemanticMeaning.Type)) { - addNewLineIfDisplayPartsExist(); - displayParts.push(keywordPart(SyntaxKind.InterfaceKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - } - if (symbolFlags & SymbolFlags.TypeAlias) { - addNewLineIfDisplayPartsExist(); - displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - writeTypeParametersOfSymbol(symbol, sourceFile); - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - addRange(displayParts, typeToDisplayParts(typeChecker, typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias)); - } - if (symbolFlags & SymbolFlags.Enum) { - addNewLineIfDisplayPartsExist(); - if (forEach(symbol.declarations, isConstEnumDeclaration)) { - displayParts.push(keywordPart(SyntaxKind.ConstKeyword)); - displayParts.push(spacePart()); - } - displayParts.push(keywordPart(SyntaxKind.EnumKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - } - if (symbolFlags & SymbolFlags.Module) { - addNewLineIfDisplayPartsExist(); - const declaration = getDeclarationOfKind(symbol, SyntaxKind.ModuleDeclaration); - const isNamespace = declaration && declaration.name && declaration.name.kind === SyntaxKind.Identifier; - displayParts.push(keywordPart(isNamespace ? SyntaxKind.NamespaceKeyword : SyntaxKind.ModuleKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - } - if ((symbolFlags & SymbolFlags.TypeParameter) && (semanticMeaning & SemanticMeaning.Type)) { - addNewLineIfDisplayPartsExist(); - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(textPart("type parameter")); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.InKeyword)); - displayParts.push(spacePart()); - if (symbol.parent) { - // Class/Interface type parameter - addFullSymbolName(symbol.parent, enclosingDeclaration); - writeTypeParametersOfSymbol(symbol.parent, enclosingDeclaration); - } - else { - // Method/function type parameter - let declaration = getDeclarationOfKind(symbol, SyntaxKind.TypeParameter); - Debug.assert(declaration !== undefined); - declaration = declaration.parent; - - if (declaration) { - if (isFunctionLikeKind(declaration.kind)) { - const signature = typeChecker.getSignatureFromDeclaration(declaration); - if (declaration.kind === SyntaxKind.ConstructSignature) { - displayParts.push(keywordPart(SyntaxKind.NewKeyword)); - displayParts.push(spacePart()); - } - else if (declaration.kind !== SyntaxKind.CallSignature && (declaration).name) { - addFullSymbolName(declaration.symbol); - } - addRange(displayParts, signatureToDisplayParts(typeChecker, signature, sourceFile, TypeFormatFlags.WriteTypeArgumentsOfSignature)); - } - else { - // Type alias type parameter - // For example - // type list = T[]; // Both T will go through same code path - displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); - displayParts.push(spacePart()); - addFullSymbolName(declaration.symbol); - writeTypeParametersOfSymbol(declaration.symbol, sourceFile); - } - } - } - } - if (symbolFlags & SymbolFlags.EnumMember) { - addPrefixForAnyFunctionOrVar(symbol, "enum member"); - const declaration = symbol.declarations[0]; - if (declaration.kind === SyntaxKind.EnumMember) { - const constantValue = typeChecker.getConstantValue(declaration); - if (constantValue !== undefined) { - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - displayParts.push(displayPart(constantValue.toString(), SymbolDisplayPartKind.numericLiteral)); - } - } - } - if (symbolFlags & SymbolFlags.Alias) { - addNewLineIfDisplayPartsExist(); - if (symbol.declarations[0].kind === SyntaxKind.NamespaceExportDeclaration) { - displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); - displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.NamespaceKeyword)); - } - else { - displayParts.push(keywordPart(SyntaxKind.ImportKeyword)); - } - displayParts.push(spacePart()); - addFullSymbolName(symbol); - ts.forEach(symbol.declarations, declaration => { - if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) { - const importEqualsDeclaration = declaration; - if (isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - displayParts.push(keywordPart(SyntaxKind.RequireKeyword)); - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(displayPart(getTextOfNode(getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), SymbolDisplayPartKind.stringLiteral)); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - } - else { - const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); - if (internalAliasSymbol) { - displayParts.push(spacePart()); - displayParts.push(operatorPart(SyntaxKind.EqualsToken)); - displayParts.push(spacePart()); - addFullSymbolName(internalAliasSymbol, enclosingDeclaration); - } - } - return true; - } - }); - } - if (!hasAddedSymbolInfo) { - if (symbolKind !== ScriptElementKind.unknown) { - if (type) { - if (isThisExpression) { - addNewLineIfDisplayPartsExist(); - displayParts.push(keywordPart(SyntaxKind.ThisKeyword)); - } - else { - addPrefixForAnyFunctionOrVar(symbol, symbolKind); - } - - // For properties, variables and local vars: show the type - if (symbolKind === ScriptElementKind.memberVariableElement || - symbolFlags & SymbolFlags.Variable || - symbolKind === ScriptElementKind.localVariableElement || - isThisExpression) { - displayParts.push(punctuationPart(SyntaxKind.ColonToken)); - displayParts.push(spacePart()); - // If the type is type parameter, format it specially - if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter) { - const typeParameterParts = mapToDisplayParts(writer => { - typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplay(type, writer, enclosingDeclaration); - }); - addRange(displayParts, typeParameterParts); - } - else { - addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration)); - } - } - else if (symbolFlags & SymbolFlags.Function || - symbolFlags & SymbolFlags.Method || - symbolFlags & SymbolFlags.Constructor || - symbolFlags & SymbolFlags.Signature || - symbolFlags & SymbolFlags.Accessor || - symbolKind === ScriptElementKind.memberFunctionElement) { - const allSignatures = type.getNonNullableType().getCallSignatures(); - addSignatureDisplayParts(allSignatures[0], allSignatures); - } - } - } - else { - symbolKind = getSymbolKind(symbol, location); - } - } - - if (!documentation) { - documentation = symbol.getDocumentationComment(); - if (documentation.length === 0 && symbol.flags & SymbolFlags.Property) { - // For some special property access expressions like `experts.foo = foo` or `module.exports.foo = foo` - // there documentation comments might be attached to the right hand side symbol of their declarations. - // The pattern of such special property access is that the parent symbol is the symbol of the file. - if (symbol.parent && forEach(symbol.parent.declarations, declaration => declaration.kind === SyntaxKind.SourceFile)) { - for (const declaration of symbol.declarations) { - if (!declaration.parent || declaration.parent.kind !== SyntaxKind.BinaryExpression) { - continue; - } - - const rhsSymbol = program.getTypeChecker().getSymbolAtLocation((declaration.parent).right); - if (!rhsSymbol) { - continue; - } - - documentation = rhsSymbol.getDocumentationComment(); - if (documentation.length > 0) { - break; - } - } - } - } - } - - return { displayParts, documentation, symbolKind }; - - function addNewLineIfDisplayPartsExist() { - if (displayParts.length) { - displayParts.push(lineBreakPart()); - } - } - - function addFullSymbolName(symbol: Symbol, enclosingDeclaration?: Node) { - const fullSymbolDisplayParts = symbolToDisplayParts(typeChecker, symbol, enclosingDeclaration || sourceFile, /*meaning*/ undefined, - SymbolFormatFlags.WriteTypeParametersOrArguments | SymbolFormatFlags.UseOnlyExternalAliasing); - addRange(displayParts, fullSymbolDisplayParts); - } - - function addPrefixForAnyFunctionOrVar(symbol: Symbol, symbolKind: string) { - addNewLineIfDisplayPartsExist(); - if (symbolKind) { - pushTypePart(symbolKind); - displayParts.push(spacePart()); - addFullSymbolName(symbol); - } - } - - function pushTypePart(symbolKind: string) { - switch (symbolKind) { - case ScriptElementKind.variableElement: - case ScriptElementKind.functionElement: - case ScriptElementKind.letElement: - case ScriptElementKind.constElement: - case ScriptElementKind.constructorImplementationElement: - displayParts.push(textOrKeywordPart(symbolKind)); - return; - default: - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(textOrKeywordPart(symbolKind)); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - return; - } - } - - function addSignatureDisplayParts(signature: Signature, allSignatures: Signature[], flags?: TypeFormatFlags) { - addRange(displayParts, signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | TypeFormatFlags.WriteTypeArgumentsOfSignature)); - if (allSignatures.length > 1) { - displayParts.push(spacePart()); - displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); - displayParts.push(operatorPart(SyntaxKind.PlusToken)); - displayParts.push(displayPart((allSignatures.length - 1).toString(), SymbolDisplayPartKind.numericLiteral)); - displayParts.push(spacePart()); - displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads")); - displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); - } - documentation = signature.getDocumentationComment(); - } - - function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node) { - const typeParameterParts = mapToDisplayParts(writer => { - typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplayFromSymbol(symbol, writer, enclosingDeclaration); - }); - addRange(displayParts, typeParameterParts); - } + return Completions.getCompletionEntryDetails(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName); } function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { @@ -4747,265 +914,25 @@ namespace ts { return undefined; } - const displayPartsDocumentationsAndKind = getSymbolDisplayPartsDocumentationAndSymbolKind(symbol, sourceFile, getContainerNode(node), node); + const displayPartsDocumentationsAndKind = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(node), node); return { kind: displayPartsDocumentationsAndKind.symbolKind, - kindModifiers: getSymbolModifiers(symbol), + kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), textSpan: createTextSpan(node.getStart(), node.getWidth()), displayParts: displayPartsDocumentationsAndKind.displayParts, documentation: displayPartsDocumentationsAndKind.documentation }; } - function createDefinitionInfo(node: Node, symbolKind: string, symbolName: string, containerName: string): DefinitionInfo { - return { - fileName: node.getSourceFile().fileName, - textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), - kind: symbolKind, - name: symbolName, - containerKind: undefined, - containerName - }; - } - - function getSymbolInfo(typeChecker: TypeChecker, symbol: Symbol, node: Node) { - return { - symbolName: typeChecker.symbolToString(symbol), // Do not get scoped name, just the name of the symbol - symbolKind: getSymbolKind(symbol, node), - containerName: symbol.parent ? typeChecker.symbolToString(symbol.parent, node) : "" - }; - } - - function createDefinitionFromSignatureDeclaration(decl: SignatureDeclaration): DefinitionInfo { - const typeChecker = program.getTypeChecker(); - const { symbolName, symbolKind, containerName } = getSymbolInfo(typeChecker, decl.symbol, decl); - return createDefinitionInfo(decl, symbolKind, symbolName, containerName); - } - - function getDefinitionFromSymbol(symbol: Symbol, node: Node): DefinitionInfo[] { - const typeChecker = program.getTypeChecker(); - const result: DefinitionInfo[] = []; - const declarations = symbol.getDeclarations(); - const { symbolName, symbolKind, containerName } = getSymbolInfo(typeChecker, symbol, node); - - if (!tryAddConstructSignature(symbol, node, symbolKind, symbolName, containerName, result) && - !tryAddCallSignature(symbol, node, symbolKind, symbolName, containerName, result)) { - // Just add all the declarations. - forEach(declarations, declaration => { - result.push(createDefinitionInfo(declaration, symbolKind, symbolName, containerName)); - }); - } - - return result; - - function tryAddConstructSignature(symbol: Symbol, location: Node, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { - // Applicable only if we are in a new expression, or we are on a constructor declaration - // and in either case the symbol has a construct signature definition, i.e. class - if (isNewExpressionTarget(location) || location.kind === SyntaxKind.ConstructorKeyword) { - if (symbol.flags & SymbolFlags.Class) { - // Find the first class-like declaration and try to get the construct signature. - for (const declaration of symbol.getDeclarations()) { - if (isClassLike(declaration)) { - return tryAddSignature(declaration.members, - /*selectConstructors*/ true, - symbolKind, - symbolName, - containerName, - result); - } - } - - Debug.fail("Expected declaration to have at least one class-like declaration"); - } - } - return false; - } - - function tryAddCallSignature(symbol: Symbol, location: Node, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { - if (isCallExpressionTarget(location) || isNewExpressionTarget(location) || isNameOfFunctionDeclaration(location)) { - return tryAddSignature(symbol.declarations, /*selectConstructors*/ false, symbolKind, symbolName, containerName, result); - } - return false; - } - - function tryAddSignature(signatureDeclarations: Declaration[], selectConstructors: boolean, symbolKind: string, symbolName: string, containerName: string, result: DefinitionInfo[]) { - const declarations: Declaration[] = []; - let definition: Declaration; - - forEach(signatureDeclarations, d => { - if ((selectConstructors && d.kind === SyntaxKind.Constructor) || - (!selectConstructors && (d.kind === SyntaxKind.FunctionDeclaration || d.kind === SyntaxKind.MethodDeclaration || d.kind === SyntaxKind.MethodSignature))) { - declarations.push(d); - if ((d).body) definition = d; - } - }); - - if (definition) { - result.push(createDefinitionInfo(definition, symbolKind, symbolName, containerName)); - return true; - } - else if (declarations.length) { - result.push(createDefinitionInfo(lastOrUndefined(declarations), symbolKind, symbolName, containerName)); - return true; - } - - return false; - } - } - - function findReferenceInPosition(refs: FileReference[], pos: number): FileReference { - for (const ref of refs) { - if (ref.pos <= pos && pos < ref.end) { - return ref; - } - } - return undefined; - } - - function getDefinitionInfoForFileReference(name: string, targetFileName: string): DefinitionInfo { - return { - fileName: targetFileName, - textSpan: createTextSpanFromBounds(0, 0), - kind: ScriptElementKind.scriptElement, - name: name, - containerName: undefined, - containerKind: undefined - }; - } - /// Goto definition function getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - - /// Triple slash reference comments - const comment = findReferenceInPosition(sourceFile.referencedFiles, position); - if (comment) { - const referenceFile = tryResolveScriptReference(program, sourceFile, comment); - if (referenceFile) { - return [getDefinitionInfoForFileReference(comment.fileName, referenceFile.fileName)]; - } - return undefined; - } - - // Type reference directives - const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); - if (typeReferenceDirective) { - const referenceFile = program.getResolvedTypeReferenceDirectives()[typeReferenceDirective.fileName]; - if (referenceFile && referenceFile.resolvedFileName) { - return [getDefinitionInfoForFileReference(typeReferenceDirective.fileName, referenceFile.resolvedFileName)]; - } - return undefined; - } - - const node = getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } - - // Labels - if (isJumpStatementTarget(node)) { - const labelName = (node).text; - const label = getTargetLabel((node.parent), (node).text); - return label ? [createDefinitionInfo(label, ScriptElementKind.label, labelName, /*containerName*/ undefined)] : undefined; - } - - const typeChecker = program.getTypeChecker(); - - const calledDeclaration = tryGetSignatureDeclaration(typeChecker, node); - if (calledDeclaration) { - return [createDefinitionFromSignatureDeclaration(calledDeclaration)]; - } - - let symbol = typeChecker.getSymbolAtLocation(node); - - // Could not find a symbol e.g. node is string or number keyword, - // or the symbol was an internal symbol and does not have a declaration e.g. undefined symbol - if (!symbol) { - return undefined; - } - - // If this is an alias, and the request came at the declaration location - // get the aliased symbol instead. This allows for goto def on an import e.g. - // import {A, B} from "mod"; - // to jump to the implementation directly. - if (symbol.flags & SymbolFlags.Alias) { - const declaration = symbol.declarations[0]; - - // Go to the original declaration for cases: - // - // (1) when the aliased symbol was declared in the location(parent). - // (2) when the aliased symbol is originating from a named import. - // - if (node.kind === SyntaxKind.Identifier && - (node.parent === declaration || - (declaration.kind === SyntaxKind.ImportSpecifier && declaration.parent && declaration.parent.kind === SyntaxKind.NamedImports))) { - - symbol = typeChecker.getAliasedSymbol(symbol); - } - } - - // Because name in short-hand property assignment has two different meanings: property name and property value, - // using go-to-definition at such position should go to the variable declaration of the property value rather than - // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition - // is performed at the location of property access, we would like to go to definition of the property in the short-hand - // assignment. This case and others are handled by the following code. - if (node.parent.kind === SyntaxKind.ShorthandPropertyAssignment) { - const shorthandSymbol = typeChecker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - if (!shorthandSymbol) { - return []; - } - - const shorthandDeclarations = shorthandSymbol.getDeclarations(); - const shorthandSymbolKind = getSymbolKind(shorthandSymbol, node); - const shorthandSymbolName = typeChecker.symbolToString(shorthandSymbol); - const shorthandContainerName = typeChecker.symbolToString(symbol.parent, node); - return map(shorthandDeclarations, - declaration => createDefinitionInfo(declaration, shorthandSymbolKind, shorthandSymbolName, shorthandContainerName)); - } - - return getDefinitionFromSymbol(symbol, node); + return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position); } - /// Goto type function getTypeDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - - const node = getTouchingPropertyName(sourceFile, position); - if (node === sourceFile) { - return undefined; - } - - const typeChecker = program.getTypeChecker(); - - const symbol = typeChecker.getSymbolAtLocation(node); - if (!symbol) { - return undefined; - } - - const type = typeChecker.getTypeOfSymbolAtLocation(symbol, node); - if (!type) { - return undefined; - } - - if (type.flags & TypeFlags.Union && !(type.flags & TypeFlags.Enum)) { - const result: DefinitionInfo[] = []; - forEach((type).types, t => { - if (t.symbol) { - addRange(/*to*/ result, /*from*/ getDefinitionFromSymbol(t.symbol, node)); - } - }); - return result; - } - - if (!type.symbol) { - return undefined; - } - - return getDefinitionFromSymbol(type.symbol, node); + return GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position); } function getOccurrencesAtPosition(fileName: string, position: number): ReferenceEntry[] { @@ -5024,628 +951,9 @@ namespace ts { function getDocumentHighlights(fileName: string, position: number, filesToSearch: string[]): DocumentHighlights[] { synchronizeHostData(); - const sourceFilesToSearch = map(filesToSearch, f => program.getSourceFile(f)); const sourceFile = getValidSourceFile(fileName); - - const node = getTouchingWord(sourceFile, position); - if (!node) { - return undefined; - } - - return getSemanticDocumentHighlights(node) || getSyntacticDocumentHighlights(node); - - function getHighlightSpanForNode(node: Node): HighlightSpan { - const start = node.getStart(); - const end = node.getEnd(); - - return { - fileName: sourceFile.fileName, - textSpan: createTextSpanFromBounds(start, end), - kind: HighlightSpanKind.none - }; - } - - function getSemanticDocumentHighlights(node: Node): DocumentHighlights[] { - if (node.kind === SyntaxKind.Identifier || - node.kind === SyntaxKind.ThisKeyword || - node.kind === SyntaxKind.ThisType || - node.kind === SyntaxKind.SuperKeyword || - node.kind === SyntaxKind.StringLiteral || - isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { - - const referencedSymbols = getReferencedSymbolsForNode(node, sourceFilesToSearch, /*findInStrings*/ false, /*findInComments*/ false); - return convertReferencedSymbols(referencedSymbols); - } - - return undefined; - - function convertReferencedSymbols(referencedSymbols: ReferencedSymbol[]): DocumentHighlights[] { - if (!referencedSymbols) { - return undefined; - } - - const fileNameToDocumentHighlights = createMap(); - const result: DocumentHighlights[] = []; - for (const referencedSymbol of referencedSymbols) { - for (const referenceEntry of referencedSymbol.references) { - const fileName = referenceEntry.fileName; - let documentHighlights = fileNameToDocumentHighlights[fileName]; - if (!documentHighlights) { - documentHighlights = { fileName, highlightSpans: [] }; - - fileNameToDocumentHighlights[fileName] = documentHighlights; - result.push(documentHighlights); - } - - documentHighlights.highlightSpans.push({ - textSpan: referenceEntry.textSpan, - kind: referenceEntry.isWriteAccess ? HighlightSpanKind.writtenReference : HighlightSpanKind.reference - }); - } - } - - return result; - } - } - - function getSyntacticDocumentHighlights(node: Node): DocumentHighlights[] { - const fileName = sourceFile.fileName; - - const highlightSpans = getHighlightSpans(node); - if (!highlightSpans || highlightSpans.length === 0) { - return undefined; - } - - return [{ 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): HighlightSpan[] { - if (node) { - switch (node.kind) { - case SyntaxKind.IfKeyword: - case SyntaxKind.ElseKeyword: - if (hasKind(node.parent, SyntaxKind.IfStatement)) { - return getIfElseOccurrences(node.parent); - } - break; - case SyntaxKind.ReturnKeyword: - if (hasKind(node.parent, SyntaxKind.ReturnStatement)) { - return getReturnOccurrences(node.parent); - } - break; - case SyntaxKind.ThrowKeyword: - if (hasKind(node.parent, SyntaxKind.ThrowStatement)) { - return getThrowOccurrences(node.parent); - } - break; - case SyntaxKind.CatchKeyword: - if (hasKind(parent(parent(node)), SyntaxKind.TryStatement)) { - return getTryCatchFinallyOccurrences(node.parent.parent); - } - break; - case SyntaxKind.TryKeyword: - case SyntaxKind.FinallyKeyword: - if (hasKind(parent(node), SyntaxKind.TryStatement)) { - return getTryCatchFinallyOccurrences(node.parent); - } - break; - case SyntaxKind.SwitchKeyword: - if (hasKind(node.parent, SyntaxKind.SwitchStatement)) { - return getSwitchCaseDefaultOccurrences(node.parent); - } - break; - case SyntaxKind.CaseKeyword: - case SyntaxKind.DefaultKeyword: - if (hasKind(parent(parent(parent(node))), SyntaxKind.SwitchStatement)) { - return getSwitchCaseDefaultOccurrences(node.parent.parent.parent); - } - break; - case SyntaxKind.BreakKeyword: - case SyntaxKind.ContinueKeyword: - if (hasKind(node.parent, SyntaxKind.BreakStatement) || hasKind(node.parent, SyntaxKind.ContinueStatement)) { - return getBreakOrContinueStatementOccurrences(node.parent); - } - break; - case SyntaxKind.ForKeyword: - if (hasKind(node.parent, SyntaxKind.ForStatement) || - hasKind(node.parent, SyntaxKind.ForInStatement) || - hasKind(node.parent, SyntaxKind.ForOfStatement)) { - return getLoopBreakContinueOccurrences(node.parent); - } - break; - case SyntaxKind.WhileKeyword: - case SyntaxKind.DoKeyword: - if (hasKind(node.parent, SyntaxKind.WhileStatement) || hasKind(node.parent, SyntaxKind.DoStatement)) { - return getLoopBreakContinueOccurrences(node.parent); - } - break; - case SyntaxKind.ConstructorKeyword: - if (hasKind(node.parent, SyntaxKind.Constructor)) { - return getConstructorOccurrences(node.parent); - } - break; - case SyntaxKind.GetKeyword: - case SyntaxKind.SetKeyword: - if (hasKind(node.parent, SyntaxKind.GetAccessor) || hasKind(node.parent, SyntaxKind.SetAccessor)) { - return getGetAndSetOccurrences(node.parent); - } - break; - default: - if (isModifierKind(node.kind) && node.parent && - (isDeclaration(node.parent) || node.parent.kind === SyntaxKind.VariableStatement)) { - return getModifierOccurrences(node.kind, node.parent); - } - } - } - - return undefined; - } - - /** - * Aggregates all throw-statements within this node *without* crossing - * into function boundaries and try-blocks with catch-clauses. - */ - function aggregateOwnedThrowStatements(node: Node): ThrowStatement[] { - const statementAccumulator: ThrowStatement[] = []; - aggregate(node); - return statementAccumulator; - - function aggregate(node: Node): void { - if (node.kind === SyntaxKind.ThrowStatement) { - statementAccumulator.push(node); - } - else if (node.kind === SyntaxKind.TryStatement) { - const tryStatement = node; - - if (tryStatement.catchClause) { - aggregate(tryStatement.catchClause); - } - else { - // Exceptions thrown within a try block lacking a catch clause - // are "owned" in the current context. - aggregate(tryStatement.tryBlock); - } - - if (tryStatement.finallyBlock) { - aggregate(tryStatement.finallyBlock); - } - } - // Do not cross function boundaries. - else if (!isFunctionLike(node)) { - forEachChild(node, aggregate); - } - } - } - - /** - * For lack of a better name, this function takes a throw statement and returns the - * nearest ancestor that is a try-block (whose try statement has a catch clause), - * function-block, or source file. - */ - function getThrowStatementOwner(throwStatement: ThrowStatement): Node { - let child: Node = throwStatement; - - while (child.parent) { - const parent = child.parent; - - if (isFunctionBlock(parent) || parent.kind === SyntaxKind.SourceFile) { - return parent; - } - - // A throw-statement is only owned by a try-statement if the try-statement has - // a catch clause, and if the throw-statement occurs within the try block. - if (parent.kind === SyntaxKind.TryStatement) { - const tryStatement = parent; - - if (tryStatement.tryBlock === child && tryStatement.catchClause) { - return child; - } - } - - child = parent; - } - - return undefined; - } - - function aggregateAllBreakAndContinueStatements(node: Node): BreakOrContinueStatement[] { - const statementAccumulator: BreakOrContinueStatement[] = []; - aggregate(node); - return statementAccumulator; - - function aggregate(node: Node): void { - if (node.kind === SyntaxKind.BreakStatement || node.kind === SyntaxKind.ContinueStatement) { - statementAccumulator.push(node); - } - // Do not cross function boundaries. - else if (!isFunctionLike(node)) { - forEachChild(node, aggregate); - } - } - } - - function ownsBreakOrContinueStatement(owner: Node, statement: BreakOrContinueStatement): boolean { - const actualOwner = getBreakOrContinueOwner(statement); - - return actualOwner && actualOwner === owner; - } - - function getBreakOrContinueOwner(statement: BreakOrContinueStatement): Node { - for (let node = statement.parent; node; node = node.parent) { - switch (node.kind) { - case SyntaxKind.SwitchStatement: - if (statement.kind === SyntaxKind.ContinueStatement) { - continue; - } - // Fall through. - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.WhileStatement: - case SyntaxKind.DoStatement: - if (!statement.label || isLabeledBy(node, statement.label.text)) { - return node; - } - break; - default: - // Don't cross function boundaries. - if (isFunctionLike(node)) { - return undefined; - } - break; - } - } - - return undefined; - } - - function getModifierOccurrences(modifier: SyntaxKind, declaration: Node): HighlightSpan[] { - const container = declaration.parent; - - // Make sure we only highlight the keyword when it makes sense to do so. - if (isAccessibilityModifier(modifier)) { - if (!(container.kind === SyntaxKind.ClassDeclaration || - container.kind === SyntaxKind.ClassExpression || - (declaration.kind === SyntaxKind.Parameter && hasKind(container, SyntaxKind.Constructor)))) { - return undefined; - } - } - else if (modifier === SyntaxKind.StaticKeyword) { - if (!(container.kind === SyntaxKind.ClassDeclaration || container.kind === SyntaxKind.ClassExpression)) { - return undefined; - } - } - else if (modifier === SyntaxKind.ExportKeyword || modifier === SyntaxKind.DeclareKeyword) { - if (!(container.kind === SyntaxKind.ModuleBlock || container.kind === SyntaxKind.SourceFile)) { - return undefined; - } - } - else if (modifier === SyntaxKind.AbstractKeyword) { - if (!(container.kind === SyntaxKind.ClassDeclaration || declaration.kind === SyntaxKind.ClassDeclaration)) { - return undefined; - } - } - else { - // unsupported modifier - return undefined; - } - - const keywords: Node[] = []; - const modifierFlag: ModifierFlags = getFlagFromModifier(modifier); - - let nodes: Node[]; - switch (container.kind) { - case SyntaxKind.ModuleBlock: - case SyntaxKind.SourceFile: - // Container is either a class declaration or the declaration is a classDeclaration - if (modifierFlag & ModifierFlags.Abstract) { - nodes = ((declaration).members).concat(declaration); - } - else { - nodes = (container).statements; - } - break; - case SyntaxKind.Constructor: - nodes = ((container).parameters).concat( - (container.parent).members); - break; - case SyntaxKind.ClassDeclaration: - case SyntaxKind.ClassExpression: - nodes = (container).members; - - // If we're an accessibility modifier, we're in an instance member and should search - // the constructor's parameter list for instance members as well. - if (modifierFlag & ModifierFlags.AccessibilityModifier) { - const constructor = forEach((container).members, member => { - return member.kind === SyntaxKind.Constructor && member; - }); - - if (constructor) { - nodes = nodes.concat(constructor.parameters); - } - } - else if (modifierFlag & ModifierFlags.Abstract) { - nodes = nodes.concat(container); - } - break; - default: - Debug.fail("Invalid container kind."); - } - - forEach(nodes, node => { - if (getModifierFlags(node) & modifierFlag) { - forEach(node.modifiers, child => pushKeywordIf(keywords, child, modifier)); - } - }); - - return map(keywords, getHighlightSpanForNode); - - function getFlagFromModifier(modifier: SyntaxKind) { - switch (modifier) { - case SyntaxKind.PublicKeyword: - return ModifierFlags.Public; - case SyntaxKind.PrivateKeyword: - return ModifierFlags.Private; - case SyntaxKind.ProtectedKeyword: - return ModifierFlags.Protected; - case SyntaxKind.StaticKeyword: - return ModifierFlags.Static; - case SyntaxKind.ExportKeyword: - return ModifierFlags.Export; - case SyntaxKind.DeclareKeyword: - return ModifierFlags.Ambient; - case SyntaxKind.AbstractKeyword: - return ModifierFlags.Abstract; - default: - Debug.fail(); - } - } - } - - function pushKeywordIf(keywordList: Node[], token: Node, ...expected: SyntaxKind[]): boolean { - if (token && contains(expected, token.kind)) { - keywordList.push(token); - return true; - } - - return false; - } - - function getGetAndSetOccurrences(accessorDeclaration: AccessorDeclaration): HighlightSpan[] { - const keywords: Node[] = []; - - tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.GetAccessor); - tryPushAccessorKeyword(accessorDeclaration.symbol, SyntaxKind.SetAccessor); - - return map(keywords, getHighlightSpanForNode); - - function tryPushAccessorKeyword(accessorSymbol: Symbol, accessorKind: SyntaxKind): void { - const accessor = getDeclarationOfKind(accessorSymbol, accessorKind); - - if (accessor) { - forEach(accessor.getChildren(), child => pushKeywordIf(keywords, child, SyntaxKind.GetKeyword, SyntaxKind.SetKeyword)); - } - } - } - - function getConstructorOccurrences(constructorDeclaration: ConstructorDeclaration): HighlightSpan[] { - const declarations = constructorDeclaration.symbol.getDeclarations(); - - const keywords: Node[] = []; - - forEach(declarations, declaration => { - forEach(declaration.getChildren(), token => { - return pushKeywordIf(keywords, token, SyntaxKind.ConstructorKeyword); - }); - }); - - return map(keywords, getHighlightSpanForNode); - } - - function getLoopBreakContinueOccurrences(loopNode: IterationStatement): HighlightSpan[] { - const keywords: Node[] = []; - - if (pushKeywordIf(keywords, loopNode.getFirstToken(), SyntaxKind.ForKeyword, SyntaxKind.WhileKeyword, SyntaxKind.DoKeyword)) { - // If we succeeded and got a do-while loop, then start looking for a 'while' keyword. - if (loopNode.kind === SyntaxKind.DoStatement) { - const loopTokens = loopNode.getChildren(); - - for (let i = loopTokens.length - 1; i >= 0; i--) { - if (pushKeywordIf(keywords, loopTokens[i], SyntaxKind.WhileKeyword)) { - break; - } - } - } - } - - const breaksAndContinues = aggregateAllBreakAndContinueStatements(loopNode.statement); - - forEach(breaksAndContinues, statement => { - if (ownsBreakOrContinueStatement(loopNode, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword, SyntaxKind.ContinueKeyword); - } - }); - - return map(keywords, getHighlightSpanForNode); - } - - function getBreakOrContinueStatementOccurrences(breakOrContinueStatement: BreakOrContinueStatement): HighlightSpan[] { - const owner = getBreakOrContinueOwner(breakOrContinueStatement); - - if (owner) { - switch (owner.kind) { - case SyntaxKind.ForStatement: - case SyntaxKind.ForInStatement: - case SyntaxKind.ForOfStatement: - case SyntaxKind.DoStatement: - case SyntaxKind.WhileStatement: - return getLoopBreakContinueOccurrences(owner); - case SyntaxKind.SwitchStatement: - return getSwitchCaseDefaultOccurrences(owner); - - } - } - - return undefined; - } - - function getSwitchCaseDefaultOccurrences(switchStatement: SwitchStatement): HighlightSpan[] { - const keywords: Node[] = []; - - pushKeywordIf(keywords, switchStatement.getFirstToken(), SyntaxKind.SwitchKeyword); - - // Go through each clause in the switch statement, collecting the 'case'/'default' keywords. - forEach(switchStatement.caseBlock.clauses, clause => { - pushKeywordIf(keywords, clause.getFirstToken(), SyntaxKind.CaseKeyword, SyntaxKind.DefaultKeyword); - - const breaksAndContinues = aggregateAllBreakAndContinueStatements(clause); - - forEach(breaksAndContinues, statement => { - if (ownsBreakOrContinueStatement(switchStatement, statement)) { - pushKeywordIf(keywords, statement.getFirstToken(), SyntaxKind.BreakKeyword); - } - }); - }); - - return map(keywords, getHighlightSpanForNode); - } - - function getTryCatchFinallyOccurrences(tryStatement: TryStatement): HighlightSpan[] { - const keywords: Node[] = []; - - pushKeywordIf(keywords, tryStatement.getFirstToken(), SyntaxKind.TryKeyword); - - if (tryStatement.catchClause) { - pushKeywordIf(keywords, tryStatement.catchClause.getFirstToken(), SyntaxKind.CatchKeyword); - } - - if (tryStatement.finallyBlock) { - const finallyKeyword = findChildOfKind(tryStatement, SyntaxKind.FinallyKeyword, sourceFile); - pushKeywordIf(keywords, finallyKeyword, SyntaxKind.FinallyKeyword); - } - - return map(keywords, getHighlightSpanForNode); - } - - function getThrowOccurrences(throwStatement: ThrowStatement): HighlightSpan[] { - const owner = getThrowStatementOwner(throwStatement); - - if (!owner) { - return undefined; - } - - const keywords: Node[] = []; - - forEach(aggregateOwnedThrowStatements(owner), throwStatement => { - pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); - }); - - // If the "owner" is a function, then we equate 'return' and 'throw' statements in their - // ability to "jump out" of the function, and include occurrences for both. - if (isFunctionBlock(owner)) { - forEachReturnStatement(owner, returnStatement => { - pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); - }); - } - - return map(keywords, getHighlightSpanForNode); - } - - function getReturnOccurrences(returnStatement: ReturnStatement): HighlightSpan[] { - 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))) { - return undefined; - } - - const keywords: Node[] = []; - forEachReturnStatement(func.body, returnStatement => { - pushKeywordIf(keywords, returnStatement.getFirstToken(), SyntaxKind.ReturnKeyword); - }); - - // Include 'throw' statements that do not occur within a try block. - forEach(aggregateOwnedThrowStatements(func.body), throwStatement => { - pushKeywordIf(keywords, throwStatement.getFirstToken(), SyntaxKind.ThrowKeyword); - }); - - return map(keywords, getHighlightSpanForNode); - } - - function getIfElseOccurrences(ifStatement: IfStatement): 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 result: HighlightSpan[] = []; - - // We'd like to highlight else/ifs together if they are only separated by whitespace - // (i.e. the keywords are separated by no comments, no newlines). - for (let i = 0; i < keywords.length; i++) { - if (keywords[i].kind === SyntaxKind.ElseKeyword && i < keywords.length - 1) { - const elseKeyword = keywords[i]; - const ifKeyword = keywords[i + 1]; // this *should* always be an 'if' keyword. - - let shouldCombindElseAndIf = true; - - // Avoid recalculating getStart() by iterating backwards. - for (let j = ifKeyword.getStart() - 1; j >= elseKeyword.end; j--) { - if (!isWhiteSpaceSingleLine(sourceFile.text.charCodeAt(j))) { - shouldCombindElseAndIf = false; - break; - } - } - - if (shouldCombindElseAndIf) { - result.push({ - fileName: fileName, - textSpan: createTextSpanFromBounds(elseKeyword.getStart(), ifKeyword.end), - kind: HighlightSpanKind.reference - }); - i++; // skip the next keyword - continue; - } - } - - // Ordinary case: just highlight the keyword. - result.push(getHighlightSpanForNode(keywords[i])); - } - - return result; - } - } + return DocumentHighlights.getDocumentHighlights(program.getTypeChecker(), cancellationToken, sourceFile, position, sourceFilesToSearch); } /// References and Occurrences @@ -5675,28 +983,14 @@ namespace ts { } } - function convertReferences(referenceSymbols: ReferencedSymbol[]): ReferenceEntry[] { - if (!referenceSymbols) { - return undefined; - } - - const referenceEntries: ReferenceEntry[] = []; - - for (const referenceSymbol of referenceSymbols) { - addRange(referenceEntries, referenceSymbol.references); - } - - return referenceEntries; - } - function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): RenameLocation[] { const referencedSymbols = findReferencedSymbols(fileName, position, findInStrings, findInComments); - return convertReferences(referencedSymbols); + return FindAllReferences.convertReferences(referencedSymbols); } function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] { const referencedSymbols = findReferencedSymbols(fileName, position, /*findInStrings*/ false, /*findInComments*/ false); - return convertReferences(referencedSymbols); + return FindAllReferences.convertReferences(referencedSymbols); } function findReferences(fileName: string, position: number): ReferencedSymbol[] { @@ -5708,1081 +1002,7 @@ namespace ts { function findReferencedSymbols(fileName: string, position: number, findInStrings: boolean, findInComments: boolean): ReferencedSymbol[] { synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - - const node = getTouchingPropertyName(sourceFile, position, /*includeJsDocComment*/ true); - if (node === sourceFile) { - return undefined; - } - - switch (node.kind) { - case SyntaxKind.NumericLiteral: - if (!isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { - break; - } - // Fallthrough - case SyntaxKind.Identifier: - case SyntaxKind.ThisKeyword: - // case SyntaxKind.SuperKeyword: TODO:GH#9268 - case SyntaxKind.ConstructorKeyword: - case SyntaxKind.StringLiteral: - return getReferencedSymbolsForNode(node, program.getSourceFiles(), findInStrings, findInComments); - } - return undefined; - } - - function isThis(node: Node): boolean { - switch (node.kind) { - case SyntaxKind.ThisKeyword: - // case SyntaxKind.ThisType: TODO: GH#9267 - return true; - case SyntaxKind.Identifier: - // 'this' as a parameter - return (node as Identifier).originalKeywordKind === SyntaxKind.ThisKeyword && node.parent.kind === SyntaxKind.Parameter; - default: - return false; - } - } - - function getReferencedSymbolsForNode(node: Node, sourceFiles: SourceFile[], findInStrings: boolean, findInComments: boolean): ReferencedSymbol[] { - const typeChecker = program.getTypeChecker(); - - // Labels - if (isLabelName(node)) { - if (isJumpStatementTarget(node)) { - const labelDefinition = getTargetLabel((node.parent), (node).text); - // if we have a label definition, look within its statement for references, if not, then - // the label is undefined and we have no results.. - return labelDefinition ? getLabelReferencesInNode(labelDefinition.parent, labelDefinition) : undefined; - } - else { - // it is a label definition and not a target, search within the parent labeledStatement - return getLabelReferencesInNode(node.parent, node); - } - } - - if (isThis(node)) { - return getReferencesForThisKeyword(node, sourceFiles); - } - - if (node.kind === SyntaxKind.SuperKeyword) { - return getReferencesForSuperKeyword(node); - } - - // `getSymbolAtLocation` normally returns the symbol of the class when given the constructor keyword, - // so we have to specify that we want the constructor symbol. - const symbol = typeChecker.getSymbolAtLocation(node); - - if (!symbol && node.kind === SyntaxKind.StringLiteral) { - return getReferencesForStringLiteral(node, sourceFiles); - } - - // Could not find a symbol e.g. unknown identifier - if (!symbol) { - // Can't have references to something that we have no symbol for. - return undefined; - } - - const declarations = symbol.declarations; - - // The symbol was an internal symbol and does not have a declaration e.g. undefined symbol - if (!declarations || !declarations.length) { - return undefined; - } - - let result: ReferencedSymbol[]; - - // Compute the meaning from the location and the symbol it references - const searchMeaning = getIntersectingMeaningFromDeclarations(getMeaningFromLocation(node), declarations); - - // Get the text to search for. - // Note: if this is an external module symbol, the name doesn't include quotes. - const declaredName = stripQuotes(getDeclaredName(typeChecker, symbol, node)); - - // Try to get the smallest valid scope that we can limit our search to; - // otherwise we'll need to search globally (i.e. include each file). - const scope = getSymbolScope(symbol); - - // Maps from a symbol ID to the ReferencedSymbol entry in 'result'. - const symbolToIndex: number[] = []; - - if (scope) { - result = []; - getReferencesInNode(scope, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex); - } - else { - const internedName = getInternedName(symbol, node, declarations); - for (const sourceFile of sourceFiles) { - cancellationToken.throwIfCancellationRequested(); - - const nameTable = getNameTable(sourceFile); - - if (nameTable[internedName] !== undefined) { - result = result || []; - getReferencesInNode(sourceFile, symbol, declaredName, node, searchMeaning, findInStrings, findInComments, result, symbolToIndex); - } - } - } - - return result; - - function getDefinition(symbol: Symbol): ReferencedSymbolDefinitionInfo { - const info = getSymbolDisplayPartsDocumentationAndSymbolKind(symbol, node.getSourceFile(), getContainerNode(node), node); - const name = map(info.displayParts, p => p.text).join(""); - const declarations = symbol.declarations; - if (!declarations || declarations.length === 0) { - return undefined; - } - - return { - containerKind: "", - containerName: "", - name, - kind: info.symbolKind, - fileName: declarations[0].getSourceFile().fileName, - textSpan: createTextSpan(declarations[0].getStart(), 0), - displayParts: info.displayParts - }; - } - - function getAliasSymbolForPropertyNameSymbol(symbol: Symbol, location: Node): Symbol | undefined { - if (symbol.flags & SymbolFlags.Alias) { - // Default import get alias - const defaultImport = getDeclarationOfKind(symbol, SyntaxKind.ImportClause); - if (defaultImport) { - return typeChecker.getAliasedSymbol(symbol); - } - - const importOrExportSpecifier = forEach(symbol.declarations, - declaration => (declaration.kind === SyntaxKind.ImportSpecifier || - declaration.kind === SyntaxKind.ExportSpecifier) ? declaration : undefined); - if (importOrExportSpecifier && - // export { a } - (!importOrExportSpecifier.propertyName || - // export {a as class } where a is location - importOrExportSpecifier.propertyName === location)) { - // If Import specifier -> get alias - // else Export specifier -> get local target - return importOrExportSpecifier.kind === SyntaxKind.ImportSpecifier ? - typeChecker.getAliasedSymbol(symbol) : - typeChecker.getExportSpecifierLocalTargetSymbol(importOrExportSpecifier); - } - } - return undefined; - } - - function followAliasIfNecessary(symbol: Symbol, location: Node): Symbol { - return getAliasSymbolForPropertyNameSymbol(symbol, location) || symbol; - } - - function getPropertySymbolOfDestructuringAssignment(location: Node) { - return isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) && - typeChecker.getPropertySymbolOfDestructuringAssignment(location); - } - - function isObjectBindingPatternElementWithoutPropertyName(symbol: Symbol) { - const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); - return bindingElement && - bindingElement.parent.kind === SyntaxKind.ObjectBindingPattern && - !bindingElement.propertyName; - } - - function getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol: Symbol) { - if (isObjectBindingPatternElementWithoutPropertyName(symbol)) { - const bindingElement = getDeclarationOfKind(symbol, SyntaxKind.BindingElement); - const typeOfPattern = typeChecker.getTypeAtLocation(bindingElement.parent); - return typeOfPattern && typeChecker.getPropertyOfType(typeOfPattern, (bindingElement.name).text); - } - return undefined; - } - - function getInternedName(symbol: Symbol, location: Node, declarations: Declaration[]): string { - // If this is an export or import specifier it could have been renamed using the 'as' syntax. - // If so we want to search for whatever under the cursor. - if (isImportOrExportSpecifierName(location)) { - return location.getText(); - } - - // Try to get the local symbol if we're dealing with an 'export default' - // since that symbol has the "true" name. - const localExportDefaultSymbol = getLocalSymbolForExportDefault(symbol); - symbol = localExportDefaultSymbol || symbol; - - return stripQuotes(symbol.name); - } - - /** - * Determines the smallest scope in which a symbol may have named references. - * Note that not every construct has been accounted for. This function can - * probably be improved. - * - * @returns undefined if the scope cannot be determined, implying that - * a reference to a symbol can occur anywhere. - */ - function getSymbolScope(symbol: Symbol): Node { - // If this is the symbol of a named function expression or named class expression, - // then named references are limited to its own scope. - const valueDeclaration = symbol.valueDeclaration; - if (valueDeclaration && (valueDeclaration.kind === SyntaxKind.FunctionExpression || valueDeclaration.kind === SyntaxKind.ClassExpression)) { - return valueDeclaration; - } - - // If this is private property or method, the scope is the containing class - if (symbol.flags & (SymbolFlags.Property | SymbolFlags.Method)) { - const privateDeclaration = forEach(symbol.getDeclarations(), d => (getModifierFlags(d) & ModifierFlags.Private) ? d : undefined); - if (privateDeclaration) { - return getAncestor(privateDeclaration, SyntaxKind.ClassDeclaration); - } - } - - // If the symbol is an import we would like to find it if we are looking for what it imports. - // So consider it visible outside its declaration scope. - if (symbol.flags & SymbolFlags.Alias) { - return undefined; - } - - // If symbol is of object binding pattern element without property name we would want to - // look for property too and that could be anywhere - if (isObjectBindingPatternElementWithoutPropertyName(symbol)) { - return undefined; - } - - // if this symbol is visible from its parent container, e.g. exported, then bail out - // if symbol correspond to the union property - bail out - if (symbol.parent || (symbol.flags & SymbolFlags.SyntheticProperty)) { - return undefined; - } - - let scope: Node; - - const declarations = symbol.getDeclarations(); - if (declarations) { - for (const declaration of declarations) { - const container = getContainerNode(declaration); - - if (!container) { - return undefined; - } - - if (scope && scope !== container) { - // Different declarations have different containers, bail out - return undefined; - } - - if (container.kind === SyntaxKind.SourceFile && !isExternalModule(container)) { - // This is a global variable and not an external module, any declaration defined - // within this scope is visible outside the file - return undefined; - } - - // The search scope is the container node - scope = container; - } - } - - return scope; - } - - function getPossibleSymbolReferencePositions(sourceFile: SourceFile, symbolName: string, start: number, end: number): number[] { - const positions: number[] = []; - - /// TODO: Cache symbol existence for files to save text search - // Also, need to make this work for unicode escapes. - - // Be resilient in the face of a symbol with no name or zero length name - if (!symbolName || !symbolName.length) { - return positions; - } - - const text = sourceFile.text; - const sourceLength = text.length; - const symbolNameLength = symbolName.length; - - let position = text.indexOf(symbolName, start); - while (position >= 0) { - cancellationToken.throwIfCancellationRequested(); - - // If we are past the end, stop looking - if (position > end) break; - - // We found a match. Make sure it's not part of a larger word (i.e. the char - // before and after it have to be a non-identifier char). - const endPosition = position + symbolNameLength; - - if ((position === 0 || !isIdentifierPart(text.charCodeAt(position - 1), ScriptTarget.Latest)) && - (endPosition === sourceLength || !isIdentifierPart(text.charCodeAt(endPosition), ScriptTarget.Latest))) { - // Found a real match. Keep searching. - positions.push(position); - } - position = text.indexOf(symbolName, position + symbolNameLength + 1); - } - - return positions; - } - - function getLabelReferencesInNode(container: Node, targetLabel: Identifier): ReferencedSymbol[] { - const references: ReferenceEntry[] = []; - const sourceFile = container.getSourceFile(); - const labelName = targetLabel.text; - const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, labelName, container.getStart(), container.getEnd()); - forEach(possiblePositions, position => { - cancellationToken.throwIfCancellationRequested(); - - const node = getTouchingWord(sourceFile, position); - if (!node || node.getWidth() !== labelName.length) { - return; - } - - // Only pick labels that are either the target label, or have a target that is the target label - if (node === targetLabel || - (isJumpStatementTarget(node) && getTargetLabel(node, labelName) === targetLabel)) { - references.push(getReferenceEntryFromNode(node)); - } - }); - - const definition: ReferencedSymbolDefinitionInfo = { - containerKind: "", - containerName: "", - fileName: targetLabel.getSourceFile().fileName, - kind: ScriptElementKind.label, - name: labelName, - textSpan: createTextSpanFromBounds(targetLabel.getStart(), targetLabel.getEnd()), - displayParts: [displayPart(labelName, SymbolDisplayPartKind.text)] - }; - - return [{ definition, references }]; - } - - function isValidReferencePosition(node: Node, searchSymbolName: string): boolean { - if (node) { - // Compare the length so we filter out strict superstrings of the symbol we are looking for - switch (node.kind) { - case SyntaxKind.Identifier: - return node.getWidth() === searchSymbolName.length; - - case SyntaxKind.StringLiteral: - if (isLiteralNameOfPropertyDeclarationOrIndexAccess(node) || - isNameOfExternalModuleImportOrDeclaration(node)) { - // For string literals we have two additional chars for the quotes - return node.getWidth() === searchSymbolName.length + 2; - } - break; - - case SyntaxKind.NumericLiteral: - if (isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) { - return node.getWidth() === searchSymbolName.length; - } - break; - } - } - - return false; - } - - /** Search within node "container" for references for a search value, where the search value is defined as a - * tuple of(searchSymbol, searchText, searchLocation, and searchMeaning). - * searchLocation: a node where the search value - */ - function getReferencesInNode(container: Node, - searchSymbol: Symbol, - searchText: string, - searchLocation: Node, - searchMeaning: SemanticMeaning, - findInStrings: boolean, - findInComments: boolean, - result: ReferencedSymbol[], - symbolToIndex: number[]): void { - - const sourceFile = container.getSourceFile(); - - const start = findInComments ? container.getFullStart() : container.getStart(); - const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, searchText, start, container.getEnd()); - - if (possiblePositions.length) { - // Build the set of symbols to search for, initially it has only the current symbol - const searchSymbols = populateSearchSymbolSet(searchSymbol, searchLocation); - - forEach(possiblePositions, position => { - cancellationToken.throwIfCancellationRequested(); - - const referenceLocation = getTouchingPropertyName(sourceFile, position); - if (!isValidReferencePosition(referenceLocation, searchText)) { - // This wasn't the start of a token. Check to see if it might be a - // match in a comment or string if that's what the caller is asking - // for. - if ((findInStrings && isInString(sourceFile, position)) || - (findInComments && isInNonReferenceComment(sourceFile, position))) { - - // In the case where we're looking inside comments/strings, we don't have - // an actual definition. So just use 'undefined' here. Features like - // 'Rename' won't care (as they ignore the definitions), and features like - // 'FindReferences' will just filter out these results. - result.push({ - definition: undefined, - references: [{ - fileName: sourceFile.fileName, - textSpan: createTextSpan(position, searchText.length), - isWriteAccess: false, - isDefinition: false - }] - }); - } - return; - } - - if (!(getMeaningFromLocation(referenceLocation) & searchMeaning)) { - return; - } - - const referenceSymbol = typeChecker.getSymbolAtLocation(referenceLocation); - if (referenceSymbol) { - const referenceSymbolDeclaration = referenceSymbol.valueDeclaration; - const shorthandValueSymbol = typeChecker.getShorthandAssignmentValueSymbol(referenceSymbolDeclaration); - const relatedSymbol = getRelatedSymbol(searchSymbols, referenceSymbol, referenceLocation, - /*searchLocationIsConstructor*/ searchLocation.kind === SyntaxKind.ConstructorKeyword); - - if (relatedSymbol) { - const referencedSymbol = getReferencedSymbol(relatedSymbol); - referencedSymbol.references.push(getReferenceEntryFromNode(referenceLocation)); - } - /* Because in short-hand property assignment, an identifier which stored as name of the short-hand property assignment - * has two meaning : property name and property value. Therefore when we do findAllReference at the position where - * an identifier is declared, the language service should return the position of the variable declaration as well as - * the position in short-hand property assignment excluding property accessing. However, if we do findAllReference at the - * position of property accessing, the referenceEntry of such position will be handled in the first case. - */ - else if (!(referenceSymbol.flags & SymbolFlags.Transient) && searchSymbols.indexOf(shorthandValueSymbol) >= 0) { - const referencedSymbol = getReferencedSymbol(shorthandValueSymbol); - referencedSymbol.references.push(getReferenceEntryFromNode(referenceSymbolDeclaration.name)); - } - else if (searchLocation.kind === SyntaxKind.ConstructorKeyword) { - findAdditionalConstructorReferences(referenceSymbol, referenceLocation); - } - } - }); - } - - return; - - /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ - function findAdditionalConstructorReferences(referenceSymbol: Symbol, referenceLocation: Node): void { - Debug.assert(isClassLike(searchSymbol.valueDeclaration)); - - const referenceClass = referenceLocation.parent; - if (referenceSymbol === searchSymbol && isClassLike(referenceClass)) { - Debug.assert(referenceClass.name === referenceLocation); - // This is the class declaration containing the constructor. - addReferences(findOwnConstructorCalls(searchSymbol)); - } - else { - // If this class appears in `extends C`, then the extending class' "super" calls are references. - const classExtending = tryGetClassByExtendingIdentifier(referenceLocation); - if (classExtending && isClassLike(classExtending) && followAliasIfNecessary(referenceSymbol, referenceLocation) === searchSymbol) { - addReferences(superConstructorAccesses(classExtending)); - } - } - } - - function addReferences(references: Node[]): void { - if (references.length) { - const referencedSymbol = getReferencedSymbol(searchSymbol); - addRange(referencedSymbol.references, map(references, getReferenceEntryFromNode)); - } - } - - /** `classSymbol` is the class where the constructor was defined. - * Reference the constructor and all calls to `new this()`. - */ - function findOwnConstructorCalls(classSymbol: Symbol): Node[] { - const result: Node[] = []; - - for (const decl of classSymbol.members["__constructor"].declarations) { - Debug.assert(decl.kind === SyntaxKind.Constructor); - const ctrKeyword = decl.getChildAt(0); - Debug.assert(ctrKeyword.kind === SyntaxKind.ConstructorKeyword); - result.push(ctrKeyword); - } - - forEachProperty(classSymbol.exports, member => { - const decl = member.valueDeclaration; - if (decl && decl.kind === SyntaxKind.MethodDeclaration) { - const body = (decl).body; - if (body) { - forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => { - if (isNewExpressionTarget(thisKeyword)) { - result.push(thisKeyword); - } - }); - } - } - }); - - return result; - } - - /** Find references to `super` in the constructor of an extending class. */ - function superConstructorAccesses(cls: ClassLikeDeclaration): Node[] { - const symbol = cls.symbol; - const ctr = symbol.members["__constructor"]; - if (!ctr) { - return []; - } - - const result: Node[] = []; - for (const decl of ctr.declarations) { - Debug.assert(decl.kind === SyntaxKind.Constructor); - const body = (decl).body; - if (body) { - forEachDescendantOfKind(body, SyntaxKind.SuperKeyword, node => { - if (isCallExpressionTarget(node)) { - result.push(node); - } - }); - } - }; - return result; - } - - function getReferencedSymbol(symbol: Symbol): ReferencedSymbol { - const symbolId = getSymbolId(symbol); - let index = symbolToIndex[symbolId]; - if (index === undefined) { - index = result.length; - symbolToIndex[symbolId] = index; - - result.push({ - definition: getDefinition(symbol), - references: [] - }); - } - - return result[index]; - } - } - - function getReferencesForSuperKeyword(superKeyword: Node): ReferencedSymbol[] { - let searchSpaceNode = getSuperContainer(superKeyword, /*stopOnFunctions*/ false); - if (!searchSpaceNode) { - return undefined; - } - // Whether 'super' occurs in a static context within a class. - let staticFlag = ModifierFlags.Static; - - switch (searchSpaceNode.kind) { - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - staticFlag &= getModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class - break; - default: - return undefined; - } - - const references: ReferenceEntry[] = []; - - const sourceFile = searchSpaceNode.getSourceFile(); - const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "super", searchSpaceNode.getStart(), searchSpaceNode.getEnd()); - forEach(possiblePositions, position => { - cancellationToken.throwIfCancellationRequested(); - - const node = getTouchingWord(sourceFile, position); - - if (!node || node.kind !== SyntaxKind.SuperKeyword) { - return; - } - - const container = getSuperContainer(node, /*stopOnFunctions*/ false); - - // If we have a 'super' container, we must have an enclosing class. - // Now make sure the owning class is the same as the search-space - // and has the same static qualifier as the original 'super's owner. - if (container && (ModifierFlags.Static & getModifierFlags(container)) === staticFlag && container.parent.symbol === searchSpaceNode.symbol) { - references.push(getReferenceEntryFromNode(node)); - } - }); - - const definition = getDefinition(searchSpaceNode.symbol); - return [{ definition, references }]; - } - - function getReferencesForThisKeyword(thisOrSuperKeyword: Node, sourceFiles: SourceFile[]): ReferencedSymbol[] { - let searchSpaceNode = getThisContainer(thisOrSuperKeyword, /* includeArrowFunctions */ false); - - // Whether 'this' occurs in a static context within a class. - let staticFlag = ModifierFlags.Static; - - switch (searchSpaceNode.kind) { - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (isObjectLiteralMethod(searchSpaceNode)) { - break; - } - // fall through - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - staticFlag &= getModifierFlags(searchSpaceNode); - searchSpaceNode = searchSpaceNode.parent; // re-assign to be the owning class - break; - case SyntaxKind.SourceFile: - if (isExternalModule(searchSpaceNode)) { - return undefined; - } - // Fall through - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - break; - // Computed properties in classes are not handled here because references to this are illegal, - // so there is no point finding references to them. - default: - return undefined; - } - - const references: ReferenceEntry[] = []; - - let possiblePositions: number[]; - if (searchSpaceNode.kind === SyntaxKind.SourceFile) { - forEach(sourceFiles, sourceFile => { - possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "this", sourceFile.getStart(), sourceFile.getEnd()); - getThisReferencesInFile(sourceFile, sourceFile, possiblePositions, references); - }); - } - else { - const sourceFile = searchSpaceNode.getSourceFile(); - possiblePositions = getPossibleSymbolReferencePositions(sourceFile, "this", searchSpaceNode.getStart(), searchSpaceNode.getEnd()); - getThisReferencesInFile(sourceFile, searchSpaceNode, possiblePositions, references); - } - - const thisOrSuperSymbol = typeChecker.getSymbolAtLocation(thisOrSuperKeyword); - - const displayParts = thisOrSuperSymbol && getSymbolDisplayPartsDocumentationAndSymbolKind( - thisOrSuperSymbol, thisOrSuperKeyword.getSourceFile(), getContainerNode(thisOrSuperKeyword), thisOrSuperKeyword).displayParts; - - return [{ - definition: { - containerKind: "", - containerName: "", - fileName: node.getSourceFile().fileName, - kind: ScriptElementKind.variableElement, - name: "this", - textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), - displayParts - }, - references: references - }]; - - function getThisReferencesInFile(sourceFile: SourceFile, searchSpaceNode: Node, possiblePositions: number[], result: ReferenceEntry[]): void { - forEach(possiblePositions, position => { - cancellationToken.throwIfCancellationRequested(); - - const node = getTouchingWord(sourceFile, position); - if (!node || !isThis(node)) { - return; - } - - const container = getThisContainer(node, /* includeArrowFunctions */ false); - - switch (searchSpaceNode.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.FunctionDeclaration: - if (searchSpaceNode.symbol === container.symbol) { - result.push(getReferenceEntryFromNode(node)); - } - break; - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - if (isObjectLiteralMethod(searchSpaceNode) && searchSpaceNode.symbol === container.symbol) { - result.push(getReferenceEntryFromNode(node)); - } - break; - case SyntaxKind.ClassExpression: - case SyntaxKind.ClassDeclaration: - // Make sure the container belongs to the same class - // and has the appropriate static modifier from the original container. - if (container.parent && searchSpaceNode.symbol === container.parent.symbol && (getModifierFlags(container) & ModifierFlags.Static) === staticFlag) { - result.push(getReferenceEntryFromNode(node)); - } - break; - case SyntaxKind.SourceFile: - if (container.kind === SyntaxKind.SourceFile && !isExternalModule(container)) { - result.push(getReferenceEntryFromNode(node)); - } - break; - } - }); - } - } - - - function getReferencesForStringLiteral(node: StringLiteral, sourceFiles: SourceFile[]): ReferencedSymbol[] { - const typeChecker = program.getTypeChecker(); - const type = getStringLiteralTypeForNode(node, typeChecker); - - if (!type) { - // nothing to do here. moving on - return undefined; - } - - const references: ReferenceEntry[] = []; - - for (const sourceFile of sourceFiles) { - const possiblePositions = getPossibleSymbolReferencePositions(sourceFile, type.text, sourceFile.getStart(), sourceFile.getEnd()); - getReferencesForStringLiteralInFile(sourceFile, type, possiblePositions, references); - } - - return [{ - definition: { - containerKind: "", - containerName: "", - fileName: node.getSourceFile().fileName, - kind: ScriptElementKind.variableElement, - name: type.text, - textSpan: createTextSpanFromBounds(node.getStart(), node.getEnd()), - displayParts: [displayPart(getTextOfNode(node), SymbolDisplayPartKind.stringLiteral)] - }, - references: references - }]; - - function getReferencesForStringLiteralInFile(sourceFile: SourceFile, searchType: Type, possiblePositions: number[], references: ReferenceEntry[]): void { - for (const position of possiblePositions) { - cancellationToken.throwIfCancellationRequested(); - - const node = getTouchingWord(sourceFile, position); - if (!node || node.kind !== SyntaxKind.StringLiteral) { - return; - } - - const type = getStringLiteralTypeForNode(node, typeChecker); - if (type === searchType) { - references.push(getReferenceEntryFromNode(node)); - } - } - } - } - - function populateSearchSymbolSet(symbol: Symbol, location: Node): Symbol[] { - // The search set contains at least the current symbol - let result = [symbol]; - - // If the location is name of property symbol from object literal destructuring pattern - // Search the property symbol - // for ( { property: p2 } of elems) { } - const containingObjectLiteralElement = getContainingObjectLiteralElement(location); - if (containingObjectLiteralElement && containingObjectLiteralElement.kind !== SyntaxKind.ShorthandPropertyAssignment) { - const propertySymbol = getPropertySymbolOfDestructuringAssignment(location); - if (propertySymbol) { - result.push(propertySymbol); - } - } - - // If the symbol is an alias, add what it aliases to the list - // import {a} from "mod"; - // export {a} - // If the symbol is an alias to default declaration, add what it aliases to the list - // declare "mod" { export default class B { } } - // import B from "mod"; - //// For export specifiers, the exported name can be referring to a local symbol, e.g.: - //// import {a} from "mod"; - //// export {a as somethingElse} - //// We want the *local* declaration of 'a' as declared in the import, - //// *not* as declared within "mod" (or farther) - const aliasSymbol = getAliasSymbolForPropertyNameSymbol(symbol, location); - if (aliasSymbol) { - result = result.concat(populateSearchSymbolSet(aliasSymbol, location)); - } - - // If the location is in a context sensitive location (i.e. in an object literal) try - // to get a contextual type for it, and add the property symbol from the contextual - // type to the search set - if (containingObjectLiteralElement) { - forEach(getPropertySymbolsFromContextualType(containingObjectLiteralElement), contextualSymbol => { - addRange(result, typeChecker.getRootSymbols(contextualSymbol)); - }); - - /* Because in short-hand property assignment, location has two meaning : property name and as value of the property - * When we do findAllReference at the position of the short-hand property assignment, we would want to have references to position of - * property name and variable declaration of the identifier. - * Like in below example, when querying for all references for an identifier 'name', of the property assignment, the language service - * should show both 'name' in 'obj' and 'name' in variable declaration - * const name = "Foo"; - * const obj = { name }; - * In order to do that, we will populate the search set with the value symbol of the identifier as a value of the property assignment - * so that when matching with potential reference symbol, both symbols from property declaration and variable declaration - * will be included correctly. - */ - const shorthandValueSymbol = typeChecker.getShorthandAssignmentValueSymbol(location.parent); - if (shorthandValueSymbol) { - result.push(shorthandValueSymbol); - } - } - - // If the symbol.valueDeclaration is a property parameter declaration, - // we should include both parameter declaration symbol and property declaration symbol - // Parameter Declaration symbol is only visible within function scope, so the symbol is stored in constructor.locals. - // Property Declaration symbol is a member of the class, so the symbol is stored in its class Declaration.symbol.members - if (symbol.valueDeclaration && symbol.valueDeclaration.kind === SyntaxKind.Parameter && - isParameterPropertyDeclaration(symbol.valueDeclaration)) { - result = result.concat(typeChecker.getSymbolsOfParameterPropertyDeclaration(symbol.valueDeclaration, symbol.name)); - } - - // If this is symbol of binding element without propertyName declaration in Object binding pattern - // Include the property in the search - const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(symbol); - if (bindingElementPropertySymbol) { - result.push(bindingElementPropertySymbol); - } - - // If this is a union property, add all the symbols from all its source symbols in all unioned types. - // If the symbol is an instantiation from a another symbol (e.g. widened symbol) , add the root the list - forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { - if (rootSymbol !== symbol) { - result.push(rootSymbol); - } - - // Add symbol of properties/methods of the same name in base classes and implemented interfaces definitions - if (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.getName(), result, /*previousIterationSymbolsCache*/ createMap()); - } - }); - - return result; - } - - /** - * Find symbol of the given property-name and add the symbol to the given result array - * @param symbol a symbol to start searching for the given propertyName - * @param propertyName a name of property to search for - * @param result an array of symbol of found property symbols - * @param previousIterationSymbolsCache a cache of symbol from previous iterations of calling this function to prevent infinite revisiting of the same symbol. - * The value of previousIterationSymbol is undefined when the function is first called. - */ - function getPropertySymbolsFromBaseTypes(symbol: Symbol, propertyName: string, result: Symbol[], - previousIterationSymbolsCache: SymbolTable): void { - if (!symbol) { - return; - } - - // If the current symbol is the same as the previous-iteration symbol, we can just return the symbol that has already been visited - // This is particularly important for the following cases, so that we do not infinitely visit the same symbol. - // For example: - // interface C extends C { - // /*findRef*/propName: string; - // } - // The first time getPropertySymbolsFromBaseTypes is called when finding-all-references at propName, - // the symbol argument will be the symbol of an interface "C" and previousIterationSymbol is undefined, - // the function will add any found symbol of the property-name, then its sub-routine will call - // getPropertySymbolsFromBaseTypes again to walk up any base types to prevent revisiting already - // visited symbol, interface "C", the sub-routine will pass the current symbol as previousIterationSymbol. - if (symbol.name in previousIterationSymbolsCache) { - return; - } - - if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - forEach(symbol.getDeclarations(), declaration => { - if (isClassLike(declaration)) { - getPropertySymbolFromTypeReference(getClassExtendsHeritageClauseElement(declaration)); - forEach(getClassImplementsHeritageClauseElements(declaration), getPropertySymbolFromTypeReference); - } - else if (declaration.kind === SyntaxKind.InterfaceDeclaration) { - forEach(getInterfaceBaseTypeNodes(declaration), getPropertySymbolFromTypeReference); - } - }); - } - return; - - function getPropertySymbolFromTypeReference(typeReference: ExpressionWithTypeArguments) { - if (typeReference) { - const type = typeChecker.getTypeAtLocation(typeReference); - if (type) { - const propertySymbol = typeChecker.getPropertyOfType(type, propertyName); - if (propertySymbol) { - result.push(...typeChecker.getRootSymbols(propertySymbol)); - } - - // Visit the typeReference as well to see if it directly or indirectly use that property - previousIterationSymbolsCache[symbol.name] = symbol; - getPropertySymbolsFromBaseTypes(type.symbol, propertyName, result, previousIterationSymbolsCache); - } - } - } - } - - function getRelatedSymbol(searchSymbols: Symbol[], referenceSymbol: Symbol, referenceLocation: Node, searchLocationIsConstructor: boolean): Symbol | undefined { - if (contains(searchSymbols, referenceSymbol)) { - // If we are searching for constructor uses, they must be 'new' expressions. - return (!searchLocationIsConstructor || isNewExpressionTarget(referenceLocation)) && referenceSymbol; - } - - // If the reference symbol is an alias, check if what it is aliasing is one of the search - // symbols but by looking up for related symbol of this alias so it can handle multiple level of indirectness. - const aliasSymbol = getAliasSymbolForPropertyNameSymbol(referenceSymbol, referenceLocation); - if (aliasSymbol) { - return getRelatedSymbol(searchSymbols, aliasSymbol, referenceLocation, searchLocationIsConstructor); - } - - // If the reference location is in an object literal, try to get the contextual type for the - // object literal, lookup the property symbol in the contextual type, and use this symbol to - // compare to our searchSymbol - const containingObjectLiteralElement = getContainingObjectLiteralElement(referenceLocation); - if (containingObjectLiteralElement) { - const contextualSymbol = forEach(getPropertySymbolsFromContextualType(containingObjectLiteralElement), contextualSymbol => { - return forEach(typeChecker.getRootSymbols(contextualSymbol), s => searchSymbols.indexOf(s) >= 0 ? s : undefined); - }); - - if (contextualSymbol) { - return contextualSymbol; - } - - // If the reference location is the name of property from object literal destructuring pattern - // Get the property symbol from the object literal's type and look if thats the search symbol - // In below eg. get 'property' from type of elems iterating type - // for ( { property: p2 } of elems) { } - const propertySymbol = getPropertySymbolOfDestructuringAssignment(referenceLocation); - if (propertySymbol && searchSymbols.indexOf(propertySymbol) >= 0) { - return propertySymbol; - } - } - - // If the reference location is the binding element and doesn't have property name - // then include the binding element in the related symbols - // let { a } : { a }; - const bindingElementPropertySymbol = getPropertySymbolOfObjectBindingPatternWithoutPropertyName(referenceSymbol); - if (bindingElementPropertySymbol && searchSymbols.indexOf(bindingElementPropertySymbol) >= 0) { - return bindingElementPropertySymbol; - } - - // Unwrap symbols to get to the root (e.g. transient symbols as a result of widening) - // Or a union property, use its underlying unioned symbols - return forEach(typeChecker.getRootSymbols(referenceSymbol), rootSymbol => { - // if it is in the list, then we are done - if (searchSymbols.indexOf(rootSymbol) >= 0) { - return rootSymbol; - } - - // Finally, try all properties with the same name in any type the containing type extended or implemented, and - // see if any is in the list - if (rootSymbol.parent && rootSymbol.parent.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { - const result: Symbol[] = []; - getPropertySymbolsFromBaseTypes(rootSymbol.parent, rootSymbol.getName(), result, /*previousIterationSymbolsCache*/ createMap()); - return forEach(result, s => searchSymbols.indexOf(s) >= 0 ? s : undefined); - } - - return undefined; - }); - } - - function getNameFromObjectLiteralElement(node: ObjectLiteralElement) { - if (node.name.kind === SyntaxKind.ComputedPropertyName) { - const nameExpression = (node.name).expression; - // treat computed property names where expression is string/numeric literal as just string/numeric literal - if (isStringOrNumericLiteral(nameExpression.kind)) { - return (nameExpression).text; - } - return undefined; - } - return (node.name).text; - } - - function getPropertySymbolsFromContextualType(node: ObjectLiteralElement): Symbol[] { - const objectLiteral = node.parent; - const contextualType = typeChecker.getContextualType(objectLiteral); - const name = getNameFromObjectLiteralElement(node); - if (name && contextualType) { - const result: Symbol[] = []; - const symbol = contextualType.getProperty(name); - if (symbol) { - result.push(symbol); - } - - if (contextualType.flags & TypeFlags.Union) { - forEach((contextualType).types, t => { - const symbol = t.getProperty(name); - if (symbol) { - result.push(symbol); - } - }); - } - return result; - } - return undefined; - } - - /** Given an initial searchMeaning, extracted from a location, widen the search scope based on the declarations - * of the corresponding symbol. e.g. if we are searching for "Foo" in value position, but "Foo" references a class - * then we need to widen the search to include type positions as well. - * On the contrary, if we are searching for "Bar" in type position and we trace bar to an interface, and an uninstantiated - * module, we want to keep the search limited to only types, as the two declarations (interface and uninstantiated module) - * do not intersect in any of the three spaces. - */ - function getIntersectingMeaningFromDeclarations(meaning: SemanticMeaning, declarations: Declaration[]): SemanticMeaning { - if (declarations) { - let lastIterationMeaning: SemanticMeaning; - do { - // The result is order-sensitive, for instance if initialMeaning === Namespace, and declarations = [class, instantiated module] - // we need to consider both as they initialMeaning intersects with the module in the namespace space, and the module - // intersects with the class in the value space. - // To achieve that we will keep iterating until the result stabilizes. - - // Remember the last meaning - lastIterationMeaning = meaning; - - for (const declaration of declarations) { - const declarationMeaning = getMeaningFromDeclaration(declaration); - - if (declarationMeaning & meaning) { - meaning |= declarationMeaning; - } - } - } - while (meaning !== lastIterationMeaning); - } - return meaning; - } - } - - function getReferenceEntryFromNode(node: Node): ReferenceEntry { - let start = node.getStart(); - let end = node.getEnd(); - - if (node.kind === SyntaxKind.StringLiteral) { - start += 1; - end -= 1; - } - - return { - fileName: node.getSourceFile().fileName, - textSpan: createTextSpanFromBounds(start, end), - isWriteAccess: isWriteAccess(node), - isDefinition: isDeclarationName(node) || isLiteralComputedPropertyDeclarationName(node) - }; - } - - /** A node is considered a writeAccess iff it is a name of a declaration or a target of an assignment */ - function isWriteAccess(node: Node): boolean { - if (node.kind === SyntaxKind.Identifier && isDeclarationName(node)) { - return true; - } - - const parent = node.parent; - if (parent) { - if (parent.kind === SyntaxKind.PostfixUnaryExpression || parent.kind === SyntaxKind.PrefixUnaryExpression) { - return true; - } - else if (parent.kind === SyntaxKind.BinaryExpression && (parent).left === node) { - const operator = (parent).operatorToken.kind; - return SyntaxKind.FirstAssignment <= operator && operator <= SyntaxKind.LastAssignment; - } - } - - return false; + return FindAllReferences.findReferencedSymbols(program.getTypeChecker(), cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position, findInStrings, findInComments); } /// NavigateTo @@ -6814,156 +1034,6 @@ namespace ts { }; } - function getMeaningFromDeclaration(node: Node): SemanticMeaning { - switch (node.kind) { - case SyntaxKind.Parameter: - case SyntaxKind.VariableDeclaration: - case SyntaxKind.BindingElement: - case SyntaxKind.PropertyDeclaration: - case SyntaxKind.PropertySignature: - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.EnumMember: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.MethodSignature: - case SyntaxKind.Constructor: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - case SyntaxKind.CatchClause: - return SemanticMeaning.Value; - - case SyntaxKind.TypeParameter: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.TypeAliasDeclaration: - case SyntaxKind.TypeLiteral: - return SemanticMeaning.Type; - - case SyntaxKind.ClassDeclaration: - case SyntaxKind.EnumDeclaration: - return SemanticMeaning.Value | SemanticMeaning.Type; - - case SyntaxKind.ModuleDeclaration: - if (isAmbientModule(node)) { - return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - else if (getModuleInstanceState(node) === ModuleInstanceState.Instantiated) { - return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - else { - return SemanticMeaning.Namespace; - } - - case SyntaxKind.NamedImports: - case SyntaxKind.ImportSpecifier: - case SyntaxKind.ImportEqualsDeclaration: - case SyntaxKind.ImportDeclaration: - case SyntaxKind.ExportAssignment: - case SyntaxKind.ExportDeclaration: - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; - - // An external module can be a Value - case SyntaxKind.SourceFile: - return SemanticMeaning.Namespace | SemanticMeaning.Value; - } - - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; - } - - function isTypeReference(node: Node): boolean { - if (isRightSideOfQualifiedNameOrPropertyAccess(node)) { - node = node.parent; - } - - return node.parent.kind === SyntaxKind.TypeReference || - (node.parent.kind === SyntaxKind.ExpressionWithTypeArguments && !isExpressionWithTypeArgumentsInClassExtendsClause(node.parent)) || - (node.kind === SyntaxKind.ThisKeyword && !isPartOfExpression(node)) || - node.kind === SyntaxKind.ThisType; - } - - function isNamespaceReference(node: Node): boolean { - return isQualifiedNameNamespaceReference(node) || isPropertyAccessNamespaceReference(node); - } - - function isPropertyAccessNamespaceReference(node: Node): boolean { - let root = node; - let isLastClause = true; - if (root.parent.kind === SyntaxKind.PropertyAccessExpression) { - while (root.parent && root.parent.kind === SyntaxKind.PropertyAccessExpression) { - root = root.parent; - } - - isLastClause = (root).name === node; - } - - if (!isLastClause && root.parent.kind === SyntaxKind.ExpressionWithTypeArguments && root.parent.parent.kind === SyntaxKind.HeritageClause) { - const decl = root.parent.parent.parent; - return (decl.kind === SyntaxKind.ClassDeclaration && (root.parent.parent).token === SyntaxKind.ImplementsKeyword) || - (decl.kind === SyntaxKind.InterfaceDeclaration && (root.parent.parent).token === SyntaxKind.ExtendsKeyword); - } - - return false; - } - - function isQualifiedNameNamespaceReference(node: Node): boolean { - let root = node; - let isLastClause = true; - if (root.parent.kind === SyntaxKind.QualifiedName) { - while (root.parent && root.parent.kind === SyntaxKind.QualifiedName) { - root = root.parent; - } - - isLastClause = (root).right === node; - } - - return root.parent.kind === SyntaxKind.TypeReference && !isLastClause; - } - - function isInRightSideOfImport(node: Node) { - while (node.parent.kind === SyntaxKind.QualifiedName) { - node = node.parent; - } - return isInternalModuleImportEqualsDeclaration(node.parent) && (node.parent).moduleReference === node; - } - - function getMeaningFromRightHandSideOfImportEquals(node: Node) { - Debug.assert(node.kind === SyntaxKind.Identifier); - - // import a = |b|; // Namespace - // import a = |b.c|; // Value, type, namespace - // import a = |b.c|.d; // Namespace - - if (node.parent.kind === SyntaxKind.QualifiedName && - (node.parent).right === node && - node.parent.parent.kind === SyntaxKind.ImportEqualsDeclaration) { - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; - } - return SemanticMeaning.Namespace; - } - - function getMeaningFromLocation(node: Node): SemanticMeaning { - if (node.parent.kind === SyntaxKind.ExportAssignment) { - return SemanticMeaning.Value | SemanticMeaning.Type | SemanticMeaning.Namespace; - } - else if (isInRightSideOfImport(node)) { - return getMeaningFromRightHandSideOfImportEquals(node); - } - else if (isDeclarationName(node)) { - return getMeaningFromDeclaration(node.parent); - } - else if (isTypeReference(node)) { - return SemanticMeaning.Type; - } - else if (isNamespaceReference(node)) { - return SemanticMeaning.Namespace; - } - else { - return SemanticMeaning.Value; - } - } - // Signature help /** * This is a semantic operation. @@ -7052,529 +1122,23 @@ namespace ts { } function getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { - return convertClassifications(getEncodedSemanticClassifications(fileName, span)); - } - - function checkForClassificationCancellation(kind: SyntaxKind) { - // We don't want to actually call back into our host on every node to find out if we've - // been canceled. That would be an enormous amount of chattyness, along with the all - // the overhead of marshalling the data to/from the host. So instead we pick a few - // reasonable node kinds to bother checking on. These node kinds represent high level - // constructs that we would expect to see commonly, but just at a far less frequent - // interval. - // - // For example, in checker.ts (around 750k) we only have around 600 of these constructs. - // That means we're calling back into the host around every 1.2k of the file we process. - // Lib.d.ts has similar numbers. - switch (kind) { - case SyntaxKind.ModuleDeclaration: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.InterfaceDeclaration: - case SyntaxKind.FunctionDeclaration: - cancellationToken.throwIfCancellationRequested(); - } + synchronizeHostData(); + return Classifier.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); } function getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications { synchronizeHostData(); - - const sourceFile = getValidSourceFile(fileName); - const typeChecker = program.getTypeChecker(); - - const result: number[] = []; - const classifiableNames = program.getClassifiableNames(); - processNode(sourceFile); - - return { spans: result, endOfLineState: EndOfLineState.None }; - - function pushClassification(start: number, length: number, type: ClassificationType) { - result.push(start); - result.push(length); - result.push(type); - } - - function classifySymbol(symbol: Symbol, meaningAtPosition: SemanticMeaning): ClassificationType { - const flags = symbol.getFlags(); - if ((flags & SymbolFlags.Classifiable) === SymbolFlags.None) { - return; - } - - if (flags & SymbolFlags.Class) { - return ClassificationType.className; - } - else if (flags & SymbolFlags.Enum) { - return ClassificationType.enumName; - } - else if (flags & SymbolFlags.TypeAlias) { - return ClassificationType.typeAliasName; - } - else if (meaningAtPosition & SemanticMeaning.Type) { - if (flags & SymbolFlags.Interface) { - return ClassificationType.interfaceName; - } - else if (flags & SymbolFlags.TypeParameter) { - return ClassificationType.typeParameterName; - } - } - else if (flags & SymbolFlags.Module) { - // Only classify a module as such if - // - It appears in a namespace context. - // - There exists a module declaration which actually impacts the value side. - if (meaningAtPosition & SemanticMeaning.Namespace || - (meaningAtPosition & SemanticMeaning.Value && hasValueSideModule(symbol))) { - return ClassificationType.moduleName; - } - } - - return undefined; - - /** - * Returns true if there exists a module that introduces entities on the value side. - */ - function hasValueSideModule(symbol: Symbol): boolean { - return forEach(symbol.declarations, declaration => { - return declaration.kind === SyntaxKind.ModuleDeclaration && - getModuleInstanceState(declaration) === ModuleInstanceState.Instantiated; - }); - } - } - - function processNode(node: Node) { - // Only walk into nodes that intersect the requested span. - if (node && textSpanIntersectsWith(span, node.getFullStart(), node.getFullWidth())) { - const kind = node.kind; - checkForClassificationCancellation(kind); - - if (kind === SyntaxKind.Identifier && !nodeIsMissing(node)) { - const identifier = node; - - // Only bother calling into the typechecker if this is an identifier that - // could possibly resolve to a type name. This makes classification run - // in a third of the time it would normally take. - if (classifiableNames[identifier.text]) { - const symbol = typeChecker.getSymbolAtLocation(node); - if (symbol) { - const type = classifySymbol(symbol, getMeaningFromLocation(node)); - if (type) { - pushClassification(node.getStart(), node.getWidth(), type); - } - } - } - } - - forEachChild(node, processNode); - } - } - } - - function getClassificationTypeName(type: ClassificationType) { - switch (type) { - case ClassificationType.comment: return ClassificationTypeNames.comment; - case ClassificationType.identifier: return ClassificationTypeNames.identifier; - case ClassificationType.keyword: return ClassificationTypeNames.keyword; - case ClassificationType.numericLiteral: return ClassificationTypeNames.numericLiteral; - case ClassificationType.operator: return ClassificationTypeNames.operator; - case ClassificationType.stringLiteral: return ClassificationTypeNames.stringLiteral; - case ClassificationType.whiteSpace: return ClassificationTypeNames.whiteSpace; - case ClassificationType.text: return ClassificationTypeNames.text; - case ClassificationType.punctuation: return ClassificationTypeNames.punctuation; - case ClassificationType.className: return ClassificationTypeNames.className; - case ClassificationType.enumName: return ClassificationTypeNames.enumName; - case ClassificationType.interfaceName: return ClassificationTypeNames.interfaceName; - case ClassificationType.moduleName: return ClassificationTypeNames.moduleName; - case ClassificationType.typeParameterName: return ClassificationTypeNames.typeParameterName; - case ClassificationType.typeAliasName: return ClassificationTypeNames.typeAliasName; - case ClassificationType.parameterName: return ClassificationTypeNames.parameterName; - case ClassificationType.docCommentTagName: return ClassificationTypeNames.docCommentTagName; - case ClassificationType.jsxOpenTagName: return ClassificationTypeNames.jsxOpenTagName; - case ClassificationType.jsxCloseTagName: return ClassificationTypeNames.jsxCloseTagName; - case ClassificationType.jsxSelfClosingTagName: return ClassificationTypeNames.jsxSelfClosingTagName; - case ClassificationType.jsxAttribute: return ClassificationTypeNames.jsxAttribute; - case ClassificationType.jsxText: return ClassificationTypeNames.jsxText; - case ClassificationType.jsxAttributeStringLiteralValue: return ClassificationTypeNames.jsxAttributeStringLiteralValue; - } - } - - function convertClassifications(classifications: Classifications): ClassifiedSpan[] { - Debug.assert(classifications.spans.length % 3 === 0); - const dense = classifications.spans; - const result: ClassifiedSpan[] = []; - for (let i = 0, n = dense.length; i < n; i += 3) { - result.push({ - textSpan: createTextSpan(dense[i], dense[i + 1]), - classificationType: getClassificationTypeName(dense[i + 2]) - }); - } - - return result; + return Classifier.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span); } function getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] { - return convertClassifications(getEncodedSyntacticClassifications(fileName, span)); + // doesn't use compiler - no need to synchronize with host + return Classifier.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); } function getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications { // doesn't use compiler - no need to synchronize with host - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - const spanStart = span.start; - const spanLength = span.length; - - // Make a scanner we can get trivia from. - const triviaScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, sourceFile.languageVariant, sourceFile.text); - const mergeConflictScanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false, sourceFile.languageVariant, sourceFile.text); - - const result: number[] = []; - processElement(sourceFile); - - return { spans: result, endOfLineState: EndOfLineState.None }; - - function pushClassification(start: number, length: number, type: ClassificationType) { - result.push(start); - result.push(length); - result.push(type); - } - - function classifyLeadingTriviaAndGetTokenStart(token: Node): number { - triviaScanner.setTextPos(token.pos); - while (true) { - const start = triviaScanner.getTextPos(); - // only bother scanning if we have something that could be trivia. - if (!couldStartTrivia(sourceFile.text, start)) { - return start; - } - - const kind = triviaScanner.scan(); - const end = triviaScanner.getTextPos(); - const width = end - start; - - // The moment we get something that isn't trivia, then stop processing. - if (!isTrivia(kind)) { - return start; - } - - // Don't bother with newlines/whitespace. - if (kind === SyntaxKind.NewLineTrivia || kind === SyntaxKind.WhitespaceTrivia) { - continue; - } - - // Only bother with the trivia if it at least intersects the span of interest. - if (isComment(kind)) { - classifyComment(token, kind, start, width); - - // Classifying a comment might cause us to reuse the trivia scanner - // (because of jsdoc comments). So after we classify the comment make - // sure we set the scanner position back to where it needs to be. - triviaScanner.setTextPos(end); - continue; - } - - if (kind === SyntaxKind.ConflictMarkerTrivia) { - const text = sourceFile.text; - const ch = text.charCodeAt(start); - - // for the <<<<<<< and >>>>>>> markers, we just add them in as comments - // in the classification stream. - if (ch === CharacterCodes.lessThan || ch === CharacterCodes.greaterThan) { - pushClassification(start, width, ClassificationType.comment); - continue; - } - - // for the ======== add a comment for the first line, and then lex all - // subsequent lines up until the end of the conflict marker. - Debug.assert(ch === CharacterCodes.equals); - classifyDisabledMergeCode(text, start, end); - } - } - } - - function classifyComment(token: Node, kind: SyntaxKind, start: number, width: number) { - if (kind === SyntaxKind.MultiLineCommentTrivia) { - // See if this is a doc comment. If so, we'll classify certain portions of it - // specially. - const docCommentAndDiagnostics = parseIsolatedJSDocComment(sourceFile.text, start, width); - if (docCommentAndDiagnostics && docCommentAndDiagnostics.jsDocComment) { - docCommentAndDiagnostics.jsDocComment.parent = token; - classifyJSDocComment(docCommentAndDiagnostics.jsDocComment); - return; - } - } - - // Simple comment. Just add as is. - pushCommentRange(start, width); - } - - function pushCommentRange(start: number, width: number) { - pushClassification(start, width, ClassificationType.comment); - } - - function classifyJSDocComment(docComment: JSDocComment) { - let pos = docComment.pos; - - for (const tag of docComment.tags) { - // As we walk through each tag, classify the portion of text from the end of - // the last tag (or the start of the entire doc comment) as 'comment'. - if (tag.pos !== pos) { - pushCommentRange(pos, tag.pos - pos); - } - - pushClassification(tag.atToken.pos, tag.atToken.end - tag.atToken.pos, ClassificationType.punctuation); - pushClassification(tag.tagName.pos, tag.tagName.end - tag.tagName.pos, ClassificationType.docCommentTagName); - - pos = tag.tagName.end; - - switch (tag.kind) { - case SyntaxKind.JSDocParameterTag: - processJSDocParameterTag(tag); - break; - case SyntaxKind.JSDocTemplateTag: - processJSDocTemplateTag(tag); - break; - case SyntaxKind.JSDocTypeTag: - processElement((tag).typeExpression); - break; - case SyntaxKind.JSDocReturnTag: - processElement((tag).typeExpression); - break; - } - - pos = tag.end; - } - - if (pos !== docComment.end) { - pushCommentRange(pos, docComment.end - pos); - } - - return; - - function processJSDocParameterTag(tag: JSDocParameterTag) { - if (tag.preParameterName) { - pushCommentRange(pos, tag.preParameterName.pos - pos); - pushClassification(tag.preParameterName.pos, tag.preParameterName.end - tag.preParameterName.pos, ClassificationType.parameterName); - pos = tag.preParameterName.end; - } - - if (tag.typeExpression) { - pushCommentRange(pos, tag.typeExpression.pos - pos); - processElement(tag.typeExpression); - pos = tag.typeExpression.end; - } - - if (tag.postParameterName) { - pushCommentRange(pos, tag.postParameterName.pos - pos); - pushClassification(tag.postParameterName.pos, tag.postParameterName.end - tag.postParameterName.pos, ClassificationType.parameterName); - pos = tag.postParameterName.end; - } - } - } - - function processJSDocTemplateTag(tag: JSDocTemplateTag) { - for (const child of tag.getChildren()) { - processElement(child); - } - } - - function classifyDisabledMergeCode(text: string, start: number, end: number) { - // Classify the line that the ======= marker is on as a comment. Then just lex - // all further tokens and add them to the result. - let i: number; - for (i = start; i < end; i++) { - if (isLineBreak(text.charCodeAt(i))) { - break; - } - } - pushClassification(start, i - start, ClassificationType.comment); - mergeConflictScanner.setTextPos(i); - - while (mergeConflictScanner.getTextPos() < end) { - classifyDisabledCodeToken(); - } - } - - function classifyDisabledCodeToken() { - const start = mergeConflictScanner.getTextPos(); - const tokenKind = mergeConflictScanner.scan(); - const end = mergeConflictScanner.getTextPos(); - - const type = classifyTokenType(tokenKind); - if (type) { - pushClassification(start, end - start, type); - } - } - - /** - * Returns true if node should be treated as classified and no further processing is required. - * False will mean that node is not classified and traverse routine should recurse into node contents. - */ - function tryClassifyNode(node: Node): boolean { - if (isJSDocTag(node)) { - return true; - } - - if (nodeIsMissing(node)) { - return true; - } - - const classifiedElementName = tryClassifyJsxElementName(node); - if (!isToken(node) && node.kind !== SyntaxKind.JsxText && classifiedElementName === undefined) { - return false; - } - - const tokenStart = node.kind === SyntaxKind.JsxText ? node.pos : classifyLeadingTriviaAndGetTokenStart(node); - - const tokenWidth = node.end - tokenStart; - Debug.assert(tokenWidth >= 0); - if (tokenWidth > 0) { - const type = classifiedElementName || classifyTokenType(node.kind, node); - if (type) { - pushClassification(tokenStart, tokenWidth, type); - } - } - - return true; - } - - function tryClassifyJsxElementName(token: Node): ClassificationType { - switch (token.parent && token.parent.kind) { - case SyntaxKind.JsxOpeningElement: - if ((token.parent).tagName === token) { - return ClassificationType.jsxOpenTagName; - } - break; - case SyntaxKind.JsxClosingElement: - if ((token.parent).tagName === token) { - return ClassificationType.jsxCloseTagName; - } - break; - case SyntaxKind.JsxSelfClosingElement: - if ((token.parent).tagName === token) { - return ClassificationType.jsxSelfClosingTagName; - } - break; - case SyntaxKind.JsxAttribute: - if ((token.parent).name === token) { - return ClassificationType.jsxAttribute; - } - break; - } - return undefined; - } - - // for accurate classification, the actual token should be passed in. however, for - // cases like 'disabled merge code' classification, we just get the token kind and - // classify based on that instead. - function classifyTokenType(tokenKind: SyntaxKind, token?: Node): ClassificationType { - if (isKeyword(tokenKind)) { - return ClassificationType.keyword; - } - - // Special case < and > If they appear in a generic context they are punctuation, - // not operators. - if (tokenKind === SyntaxKind.LessThanToken || tokenKind === SyntaxKind.GreaterThanToken) { - // If the node owning the token has a type argument list or type parameter list, then - // we can effectively assume that a '<' and '>' belong to those lists. - if (token && getTypeArgumentOrTypeParameterList(token.parent)) { - return ClassificationType.punctuation; - } - } - - if (isPunctuation(tokenKind)) { - if (token) { - if (tokenKind === SyntaxKind.EqualsToken) { - // the '=' in a variable declaration is special cased here. - if (token.parent.kind === SyntaxKind.VariableDeclaration || - token.parent.kind === SyntaxKind.PropertyDeclaration || - token.parent.kind === SyntaxKind.Parameter || - token.parent.kind === SyntaxKind.JsxAttribute) { - return ClassificationType.operator; - } - } - - if (token.parent.kind === SyntaxKind.BinaryExpression || - token.parent.kind === SyntaxKind.PrefixUnaryExpression || - token.parent.kind === SyntaxKind.PostfixUnaryExpression || - token.parent.kind === SyntaxKind.ConditionalExpression) { - return ClassificationType.operator; - } - } - - return ClassificationType.punctuation; - } - else if (tokenKind === SyntaxKind.NumericLiteral) { - return ClassificationType.numericLiteral; - } - else if (tokenKind === SyntaxKind.StringLiteral) { - return token.parent.kind === SyntaxKind.JsxAttribute ? ClassificationType.jsxAttributeStringLiteralValue : ClassificationType.stringLiteral; - } - else if (tokenKind === SyntaxKind.RegularExpressionLiteral) { - // TODO: we should get another classification type for these literals. - return ClassificationType.stringLiteral; - } - else if (isTemplateLiteralKind(tokenKind)) { - // TODO (drosen): we should *also* get another classification type for these literals. - return ClassificationType.stringLiteral; - } - else if (tokenKind === SyntaxKind.JsxText) { - return ClassificationType.jsxText; - } - else if (tokenKind === SyntaxKind.Identifier) { - if (token) { - switch (token.parent.kind) { - case SyntaxKind.ClassDeclaration: - if ((token.parent).name === token) { - return ClassificationType.className; - } - return; - case SyntaxKind.TypeParameter: - if ((token.parent).name === token) { - return ClassificationType.typeParameterName; - } - return; - case SyntaxKind.InterfaceDeclaration: - if ((token.parent).name === token) { - return ClassificationType.interfaceName; - } - return; - case SyntaxKind.EnumDeclaration: - if ((token.parent).name === token) { - return ClassificationType.enumName; - } - return; - case SyntaxKind.ModuleDeclaration: - if ((token.parent).name === token) { - return ClassificationType.moduleName; - } - return; - case SyntaxKind.Parameter: - if ((token.parent).name === token) { - const isThis = token.kind === SyntaxKind.Identifier && (token).originalKeywordKind === SyntaxKind.ThisKeyword; - return isThis ? ClassificationType.keyword : ClassificationType.parameterName; - } - return; - } - } - return ClassificationType.identifier; - } - } - - function processElement(element: Node) { - if (!element) { - return; - } - - // Ignore nodes that don't intersect the original span to classify. - if (decodedTextSpanIntersectsWith(spanStart, spanLength, element.pos, element.getFullWidth())) { - checkForClassificationCancellation(element.kind); - - const children = element.getChildren(sourceFile); - for (let i = 0, n = children.length; i < n; i++) { - const child = children[i]; - if (!tryClassifyNode(child)) { - // Recurse into our child nodes. - processElement(child); - } - } - } - } + return Classifier.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span); } function getOutliningSpans(fileName: string): OutliningSpan[] { @@ -7673,109 +1237,11 @@ namespace ts { return []; } - /** - * Checks if position points to a valid position to add JSDoc comments, and if so, - * returns the appropriate template. Otherwise returns an empty string. - * Valid positions are - * - outside of comments, statements, and expressions, and - * - preceding a: - * - function/constructor/method declaration - * - class declarations - * - variable statements - * - namespace declarations - * - * Hosts should ideally check that: - * - The line is all whitespace up to 'position' before performing the insertion. - * - If the keystroke sequence "/\*\*" induced the call, we also check that the next - * non-whitespace character is '*', which (approximately) indicates whether we added - * the second '*' to complete an existing (JSDoc) comment. - * @param fileName The file in which to perform the check. - * @param position The (character-indexed) position in the file where the check should - * be performed. - */ function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { - const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName); - - // Check if in a context where we don't want to perform any insertion - if (isInString(sourceFile, position) || isInComment(sourceFile, position) || hasDocComment(sourceFile, position)) { - return undefined; - } - - const tokenAtPos = getTokenAtPosition(sourceFile, position); - const tokenStart = tokenAtPos.getStart(); - if (!tokenAtPos || tokenStart < position) { - return undefined; - } - - // TODO: add support for: - // - enums/enum members - // - interfaces - // - property declarations - // - potentially property assignments - let commentOwner: Node; - findOwner: for (commentOwner = tokenAtPos; commentOwner; commentOwner = commentOwner.parent) { - switch (commentOwner.kind) { - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.Constructor: - case SyntaxKind.ClassDeclaration: - case SyntaxKind.VariableStatement: - break findOwner; - case SyntaxKind.SourceFile: - return undefined; - case SyntaxKind.ModuleDeclaration: - // If in walking up the tree, we hit a a nested namespace declaration, - // then we must be somewhere within a dotted namespace name; however we don't - // want to give back a JSDoc template for the 'b' or 'c' in 'namespace a.b.c { }'. - if (commentOwner.parent.kind === SyntaxKind.ModuleDeclaration) { - return undefined; - } - break findOwner; - } - } - - if (!commentOwner || commentOwner.getStart() < position) { - return undefined; - } - - const parameters = getParametersForJsDocOwningNode(commentOwner); - const posLineAndChar = sourceFile.getLineAndCharacterOfPosition(position); - const lineStart = sourceFile.getLineStarts()[posLineAndChar.line]; - - const indentationStr = sourceFile.text.substr(lineStart, posLineAndChar.character); - - const newLine = getNewLineOrDefaultFromHost(host); - - let docParams = ""; - for (let i = 0, numParams = parameters.length; i < numParams; i++) { - const currentName = parameters[i].name; - const paramName = currentName.kind === SyntaxKind.Identifier ? - (currentName).text : - "param" + i; - - docParams += `${indentationStr} * @param ${paramName}${newLine}`; - } - - // A doc comment consists of the following - // * The opening comment line - // * the first line (without a param) for the object's untagged info (this is also where the caret ends up) - // * the '@param'-tagged lines - // * TODO: other tags. - // * the closing comment line - // * if the caret was directly in front of the object, then we add an extra line and indentation. - const preamble = "/**" + newLine + - indentationStr + " * "; - const result = - preamble + newLine + - docParams + - indentationStr + " */" + - (tokenStart === position ? newLine + indentationStr : ""); - - return { newText: result, caretOffset: preamble.length }; + return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } function isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean { - // '<' is currently not supported, figuring out if we're in a Generic Type vs. a comparison is too // expensive to do during typing scenarios // i.e. whether we're dealing with: @@ -7804,52 +1270,6 @@ namespace ts { return true; } - function getParametersForJsDocOwningNode(commentOwner: Node): ParameterDeclaration[] { - if (isFunctionLike(commentOwner)) { - return commentOwner.parameters; - } - - if (commentOwner.kind === SyntaxKind.VariableStatement) { - const varStatement = commentOwner; - const varDeclarations = varStatement.declarationList.declarations; - - if (varDeclarations.length === 1 && varDeclarations[0].initializer) { - return getParametersFromRightHandSideOfAssignment(varDeclarations[0].initializer); - } - } - - return emptyArray; - } - - /** - * Digs into an an initializer or RHS operand of an assignment operation - * to get the parameters of an apt signature corresponding to a - * function expression or a class expression. - * - * @param rightHandSide the expression which may contain an appropriate set of parameters - * @returns the parameters of a signature found on the RHS if one exists; otherwise 'emptyArray'. - */ - function getParametersFromRightHandSideOfAssignment(rightHandSide: Expression): ParameterDeclaration[] { - while (rightHandSide.kind === SyntaxKind.ParenthesizedExpression) { - rightHandSide = (rightHandSide).expression; - } - - switch (rightHandSide.kind) { - case SyntaxKind.FunctionExpression: - case SyntaxKind.ArrowFunction: - return (rightHandSide).parameters; - case SyntaxKind.ClassExpression: - for (const member of (rightHandSide).members) { - if (member.kind === SyntaxKind.Constructor) { - return (member).parameters; - } - } - break; - } - - return emptyArray; - } - function getTodoComments(fileName: string, descriptors: TodoCommentDescriptor[]): TodoComment[] { // Note: while getting todo comments seems like a syntactic operation, we actually // treat it as a semantic operation here. This is because we expect our host to call @@ -7997,15 +1417,6 @@ namespace ts { } } - function getStringLiteralTypeForNode(node: StringLiteral | LiteralTypeNode, typeChecker: TypeChecker): LiteralType { - const searchNode = node.parent.kind === SyntaxKind.LiteralType ? node.parent : node; - const type = typeChecker.getTypeAtLocation(searchNode); - if (type && type.flags & TypeFlags.StringLiteral) { - return type; - } - return undefined; - } - function getRenameInfo(fileName: string, position: number): RenameInfo { synchronizeHostData(); @@ -8034,7 +1445,7 @@ namespace ts { } const displayName = stripQuotes(getDeclaredName(typeChecker, symbol, node)); - const kind = getSymbolKind(symbol, node); + const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, node); if (kind) { return { canRename: true, @@ -8042,7 +1453,7 @@ namespace ts { displayName, localizedErrorMessage: undefined, fullDisplayName: typeChecker.getFullyQualifiedName(symbol), - kindModifiers: getSymbolModifiers(symbol), + kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), triggerSpan: createTriggerSpanForNode(node, sourceFile) }; } @@ -8149,15 +1560,6 @@ namespace ts { }; } - function forEachDescendantOfKind(node: Node, kind: SyntaxKind, action: (node: Node) => void) { - forEachChild(node, child => { - if (child.kind === kind) { - action(child); - } - forEachDescendantOfKind(child, kind, action); - }); - } - /* @internal */ export function getNameTable(sourceFile: SourceFile): Map { if (!sourceFile.nameTable) { @@ -8210,468 +1612,6 @@ namespace ts { (node.parent).argumentExpression === node; } - /// Classifier - export function createClassifier(): Classifier { - const scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ false); - - /// We do not have a full parser support to know when we should parse a regex or not - /// If we consider every slash token to be a regex, we could be missing cases like "1/2/3", where - /// we have a series of divide operator. this list allows us to be more accurate by ruling out - /// locations where a regexp cannot exist. - const noRegexTable: boolean[] = []; - noRegexTable[SyntaxKind.Identifier] = true; - noRegexTable[SyntaxKind.StringLiteral] = true; - noRegexTable[SyntaxKind.NumericLiteral] = true; - noRegexTable[SyntaxKind.RegularExpressionLiteral] = true; - noRegexTable[SyntaxKind.ThisKeyword] = true; - noRegexTable[SyntaxKind.PlusPlusToken] = true; - noRegexTable[SyntaxKind.MinusMinusToken] = true; - noRegexTable[SyntaxKind.CloseParenToken] = true; - noRegexTable[SyntaxKind.CloseBracketToken] = true; - noRegexTable[SyntaxKind.CloseBraceToken] = true; - noRegexTable[SyntaxKind.TrueKeyword] = true; - noRegexTable[SyntaxKind.FalseKeyword] = true; - - // Just a stack of TemplateHeads and OpenCurlyBraces, used to perform rudimentary (inexact) - // classification on template strings. Because of the context free nature of templates, - // the only precise way to classify a template portion would be by propagating the stack across - // lines, just as we do with the end-of-line state. However, this is a burden for implementers, - // and the behavior is entirely subsumed by the syntactic classifier anyway, so we instead - // flatten any nesting when the template stack is non-empty and encode it in the end-of-line state. - // Situations in which this fails are - // 1) When template strings are nested across different lines: - // `hello ${ `world - // ` }` - // - // Where on the second line, you will get the closing of a template, - // a closing curly, and a new template. - // - // 2) When substitution expressions have curly braces and the curly brace falls on the next line: - // `hello ${ () => { - // return "world" } } ` - // - // Where on the second line, you will get the 'return' keyword, - // a string literal, and a template end consisting of '} } `'. - const templateStack: SyntaxKind[] = []; - - /** Returns true if 'keyword2' can legally follow 'keyword1' in any language construct. */ - function canFollow(keyword1: SyntaxKind, keyword2: SyntaxKind) { - if (isAccessibilityModifier(keyword1)) { - if (keyword2 === SyntaxKind.GetKeyword || - keyword2 === SyntaxKind.SetKeyword || - keyword2 === SyntaxKind.ConstructorKeyword || - keyword2 === SyntaxKind.StaticKeyword) { - - // Allow things like "public get", "public constructor" and "public static". - // These are all legal. - return true; - } - - // Any other keyword following "public" is actually an identifier an not a real - // keyword. - return false; - } - - // Assume any other keyword combination is legal. This can be refined in the future - // if there are more cases we want the classifier to be better at. - return true; - } - - function convertClassifications(classifications: Classifications, text: string): ClassificationResult { - const entries: ClassificationInfo[] = []; - const dense = classifications.spans; - let lastEnd = 0; - - for (let i = 0, n = dense.length; i < n; i += 3) { - const start = dense[i]; - const length = dense[i + 1]; - const type = dense[i + 2]; - - // Make a whitespace entry between the last item and this one. - if (lastEnd >= 0) { - const whitespaceLength = start - lastEnd; - if (whitespaceLength > 0) { - entries.push({ length: whitespaceLength, classification: TokenClass.Whitespace }); - } - } - - entries.push({ length, classification: convertClassification(type) }); - lastEnd = start + length; - } - - const whitespaceLength = text.length - lastEnd; - if (whitespaceLength > 0) { - entries.push({ length: whitespaceLength, classification: TokenClass.Whitespace }); - } - - return { entries, finalLexState: classifications.endOfLineState }; - } - - function convertClassification(type: ClassificationType): TokenClass { - switch (type) { - case ClassificationType.comment: return TokenClass.Comment; - case ClassificationType.keyword: return TokenClass.Keyword; - case ClassificationType.numericLiteral: return TokenClass.NumberLiteral; - case ClassificationType.operator: return TokenClass.Operator; - case ClassificationType.stringLiteral: return TokenClass.StringLiteral; - case ClassificationType.whiteSpace: return TokenClass.Whitespace; - case ClassificationType.punctuation: return TokenClass.Punctuation; - case ClassificationType.identifier: - case ClassificationType.className: - case ClassificationType.enumName: - case ClassificationType.interfaceName: - case ClassificationType.moduleName: - case ClassificationType.typeParameterName: - case ClassificationType.typeAliasName: - case ClassificationType.text: - case ClassificationType.parameterName: - default: - return TokenClass.Identifier; - } - } - - function getClassificationsForLine(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): ClassificationResult { - return convertClassifications(getEncodedLexicalClassifications(text, lexState, syntacticClassifierAbsent), text); - } - - // If there is a syntactic classifier ('syntacticClassifierAbsent' is false), - // we will be more conservative in order to avoid conflicting with the syntactic classifier. - function getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent: boolean): Classifications { - let offset = 0; - let token = SyntaxKind.Unknown; - let lastNonTriviaToken = SyntaxKind.Unknown; - - // Empty out the template stack for reuse. - while (templateStack.length > 0) { - templateStack.pop(); - } - - // If we're in a string literal, then prepend: "\ - // (and a newline). That way when we lex we'll think we're still in a string literal. - // - // If we're in a multiline comment, then prepend: /* - // (and a newline). That way when we lex we'll think we're still in a multiline comment. - switch (lexState) { - case EndOfLineState.InDoubleQuoteStringLiteral: - text = "\"\\\n" + text; - offset = 3; - break; - case EndOfLineState.InSingleQuoteStringLiteral: - text = "'\\\n" + text; - offset = 3; - break; - case EndOfLineState.InMultiLineCommentTrivia: - text = "/*\n" + text; - offset = 3; - break; - case EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate: - text = "`\n" + text; - offset = 2; - break; - case EndOfLineState.InTemplateMiddleOrTail: - text = "}\n" + text; - offset = 2; - // fallthrough - case EndOfLineState.InTemplateSubstitutionPosition: - templateStack.push(SyntaxKind.TemplateHead); - break; - } - - scanner.setText(text); - - const result: Classifications = { - endOfLineState: EndOfLineState.None, - spans: [] - }; - - // We can run into an unfortunate interaction between the lexical and syntactic classifier - // when the user is typing something generic. Consider the case where the user types: - // - // Foo tokens. It's a weak heuristic, but should - // work well enough in practice. - let angleBracketStack = 0; - - do { - token = scanner.scan(); - - if (!isTrivia(token)) { - if ((token === SyntaxKind.SlashToken || token === SyntaxKind.SlashEqualsToken) && !noRegexTable[lastNonTriviaToken]) { - if (scanner.reScanSlashToken() === SyntaxKind.RegularExpressionLiteral) { - token = SyntaxKind.RegularExpressionLiteral; - } - } - else if (lastNonTriviaToken === SyntaxKind.DotToken && isKeyword(token)) { - token = SyntaxKind.Identifier; - } - else if (isKeyword(lastNonTriviaToken) && isKeyword(token) && !canFollow(lastNonTriviaToken, token)) { - // We have two keywords in a row. Only treat the second as a keyword if - // it's a sequence that could legally occur in the language. Otherwise - // treat it as an identifier. This way, if someone writes "private var" - // we recognize that 'var' is actually an identifier here. - token = SyntaxKind.Identifier; - } - else if (lastNonTriviaToken === SyntaxKind.Identifier && - token === SyntaxKind.LessThanToken) { - // Could be the start of something generic. Keep track of that by bumping - // up the current count of generic contexts we may be in. - angleBracketStack++; - } - else if (token === SyntaxKind.GreaterThanToken && angleBracketStack > 0) { - // If we think we're currently in something generic, then mark that that - // generic entity is complete. - angleBracketStack--; - } - else if (token === SyntaxKind.AnyKeyword || - token === SyntaxKind.StringKeyword || - token === SyntaxKind.NumberKeyword || - token === SyntaxKind.BooleanKeyword || - token === SyntaxKind.SymbolKeyword) { - if (angleBracketStack > 0 && !syntacticClassifierAbsent) { - // If it looks like we're could be in something generic, don't classify this - // as a keyword. We may just get overwritten by the syntactic classifier, - // causing a noisy experience for the user. - token = SyntaxKind.Identifier; - } - } - else if (token === SyntaxKind.TemplateHead) { - templateStack.push(token); - } - else if (token === SyntaxKind.OpenBraceToken) { - // If we don't have anything on the template stack, - // then we aren't trying to keep track of a previously scanned template head. - if (templateStack.length > 0) { - templateStack.push(token); - } - } - else if (token === SyntaxKind.CloseBraceToken) { - // If we don't have anything on the template stack, - // then we aren't trying to keep track of a previously scanned template head. - if (templateStack.length > 0) { - const lastTemplateStackToken = lastOrUndefined(templateStack); - - if (lastTemplateStackToken === SyntaxKind.TemplateHead) { - token = scanner.reScanTemplateToken(); - - // Only pop on a TemplateTail; a TemplateMiddle indicates there is more for us. - if (token === SyntaxKind.TemplateTail) { - templateStack.pop(); - } - else { - Debug.assert(token === SyntaxKind.TemplateMiddle, "Should have been a template middle. Was " + token); - } - } - else { - Debug.assert(lastTemplateStackToken === SyntaxKind.OpenBraceToken, "Should have been an open brace. Was: " + token); - templateStack.pop(); - } - } - } - - lastNonTriviaToken = token; - } - - processToken(); - } - while (token !== SyntaxKind.EndOfFileToken); - - return result; - - function processToken(): void { - const start = scanner.getTokenPos(); - const end = scanner.getTextPos(); - - addResult(start, end, classFromKind(token)); - - if (end >= text.length) { - if (token === SyntaxKind.StringLiteral) { - // Check to see if we finished up on a multiline string literal. - const tokenText = scanner.getTokenText(); - if (scanner.isUnterminated()) { - const lastCharIndex = tokenText.length - 1; - - let numBackslashes = 0; - while (tokenText.charCodeAt(lastCharIndex - numBackslashes) === CharacterCodes.backslash) { - numBackslashes++; - } - - // If we have an odd number of backslashes, then the multiline string is unclosed - if (numBackslashes & 1) { - const quoteChar = tokenText.charCodeAt(0); - result.endOfLineState = quoteChar === CharacterCodes.doubleQuote - ? EndOfLineState.InDoubleQuoteStringLiteral - : EndOfLineState.InSingleQuoteStringLiteral; - } - } - } - else if (token === SyntaxKind.MultiLineCommentTrivia) { - // Check to see if the multiline comment was unclosed. - if (scanner.isUnterminated()) { - result.endOfLineState = EndOfLineState.InMultiLineCommentTrivia; - } - } - else if (isTemplateLiteralKind(token)) { - if (scanner.isUnterminated()) { - if (token === SyntaxKind.TemplateTail) { - result.endOfLineState = EndOfLineState.InTemplateMiddleOrTail; - } - else if (token === SyntaxKind.NoSubstitutionTemplateLiteral) { - result.endOfLineState = EndOfLineState.InTemplateHeadOrNoSubstitutionTemplate; - } - else { - Debug.fail("Only 'NoSubstitutionTemplateLiteral's and 'TemplateTail's can be unterminated; got SyntaxKind #" + token); - } - } - } - else if (templateStack.length > 0 && lastOrUndefined(templateStack) === SyntaxKind.TemplateHead) { - result.endOfLineState = EndOfLineState.InTemplateSubstitutionPosition; - } - } - } - - function addResult(start: number, end: number, classification: ClassificationType): void { - if (classification === ClassificationType.whiteSpace) { - // Don't bother with whitespace classifications. They're not needed. - return; - } - - if (start === 0 && offset > 0) { - // We're classifying the first token, and this was a case where we prepended - // text. We should consider the start of this token to be at the start of - // the original text. - start += offset; - } - - // All our tokens are in relation to the augmented text. Move them back to be - // relative to the original text. - start -= offset; - end -= offset; - const length = end - start; - - if (length > 0) { - result.spans.push(start); - result.spans.push(length); - result.spans.push(classification); - } - } - } - - function isBinaryExpressionOperatorToken(token: SyntaxKind): boolean { - switch (token) { - case SyntaxKind.AsteriskToken: - case SyntaxKind.SlashToken: - case SyntaxKind.PercentToken: - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.LessThanLessThanToken: - case SyntaxKind.GreaterThanGreaterThanToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: - case SyntaxKind.LessThanToken: - case SyntaxKind.GreaterThanToken: - case SyntaxKind.LessThanEqualsToken: - case SyntaxKind.GreaterThanEqualsToken: - case SyntaxKind.InstanceOfKeyword: - case SyntaxKind.InKeyword: - case SyntaxKind.AsKeyword: - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - case SyntaxKind.AmpersandToken: - case SyntaxKind.CaretToken: - case SyntaxKind.BarToken: - case SyntaxKind.AmpersandAmpersandToken: - case SyntaxKind.BarBarToken: - case SyntaxKind.BarEqualsToken: - case SyntaxKind.AmpersandEqualsToken: - case SyntaxKind.CaretEqualsToken: - case SyntaxKind.LessThanLessThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanEqualsToken: - case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: - case SyntaxKind.PlusEqualsToken: - case SyntaxKind.MinusEqualsToken: - case SyntaxKind.AsteriskEqualsToken: - case SyntaxKind.SlashEqualsToken: - case SyntaxKind.PercentEqualsToken: - case SyntaxKind.EqualsToken: - case SyntaxKind.CommaToken: - return true; - default: - return false; - } - } - - function isPrefixUnaryExpressionOperatorToken(token: SyntaxKind): boolean { - switch (token) { - case SyntaxKind.PlusToken: - case SyntaxKind.MinusToken: - case SyntaxKind.TildeToken: - case SyntaxKind.ExclamationToken: - case SyntaxKind.PlusPlusToken: - case SyntaxKind.MinusMinusToken: - return true; - default: - return false; - } - } - - function isKeyword(token: SyntaxKind): boolean { - return token >= SyntaxKind.FirstKeyword && token <= SyntaxKind.LastKeyword; - } - - function classFromKind(token: SyntaxKind): ClassificationType { - if (isKeyword(token)) { - return ClassificationType.keyword; - } - else if (isBinaryExpressionOperatorToken(token) || isPrefixUnaryExpressionOperatorToken(token)) { - return ClassificationType.operator; - } - else if (token >= SyntaxKind.FirstPunctuation && token <= SyntaxKind.LastPunctuation) { - return ClassificationType.punctuation; - } - - switch (token) { - case SyntaxKind.NumericLiteral: - return ClassificationType.numericLiteral; - case SyntaxKind.StringLiteral: - return ClassificationType.stringLiteral; - case SyntaxKind.RegularExpressionLiteral: - return ClassificationType.regularExpressionLiteral; - case SyntaxKind.ConflictMarkerTrivia: - case SyntaxKind.MultiLineCommentTrivia: - case SyntaxKind.SingleLineCommentTrivia: - return ClassificationType.comment; - case SyntaxKind.WhitespaceTrivia: - case SyntaxKind.NewLineTrivia: - return ClassificationType.whiteSpace; - case SyntaxKind.Identifier: - default: - if (isTemplateLiteralKind(token)) { - return ClassificationType.stringLiteral; - } - return ClassificationType.identifier; - } - } - - return { - getClassificationsForLine, - getEncodedLexicalClassifications - }; - } - /// getDefaultLibraryFilePath declare const __dirname: string; @@ -8690,15 +1630,7 @@ namespace ts { } function initializeServices() { - objectAllocator = { - getNodeConstructor: () => NodeObject, - getTokenConstructor: () => TokenObject, - getIdentifierConstructor: () => IdentifierObject, - getSourceFileConstructor: () => SourceFileObject, - getSymbolConstructor: () => SymbolObject, - getTypeConstructor: () => TypeObject, - getSignatureConstructor: () => SignatureObject, - }; + objectAllocator = Allocators.getServicesObjectAllocator(); } initializeServices(); diff --git a/src/services/shims.ts b/src/services/shims.ts index 41b366e0857..0c7a56199cc 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -986,7 +986,7 @@ namespace ts { constructor(factory: ShimFactory, private logger: Logger) { super(factory); - this.classifier = createClassifier(); + this.classifier = Classifier.createClassifier(); } public getEncodedLexicalClassifications(text: string, lexState: EndOfLineState, syntacticClassifierAbsent?: boolean): string { @@ -1047,7 +1047,7 @@ namespace ts { `getPreProcessedFileInfo('${fileName}')`, () => { // for now treat files as JavaScript - const result = preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()), /* readImportFiles */ true, /* detectJavaScriptImports */ true); + const result = PreProcess.preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()), /* readImportFiles */ true, /* detectJavaScriptImports */ true); return { referencedFiles: this.convertFileReferences(result.referencedFiles), importedFiles: this.convertFileReferences(result.importedFiles), diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts new file mode 100644 index 00000000000..1a2319c272a --- /dev/null +++ b/src/services/symbolDisplay.ts @@ -0,0 +1,523 @@ +/* @internal */ +namespace ts.SymbolDisplay { + // TODO(drosen): use contextual SemanticMeaning. + export function getSymbolKind(typeChecker: TypeChecker, symbol: Symbol, location: Node): string { + const flags = symbol.getFlags(); + + if (flags & SymbolFlags.Class) return getDeclarationOfKind(symbol, SyntaxKind.ClassExpression) ? + ScriptElementKind.localClassElement : ScriptElementKind.classElement; + if (flags & SymbolFlags.Enum) return ScriptElementKind.enumElement; + if (flags & SymbolFlags.TypeAlias) return ScriptElementKind.typeElement; + if (flags & SymbolFlags.Interface) return ScriptElementKind.interfaceElement; + if (flags & SymbolFlags.TypeParameter) return ScriptElementKind.typeParameterElement; + + const result = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, flags, location); + if (result === ScriptElementKind.unknown) { + if (flags & SymbolFlags.TypeParameter) return ScriptElementKind.typeParameterElement; + if (flags & SymbolFlags.EnumMember) return ScriptElementKind.variableElement; + if (flags & SymbolFlags.Alias) return ScriptElementKind.alias; + if (flags & SymbolFlags.Module) return ScriptElementKind.moduleElement; + } + + return result; + } + + function getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker: TypeChecker, symbol: Symbol, flags: SymbolFlags, location: Node) { + if (typeChecker.isUndefinedSymbol(symbol)) { + return ScriptElementKind.variableElement; + } + if (typeChecker.isArgumentsSymbol(symbol)) { + return ScriptElementKind.localVariableElement; + } + if (location.kind === SyntaxKind.ThisKeyword && isExpression(location)) { + return ScriptElementKind.parameterElement; + } + if (flags & SymbolFlags.Variable) { + if (isFirstDeclarationOfSymbolParameter(symbol)) { + return ScriptElementKind.parameterElement; + } + else if (symbol.valueDeclaration && isConst(symbol.valueDeclaration)) { + return ScriptElementKind.constElement; + } + else if (forEach(symbol.declarations, isLet)) { + return ScriptElementKind.letElement; + } + return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localVariableElement : ScriptElementKind.variableElement; + } + if (flags & SymbolFlags.Function) return isLocalVariableOrFunction(symbol) ? ScriptElementKind.localFunctionElement : ScriptElementKind.functionElement; + if (flags & SymbolFlags.GetAccessor) return ScriptElementKind.memberGetAccessorElement; + if (flags & SymbolFlags.SetAccessor) return ScriptElementKind.memberSetAccessorElement; + if (flags & SymbolFlags.Method) return ScriptElementKind.memberFunctionElement; + if (flags & SymbolFlags.Constructor) return ScriptElementKind.constructorImplementationElement; + + if (flags & SymbolFlags.Property) { + if (flags & SymbolFlags.SyntheticProperty) { + // If union property is result of union of non method (property/accessors/variables), it is labeled as property + const unionPropertyKind = forEach(typeChecker.getRootSymbols(symbol), rootSymbol => { + const rootSymbolFlags = rootSymbol.getFlags(); + if (rootSymbolFlags & (SymbolFlags.PropertyOrAccessor | SymbolFlags.Variable)) { + return ScriptElementKind.memberVariableElement; + } + Debug.assert(!!(rootSymbolFlags & SymbolFlags.Method)); + }); + if (!unionPropertyKind) { + // If this was union of all methods, + // make sure it has call signatures before we can label it as method + const typeOfUnionProperty = typeChecker.getTypeOfSymbolAtLocation(symbol, location); + if (typeOfUnionProperty.getCallSignatures().length) { + return ScriptElementKind.memberFunctionElement; + } + return ScriptElementKind.memberVariableElement; + } + return unionPropertyKind; + } + return ScriptElementKind.memberVariableElement; + } + + return ScriptElementKind.unknown; + } + + export function getSymbolModifiers(symbol: Symbol): string { + return symbol && symbol.declarations && symbol.declarations.length > 0 + ? getNodeModifiers(symbol.declarations[0]) + : ScriptElementKindModifier.none; + } + + // TODO(drosen): Currently completion entry details passes the SemanticMeaning.All instead of using semanticMeaning of location + export function getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker: TypeChecker, symbol: Symbol, sourceFile: SourceFile, enclosingDeclaration: Node, + location: Node, semanticMeaning = Meaning.getMeaningFromLocation(location)) { + + const displayParts: SymbolDisplayPart[] = []; + let documentation: SymbolDisplayPart[]; + const symbolFlags = symbol.flags; + let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(typeChecker, symbol, symbolFlags, location); + let hasAddedSymbolInfo: boolean; + const isThisExpression = location.kind === SyntaxKind.ThisKeyword && isExpression(location); + let type: Type; + + // Class at constructor site need to be shown as constructor apart from property,method, vars + if (symbolKind !== ScriptElementKind.unknown || symbolFlags & SymbolFlags.Class || symbolFlags & SymbolFlags.Alias) { + // If it is accessor they are allowed only if location is at name of the accessor + if (symbolKind === ScriptElementKind.memberGetAccessorElement || symbolKind === ScriptElementKind.memberSetAccessorElement) { + symbolKind = ScriptElementKind.memberVariableElement; + } + + let signature: Signature; + type = isThisExpression ? typeChecker.getTypeAtLocation(location) : typeChecker.getTypeOfSymbolAtLocation(symbol, location); + if (type) { + if (location.parent && location.parent.kind === SyntaxKind.PropertyAccessExpression) { + const right = (location.parent).name; + // Either the location is on the right of a property access, or on the left and the right is missing + if (right === location || (right && right.getFullWidth() === 0)) { + location = location.parent; + } + } + + // try get the call/construct signature from the type if it matches + let callExpression: CallExpression; + if (location.kind === SyntaxKind.CallExpression || location.kind === SyntaxKind.NewExpression) { + callExpression = location; + } + else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { + callExpression = location.parent; + } + + if (callExpression) { + const candidateSignatures: Signature[] = []; + signature = typeChecker.getResolvedSignature(callExpression, candidateSignatures); + if (!signature && candidateSignatures.length) { + // Use the first candidate: + signature = candidateSignatures[0]; + } + + const useConstructSignatures = callExpression.kind === SyntaxKind.NewExpression || callExpression.expression.kind === SyntaxKind.SuperKeyword; + const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); + + if (!contains(allSignatures, signature.target) && !contains(allSignatures, signature)) { + // Get the first signature if there is one -- allSignatures may contain + // either the original signature or its target, so check for either + signature = allSignatures.length ? allSignatures[0] : undefined; + } + + if (signature) { + if (useConstructSignatures && (symbolFlags & SymbolFlags.Class)) { + // Constructor + symbolKind = ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + } + else if (symbolFlags & SymbolFlags.Alias) { + symbolKind = ScriptElementKind.alias; + pushTypePart(symbolKind); + displayParts.push(spacePart()); + if (useConstructSignatures) { + displayParts.push(keywordPart(SyntaxKind.NewKeyword)); + displayParts.push(spacePart()); + } + addFullSymbolName(symbol); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + + switch (symbolKind) { + case ScriptElementKind.memberVariableElement: + case ScriptElementKind.variableElement: + case ScriptElementKind.constElement: + case ScriptElementKind.letElement: + case ScriptElementKind.parameterElement: + case ScriptElementKind.localVariableElement: + // If it is call or construct signature of lambda's write type name + displayParts.push(punctuationPart(SyntaxKind.ColonToken)); + displayParts.push(spacePart()); + if (useConstructSignatures) { + displayParts.push(keywordPart(SyntaxKind.NewKeyword)); + displayParts.push(spacePart()); + } + if (!(type.flags & TypeFlags.Anonymous) && type.symbol) { + addRange(displayParts, symbolToDisplayParts(typeChecker, type.symbol, enclosingDeclaration, /*meaning*/ undefined, SymbolFormatFlags.WriteTypeParametersOrArguments)); + } + addSignatureDisplayParts(signature, allSignatures, TypeFormatFlags.WriteArrowStyleSignature); + break; + + default: + // Just signature + addSignatureDisplayParts(signature, allSignatures); + } + hasAddedSymbolInfo = true; + } + } + else if ((isNameOfFunctionDeclaration(location) && !(symbol.flags & SymbolFlags.Accessor)) || // name of function declaration + (location.kind === SyntaxKind.ConstructorKeyword && location.parent.kind === SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration + // get the signature from the declaration and write it + const functionDeclaration = location.parent; + const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures(); + if (!typeChecker.isImplementationOfOverload(functionDeclaration)) { + signature = typeChecker.getSignatureFromDeclaration(functionDeclaration); + } + else { + signature = allSignatures[0]; + } + + if (functionDeclaration.kind === SyntaxKind.Constructor) { + // show (constructor) Type(...) signature + symbolKind = ScriptElementKind.constructorImplementationElement; + addPrefixForAnyFunctionOrVar(type.symbol, symbolKind); + } + else { + // (function/method) symbol(..signature) + addPrefixForAnyFunctionOrVar(functionDeclaration.kind === SyntaxKind.CallSignature && + !(type.symbol.flags & SymbolFlags.TypeLiteral || type.symbol.flags & SymbolFlags.ObjectLiteral) ? type.symbol : symbol, symbolKind); + } + + addSignatureDisplayParts(signature, allSignatures); + hasAddedSymbolInfo = true; + } + } + } + if (symbolFlags & SymbolFlags.Class && !hasAddedSymbolInfo && !isThisExpression) { + if (getDeclarationOfKind(symbol, SyntaxKind.ClassExpression)) { + // Special case for class expressions because we would like to indicate that + // the class name is local to the class body (similar to function expression) + // (local class) class + pushTypePart(ScriptElementKind.localClassElement); + } + else { + // Class declaration has name which is not local. + displayParts.push(keywordPart(SyntaxKind.ClassKeyword)); + } + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if ((symbolFlags & SymbolFlags.Interface) && (semanticMeaning & Meaning.SemanticMeaning.Type)) { + addNewLineIfDisplayPartsExist(); + displayParts.push(keywordPart(SyntaxKind.InterfaceKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + } + if (symbolFlags & SymbolFlags.TypeAlias) { + addNewLineIfDisplayPartsExist(); + displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + writeTypeParametersOfSymbol(symbol, sourceFile); + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + addRange(displayParts, typeToDisplayParts(typeChecker, typeChecker.getDeclaredTypeOfSymbol(symbol), enclosingDeclaration, TypeFormatFlags.InTypeAlias)); + } + if (symbolFlags & SymbolFlags.Enum) { + addNewLineIfDisplayPartsExist(); + if (forEach(symbol.declarations, isConstEnumDeclaration)) { + displayParts.push(keywordPart(SyntaxKind.ConstKeyword)); + displayParts.push(spacePart()); + } + displayParts.push(keywordPart(SyntaxKind.EnumKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + } + if (symbolFlags & SymbolFlags.Module) { + addNewLineIfDisplayPartsExist(); + const declaration = getDeclarationOfKind(symbol, SyntaxKind.ModuleDeclaration); + const isNamespace = declaration && declaration.name && declaration.name.kind === SyntaxKind.Identifier; + displayParts.push(keywordPart(isNamespace ? SyntaxKind.NamespaceKeyword : SyntaxKind.ModuleKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + } + if ((symbolFlags & SymbolFlags.TypeParameter) && (semanticMeaning & Meaning.SemanticMeaning.Type)) { + addNewLineIfDisplayPartsExist(); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(textPart("type parameter")); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + displayParts.push(spacePart()); + displayParts.push(keywordPart(SyntaxKind.InKeyword)); + displayParts.push(spacePart()); + if (symbol.parent) { + // Class/Interface type parameter + addFullSymbolName(symbol.parent, enclosingDeclaration); + writeTypeParametersOfSymbol(symbol.parent, enclosingDeclaration); + } + else { + // Method/function type parameter + let declaration = getDeclarationOfKind(symbol, SyntaxKind.TypeParameter); + Debug.assert(declaration !== undefined); + declaration = declaration.parent; + + if (declaration) { + if (isFunctionLikeKind(declaration.kind)) { + const signature = typeChecker.getSignatureFromDeclaration(declaration); + if (declaration.kind === SyntaxKind.ConstructSignature) { + displayParts.push(keywordPart(SyntaxKind.NewKeyword)); + displayParts.push(spacePart()); + } + else if (declaration.kind !== SyntaxKind.CallSignature && (declaration).name) { + addFullSymbolName(declaration.symbol); + } + addRange(displayParts, signatureToDisplayParts(typeChecker, signature, sourceFile, TypeFormatFlags.WriteTypeArgumentsOfSignature)); + } + else { + // Type alias type parameter + // For example + // type list = T[]; // Both T will go through same code path + displayParts.push(keywordPart(SyntaxKind.TypeKeyword)); + displayParts.push(spacePart()); + addFullSymbolName(declaration.symbol); + writeTypeParametersOfSymbol(declaration.symbol, sourceFile); + } + } + } + } + if (symbolFlags & SymbolFlags.EnumMember) { + addPrefixForAnyFunctionOrVar(symbol, "enum member"); + const declaration = symbol.declarations[0]; + if (declaration.kind === SyntaxKind.EnumMember) { + const constantValue = typeChecker.getConstantValue(declaration); + if (constantValue !== undefined) { + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + displayParts.push(displayPart(constantValue.toString(), SymbolDisplayPartKind.numericLiteral)); + } + } + } + if (symbolFlags & SymbolFlags.Alias) { + addNewLineIfDisplayPartsExist(); + if (symbol.declarations[0].kind === SyntaxKind.NamespaceExportDeclaration) { + displayParts.push(keywordPart(SyntaxKind.ExportKeyword)); + displayParts.push(spacePart()); + displayParts.push(keywordPart(SyntaxKind.NamespaceKeyword)); + } + else { + displayParts.push(keywordPart(SyntaxKind.ImportKeyword)); + } + displayParts.push(spacePart()); + addFullSymbolName(symbol); + ts.forEach(symbol.declarations, declaration => { + if (declaration.kind === SyntaxKind.ImportEqualsDeclaration) { + const importEqualsDeclaration = declaration; + if (isExternalModuleImportEqualsDeclaration(importEqualsDeclaration)) { + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + displayParts.push(keywordPart(SyntaxKind.RequireKeyword)); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(displayPart(getTextOfNode(getExternalModuleImportEqualsDeclarationExpression(importEqualsDeclaration)), SymbolDisplayPartKind.stringLiteral)); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + } + else { + const internalAliasSymbol = typeChecker.getSymbolAtLocation(importEqualsDeclaration.moduleReference); + if (internalAliasSymbol) { + displayParts.push(spacePart()); + displayParts.push(operatorPart(SyntaxKind.EqualsToken)); + displayParts.push(spacePart()); + addFullSymbolName(internalAliasSymbol, enclosingDeclaration); + } + } + return true; + } + }); + } + if (!hasAddedSymbolInfo) { + if (symbolKind !== ScriptElementKind.unknown) { + if (type) { + if (isThisExpression) { + addNewLineIfDisplayPartsExist(); + displayParts.push(keywordPart(SyntaxKind.ThisKeyword)); + } + else { + addPrefixForAnyFunctionOrVar(symbol, symbolKind); + } + + // For properties, variables and local vars: show the type + if (symbolKind === ScriptElementKind.memberVariableElement || + symbolFlags & SymbolFlags.Variable || + symbolKind === ScriptElementKind.localVariableElement || + isThisExpression) { + displayParts.push(punctuationPart(SyntaxKind.ColonToken)); + displayParts.push(spacePart()); + // If the type is type parameter, format it specially + if (type.symbol && type.symbol.flags & SymbolFlags.TypeParameter) { + const typeParameterParts = mapToDisplayParts(writer => { + typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplay(type, writer, enclosingDeclaration); + }); + addRange(displayParts, typeParameterParts); + } + else { + addRange(displayParts, typeToDisplayParts(typeChecker, type, enclosingDeclaration)); + } + } + else if (symbolFlags & SymbolFlags.Function || + symbolFlags & SymbolFlags.Method || + symbolFlags & SymbolFlags.Constructor || + symbolFlags & SymbolFlags.Signature || + symbolFlags & SymbolFlags.Accessor || + symbolKind === ScriptElementKind.memberFunctionElement) { + const allSignatures = type.getNonNullableType().getCallSignatures(); + addSignatureDisplayParts(allSignatures[0], allSignatures); + } + } + } + else { + symbolKind = getSymbolKind(typeChecker, symbol, location); + } + } + + if (!documentation) { + documentation = symbol.getDocumentationComment(); + if (documentation.length === 0 && symbol.flags & SymbolFlags.Property) { + // For some special property access expressions like `experts.foo = foo` or `module.exports.foo = foo` + // there documentation comments might be attached to the right hand side symbol of their declarations. + // The pattern of such special property access is that the parent symbol is the symbol of the file. + if (symbol.parent && forEach(symbol.parent.declarations, declaration => declaration.kind === SyntaxKind.SourceFile)) { + for (const declaration of symbol.declarations) { + if (!declaration.parent || declaration.parent.kind !== SyntaxKind.BinaryExpression) { + continue; + } + + const rhsSymbol = typeChecker.getSymbolAtLocation((declaration.parent).right); + if (!rhsSymbol) { + continue; + } + + documentation = rhsSymbol.getDocumentationComment(); + if (documentation.length > 0) { + break; + } + } + } + } + } + + return { displayParts, documentation, symbolKind }; + + function addNewLineIfDisplayPartsExist() { + if (displayParts.length) { + displayParts.push(lineBreakPart()); + } + } + + function addFullSymbolName(symbol: Symbol, enclosingDeclaration?: Node) { + const fullSymbolDisplayParts = symbolToDisplayParts(typeChecker, symbol, enclosingDeclaration || sourceFile, /*meaning*/ undefined, + SymbolFormatFlags.WriteTypeParametersOrArguments | SymbolFormatFlags.UseOnlyExternalAliasing); + addRange(displayParts, fullSymbolDisplayParts); + } + + function addPrefixForAnyFunctionOrVar(symbol: Symbol, symbolKind: string) { + addNewLineIfDisplayPartsExist(); + if (symbolKind) { + pushTypePart(symbolKind); + displayParts.push(spacePart()); + addFullSymbolName(symbol); + } + } + + function pushTypePart(symbolKind: string) { + switch (symbolKind) { + case ScriptElementKind.variableElement: + case ScriptElementKind.functionElement: + case ScriptElementKind.letElement: + case ScriptElementKind.constElement: + case ScriptElementKind.constructorImplementationElement: + displayParts.push(textOrKeywordPart(symbolKind)); + return; + default: + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(textOrKeywordPart(symbolKind)); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + return; + } + } + + function addSignatureDisplayParts(signature: Signature, allSignatures: Signature[], flags?: TypeFormatFlags) { + addRange(displayParts, signatureToDisplayParts(typeChecker, signature, enclosingDeclaration, flags | TypeFormatFlags.WriteTypeArgumentsOfSignature)); + if (allSignatures.length > 1) { + displayParts.push(spacePart()); + displayParts.push(punctuationPart(SyntaxKind.OpenParenToken)); + displayParts.push(operatorPart(SyntaxKind.PlusToken)); + displayParts.push(displayPart((allSignatures.length - 1).toString(), SymbolDisplayPartKind.numericLiteral)); + displayParts.push(spacePart()); + displayParts.push(textPart(allSignatures.length === 2 ? "overload" : "overloads")); + displayParts.push(punctuationPart(SyntaxKind.CloseParenToken)); + } + documentation = signature.getDocumentationComment(); + } + + function writeTypeParametersOfSymbol(symbol: Symbol, enclosingDeclaration: Node) { + const typeParameterParts = mapToDisplayParts(writer => { + typeChecker.getSymbolDisplayBuilder().buildTypeParameterDisplayFromSymbol(symbol, writer, enclosingDeclaration); + }); + addRange(displayParts, typeParameterParts); + } + } + + function isLocalVariableOrFunction(symbol: Symbol) { + if (symbol.parent) { + return false; // This is exported symbol + } + + return ts.forEach(symbol.declarations, declaration => { + // Function expressions are local + if (declaration.kind === SyntaxKind.FunctionExpression) { + return true; + } + + if (declaration.kind !== SyntaxKind.VariableDeclaration && declaration.kind !== SyntaxKind.FunctionDeclaration) { + return false; + } + + // If the parent is not sourceFile or module block it is local variable + for (let parent = declaration.parent; !isFunctionBlock(parent); parent = parent.parent) { + // Reached source file or module block + if (parent.kind === SyntaxKind.SourceFile || parent.kind === SyntaxKind.ModuleBlock) { + return false; + } + } + + // parent is in function block + return true; + }); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index e3856dc3251..47d3294a65c 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -40,16 +40,26 @@ "../compiler/program.ts", "../compiler/commandLineParser.ts", "../compiler/diagnosticInformationMap.generated.ts", + "types.ts", + "utilities.ts", + "allocators.ts", "breakpoints.ts", + "classifier.ts", + "completions.ts", + "documentHighlights.ts", + "findAllReferences.ts", + "goToDefinition.ts", + "jsDoc.ts", + "meaning.ts", "navigateTo.ts", "navigationBar.ts", "outliningElementsCollector.ts", "patternMatcher.ts", + "preProcess.ts", "services.ts", "shims.ts", "signatureHelp.ts", - "types.ts", - "utilities.ts", + "symbolDisplay.ts", "jsTyping.ts", "formatting/formatting.ts", "formatting/formattingContext.ts", diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 593664c4128..8324c03a2c9 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1,6 +1,237 @@ // These utilities are common to multiple language service features. /* @internal */ namespace ts { + export const scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); + export const emptyArray: any[] = []; + + export function isCallExpressionTarget(node: Node): boolean { + return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpression); + } + + export function isNewExpressionTarget(node: Node): boolean { + return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpression); + } + + function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) { + const target = climbPastPropertyAccess(node); + return target && target.parent && target.parent.kind === kind && (target.parent).expression === target; + } + + export function climbPastPropertyAccess(node: Node) { + return isRightSideOfPropertyAccess(node) ? node.parent : node; + } + + export function getTargetLabel(referenceNode: Node, labelName: string): Identifier { + while (referenceNode) { + if (referenceNode.kind === SyntaxKind.LabeledStatement && (referenceNode).label.text === labelName) { + return (referenceNode).label; + } + referenceNode = referenceNode.parent; + } + return undefined; + } + + export function isJumpStatementTarget(node: Node): boolean { + return node.kind === SyntaxKind.Identifier && + (node.parent.kind === SyntaxKind.BreakStatement || node.parent.kind === SyntaxKind.ContinueStatement) && + (node.parent).label === node; + } + + function isLabelOfLabeledStatement(node: Node): boolean { + return node.kind === SyntaxKind.Identifier && + node.parent.kind === SyntaxKind.LabeledStatement && + (node.parent).label === node; + } + + export function isLabelName(node: Node): boolean { + return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node); + } + + export function isRightSideOfQualifiedName(node: Node) { + return node.parent.kind === SyntaxKind.QualifiedName && (node.parent).right === node; + } + + export function isRightSideOfPropertyAccess(node: Node) { + return node && node.parent && node.parent.kind === SyntaxKind.PropertyAccessExpression && (node.parent).name === node; + } + + export function isNameOfModuleDeclaration(node: Node) { + return node.parent.kind === SyntaxKind.ModuleDeclaration && (node.parent).name === node; + } + + export function isNameOfFunctionDeclaration(node: Node): boolean { + return node.kind === SyntaxKind.Identifier && + isFunctionLike(node.parent) && (node.parent).name === node; + } + + export function isLiteralNameOfPropertyDeclarationOrIndexAccess(node: Node): boolean { + if (node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NumericLiteral) { + switch (node.parent.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.EnumMember: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.ModuleDeclaration: + return (node.parent).name === node; + case SyntaxKind.ElementAccessExpression: + return (node.parent).argumentExpression === node; + case SyntaxKind.ComputedPropertyName: + return true; + } + } + + return false; + } + + export function isNameOfExternalModuleImportOrDeclaration(node: Node): boolean { + if (node.kind === SyntaxKind.StringLiteral) { + return isNameOfModuleDeclaration(node) || isExpressionOfExternalModuleImportEqualsDeclaration(node); + } + + return false; + } + + export function isExpressionOfExternalModuleImportEqualsDeclaration(node: Node) { + return isExternalModuleImportEqualsDeclaration(node.parent.parent) && + getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node; + } + + /** Returns true if the position is within a comment */ + export function isInsideComment(sourceFile: SourceFile, token: Node, position: number): boolean { + // The position has to be: 1. in the leading trivia (before token.getStart()), and 2. within a comment + return position <= token.getStart(sourceFile) && + (isInsideCommentRange(getTrailingCommentRanges(sourceFile.text, token.getFullStart())) || + isInsideCommentRange(getLeadingCommentRanges(sourceFile.text, token.getFullStart()))); + + function isInsideCommentRange(comments: CommentRange[]): boolean { + return forEach(comments, comment => { + // either we are 1. completely inside the comment, or 2. at the end of the comment + if (comment.pos < position && position < comment.end) { + return true; + } + else if (position === comment.end) { + const text = sourceFile.text; + const width = comment.end - comment.pos; + // is single line comment or just /* + if (width <= 2 || text.charCodeAt(comment.pos + 1) === CharacterCodes.slash) { + return true; + } + else { + // is unterminated multi-line comment + return !(text.charCodeAt(comment.end - 1) === CharacterCodes.slash && + text.charCodeAt(comment.end - 2) === CharacterCodes.asterisk); + } + } + return false; + }); + } + } + + export function getContainerNode(node: Node): Declaration { + while (true) { + node = node.parent; + if (!node) { + return undefined; + } + switch (node.kind) { + case SyntaxKind.SourceFile: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + case SyntaxKind.ClassDeclaration: + case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.EnumDeclaration: + case SyntaxKind.ModuleDeclaration: + return node; + } + } + } + + export function getNodeKind(node: Node): string { + switch (node.kind) { + case SyntaxKind.SourceFile: + return isExternalModule(node) ? ScriptElementKind.moduleElement : ScriptElementKind.scriptElement; + case SyntaxKind.ModuleDeclaration: + return ScriptElementKind.moduleElement; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return ScriptElementKind.classElement; + case SyntaxKind.InterfaceDeclaration: return ScriptElementKind.interfaceElement; + case SyntaxKind.TypeAliasDeclaration: return ScriptElementKind.typeElement; + case SyntaxKind.EnumDeclaration: return ScriptElementKind.enumElement; + case SyntaxKind.VariableDeclaration: + return getKindOfVariableDeclaration(node); + case SyntaxKind.BindingElement: + return getKindOfVariableDeclaration(getRootDeclaration(node)); + case SyntaxKind.ArrowFunction: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.FunctionExpression: + return ScriptElementKind.functionElement; + case SyntaxKind.GetAccessor: return ScriptElementKind.memberGetAccessorElement; + case SyntaxKind.SetAccessor: return ScriptElementKind.memberSetAccessorElement; + case SyntaxKind.MethodDeclaration: + case SyntaxKind.MethodSignature: + return ScriptElementKind.memberFunctionElement; + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return ScriptElementKind.memberVariableElement; + case SyntaxKind.IndexSignature: return ScriptElementKind.indexSignatureElement; + case SyntaxKind.ConstructSignature: return ScriptElementKind.constructSignatureElement; + case SyntaxKind.CallSignature: return ScriptElementKind.callSignatureElement; + case SyntaxKind.Constructor: return ScriptElementKind.constructorImplementationElement; + case SyntaxKind.TypeParameter: return ScriptElementKind.typeParameterElement; + case SyntaxKind.EnumMember: return ScriptElementKind.enumMemberElement; + case SyntaxKind.Parameter: return hasModifier(node, ModifierFlags.ParameterPropertyModifier) ? ScriptElementKind.memberVariableElement : ScriptElementKind.parameterElement; + case SyntaxKind.ImportEqualsDeclaration: + case SyntaxKind.ImportSpecifier: + case SyntaxKind.ImportClause: + case SyntaxKind.ExportSpecifier: + case SyntaxKind.NamespaceImport: + return ScriptElementKind.alias; + case SyntaxKind.JSDocTypedefTag: + return ScriptElementKind.typeElement; + default: + return ScriptElementKind.unknown; + } + + function getKindOfVariableDeclaration(v: VariableDeclaration): string { + return isConst(v) + ? ScriptElementKind.constElement + : isLet(v) + ? ScriptElementKind.letElement + : ScriptElementKind.variableElement; + } + } + + export function getStringLiteralTypeForNode(node: StringLiteral | LiteralTypeNode, typeChecker: TypeChecker): LiteralType { + const searchNode = node.parent.kind === SyntaxKind.LiteralType ? node.parent : node; + const type = typeChecker.getTypeAtLocation(searchNode); + if (type && type.flags & TypeFlags.StringLiteral) { + return type; + } + return undefined; + } + + export function isThis(node: Node): boolean { + switch (node.kind) { + case SyntaxKind.ThisKeyword: + // case SyntaxKind.ThisType: TODO: GH#9267 + return true; + case SyntaxKind.Identifier: + // 'this' as a parameter + return (node as Identifier).originalKeywordKind === SyntaxKind.ThisKeyword && node.parent.kind === SyntaxKind.Parameter; + default: + return false; + } + } + // Matches the beginning of a triple slash directive const tripleSlashDirectivePrefixRegex = /^\/\/\/\s*