mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
* Fix display of private names in language server Signed-off-by: Joseph Watts <jwatts43@bloomberg.net> Signed-off-by: Max Heiber <max.heiber@gmail.com> * Parse private names Signed-off-by: Max Heiber <max.heiber@gmail.com> * update parser error recovery tests to use ¬ not # The intent of the tests seemed to be to regiment the behavior of the parser when a weird symbol is encountered. The `#` is now taken by private identifiers, so `¬` seems like a good new choice for keeping the diff small, since it also fits in 16 bits (wide emojis would be treated as multiple characters, since the scanner uses ++). Signed-off-by: Max Heiber <max.heiber@gmail.com> * Private Name Support in the Checker (#5) - [x] treat private names as unique: - case 1: cannot say that a variable is of a class type unless the variable points to an instance of the class - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesUnique.ts) - case 2: private names in class hierarchies do not conflict - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoConflictWhenInheriting.ts) - [x] `#constructor` is reserved - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNameConstructorReserved.ts) - check in `bindWorker`, where every node is visited - [x] Accessibility modifiers can't be used with private names - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoAccessibilityModifiers.ts) - implemented in `checkAccessibilityModifiers`, using `ModifierFlags.AccessibilityModifier` - [x] `delete #foo` not allowed - [x] Private name accesses not allowed outside of the defining class - see test: https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNameNotAccessibleOutsideDefiningClass.ts - see [test](https://github.com/mheiber/TypeScript/tree/checker/tests/cases/conformance/classes/members/privateNames/privateNamesNoDelete.ts) - implemented in `checkDeleteExpression` - [x] Do [the right thing](https://gist.github.com/mheiber/b6fc7adb426c2e1cdaceb5d7786fc630) for nested classes mv private name tests together more checker tests for private names update naming and cleanup for check private names for private name support in the checker: - make names more consistent - remove unnecessary checks - add utility function to public API - other small cleanup Move getPropertyNameForUniqueESSymbol to utility for consistency with other calculation of special property names (starting with __), move the calculation of property names for unique es symbols to `utilities.ts`. private name tests strict+es6 Update private name tests to use 'strict' type checking and to target es6 instead of default. Makes the js output easier to read and tests more surface area with other checker features. error message for private names in obj literals Disallow decorating private-named properties because the spec is still in flux. Signed-off-by: Max Heiber <max.heiber@gmail.com> * Add private instance field transformation, pr feedback Implements private instance fields on top of the class properties refactor. This commit also includes incorporation of PR feedback on the checker, parser, and transformer in order to make the rebase manageable. Co-Authored-By: Max Heiber <max.heiber@gmail.com> Co-Authored-By: Ron Buckton <ron.buckton@microsoft.com> Signed-off-by: Joey Watts <jwatts43@bloomberg.net> Signed-off-by: Max Heiber <max.heiber@gmail.com> Incorporate PR feedback Fix checker crash with new block scoped bindings Add classExpressionInLoop test Update baselines for private name errors Apply suggestions from code review Fix privateNameFieldCallExpression test Remove unnecessary comment Fix PropertyAccessEntityNameExpression type Remove PrivateName from PropertyNameLiteral Add createPrivateName Remove PrivateName type from textSourceNode Add Debug asserts for invalid syntax kinds Don't output private name syntax when undeclared Make PrivateName extend Node Update baselines for public API Fix completions in language server Fix fourslash test crash intern private name descriptions undo expensive node.name.parent assignment Back the way things were on `master`: only assign node.name.parent when we need to Add tests for private names and JSDoc Patch hoverOverPrivateName fourslash test Fix goToDefinition for private-named fields remove Debug.fail for private name outside class Remove Debug.fail in binder.ts::`getDeclarationName`. It turns out this code path *does* get hit (intentionally). For best error messages, return `undefined` and rely on the parser generating a good error message "Private names are not allowed outside class bodies" Add rbuckton test cases for private names These test cases specifically exercise where we needed to use name-mangling. They are cases where private names have the same description. - private names no conflict when inheriting - private names distinct even when the two classes have the same name check dup instance+static private identifiers class A { #foo; static #foo; } not allowed because static and private names share the same lexical scope https://tc39.es/proposal-class-fields/#prod-ClassBody refactor getPropertyForPrivateName, rel spans refactor getPropertyForPrivateName so it is easier to read (use findAncestor instead of loop). Fix bugs in getPropertyForPrivateName: - make it work with deeply-nested classes with and without shadowing - make it catch shadowing when the conflict is between static and instance private name descriptions (these can actually clash) And add related spans to diagnostics for getPropertyForPrivateName catch conflicts between static and instance private identifiers: - cannot have an instance and static private identifier with the same spelling, as there is only one namespace for private names rename 'PrivateName' to 'PrivateIdentifier' to match the change of wording in the spec prposal: https://tc39.es/proposal-class-fields/#sec-syntax The rename in the spec was to avoid confusion between the AST Node PrivateIdentifier and the internal spec type PrivateName. So one uses the [[Description]] of a PrivateIdentifier to look up the PrivateName for a given scope. This corresponds closely to our implementation in the binder and checker: - we mangle PrivateIdentifier.escapedText to get a `key` which we use to get the symbol for a property f getLiteralTypeFromProperty-check privateIdentifier rename and simplify privateNameAndAny test case make it clearer that all this test is showing is that we allow accessing arbitrary private identifiers on `any`. Note: we could have something more sound here by checking that there is a private-named field declaration in a class scope above the property access. Discussion: https://github.com/microsoft/TypeScript/pull/30829/files#r302760015 Fix typo in checker s/identifer/identifier remove accidental change patch fourslash test broken by isPrivateIdentifier just needed to add a check to see if the symbol .name is defined extract reportUnmatchedProperty per @nsandersn feedback propertiesRelatedTo was getting to long pull out the unmatchedProperty reporting into a seprate function fix typo in private names test Fix type soundness with private names Remove PrivateIdentifier from emit with Expr hint Fixup helpers and set function name for privates remove accidentally-committed baselines These baselines somehow ended up in this pr, though they have nothing to do with the changes Revert "getLiteralTypeFromProperty-check privateIdentifier" This reverts commit bd1155c300bc3517a0543580f4790268f86e473f. reason: the check for isIdentifier in getLiteralTypeFromProperty is superfluous because we do this check in getLiteralTypeFromPropertyName Update comment in private name uniqueness test 3 I think the comments were not accurate and that we export the error on `this.#x = child.#x` because: - private names are lexically scoped: the code in question is not in a lexical scope under the definition of Child's #x. - private names are private to their containing class: never inherited This expected behavior matches that of Chrome Canary and my understanding of the spec test private names use before def, circular ref test private names use before def, circular ref update diagnosticMessages s/delete/'delete' per @DanielRosenwasser and @sandersn guidance, use this style in diagnostic messages: "operand of a 'delete' operator" (single quotes) rather than old style: "operand of a delete operator" (single quotes) This is for consistency, as we use the new style in the privateIdentifiers error messages and it is consistent with our messages about other operators incorporate private names early exit feedback and code style change to use a ternary instead of if so we can 'const' require private-named fields to be declared in JS All fields must be declared in TS files. In JS files, we typically do not have this requirement. So special-case private fields, which must always be declared (even in JS files, per spec) update debug failure for es2015 private identifier Co-Authored-By: Ron Buckton <ron.buckton@microsoft.com> fix checker handling of private name in subclasse update checker and tests to account for the following ts features: - private names do not participate in the prototype chain, but *are* assigned in the parent class' constructor. So parent can access its *own* private fields on instances of the subclass Add more tests for private-named fields in JS add test to hit symbolToExpression w private names symbolToExpression knows about private names add a test to exercise this code path ban private-named static methods (not supported yet) ban private-named methods (not supported yet) ban private-named accessors (not supported yet) fix privateIdentifier fourslash test change assertion throw to return Co-Authored-By: Ron Buckton <ron.buckton@microsoft.com> Update comment in checker.ts re reserved members Remove case for privateIdentifier in EntityNameExpr Remove case for privateIdentifier in EntityNameExpr. That code path is never hit, and privateIdnetifiers cannot be entity names. remove unnecessary parentheses Ban private identifier in enum As the new test, 'privateNameEnumNoEmit', shows, the checker now correctly makes a diagnostic for private identifiers in enums. However, when noEmit is false we hit this assertion in the transformer. This assertion will have to be removed so that we have a diagnostic here instead of an assertion error. When the assertion is removed, the 'privateNameEnum' baseline will have to be updated Fix destructuring assignment, use createCallBinding, remove unneeded helper Add class private field helpers to external helpers Remove private identifier enum assertion, use missing identifiers Fix hash map inefficiency by only using get Update baselines with empty identifier change Add privateNameEnum test baselines Fix private identifier destructuring (array assignment defaults, unique names) Use createPrivateIdentifierAssignment in destructuring transform Fix lint error Separate destructuring target visitor from regular visitor Fix destructuring assignment with this bindings Fix destructuring assignment with this bindings Fix syntax error with destructuring assignment output Disallow private identifiers in property signatures remove duplicate test baselines Add tests for undeclarated private identifiers remove unnecessary cast Nicer error message for mis-placed hashbang Workaround v8 bug with destructured assignments Optimize private identifier stack lookup Avoid the cost of performing an array lookup to look at the top of the private identifier environment stack. Change function name to be more specific Changes "getOperatorForCompoundAssignment" to "getNonAssignmentOperatorForCompoundAssignment" now that this function is accessible to the entire compiler. Improve test case for private name assignment Adds a compound assignment test case for a class with private names being declared on the left-hand-side of the assignment expression. Remove extra non-private-field test Remove isPrivateIdentifierAssignmentExpression helper Don't transform private names in ESNext target Preserve private fields initialized to functions Move function expressions to outer scope for efficiency Add WeakMap collision check Modify potential WeakMap collision condition Fix this property assignment binding with private names Add correct error message for WeakMap collision Add error for statements before super with private identifiers Refactor getPropertyForPrivateIdentifier Add test for private identifier fields initialized to class exprs Fix shebang errors Fix private errors on index signatures Add codefix for missing private property Update error messages for unsupported private features Fix inheritance-related errors Fixes inheritance-related errors with private identifiers by resolving properties from base classes. Private identifiers do not show up as properties on a union type, so those do not type-check. Add test for interface extending class with private access Remove debugging log Remove name assignment from private named functions Rename to getPrivateIdentifierPropertyOfType Fix index signature test comment Fix test target syntax error Change error messages patch private identifiers outside class bodies Add test for private identifier with ooo super Add test for a class with a private identifier with a non-preambly (for example, not a comment) statement before 'super': should error, saying 'super' must come first Fix nits incorporate PR feedback Incorporate checker feedback - reorganize if statements in checkFunctionOrConstructorSymbol - remove overload for getPrivateIdentifierPropertyOfType reorganize if statements in checkFunctionOrConstructorSymbol test for private names with JSX use getPropertyOftype in getPropertyForPrivateIdentifier getPrivateIdentifierPropertyOfType error on synthetic make getPrivateIdentifierPropertyOfType error if given a node that is not from the parse tree Simplify checkPrivateIdentifierPropertyAccess use getPropertiesOfType instead of rehashing that logic test for private identifiers w decl merging fix test target for privateNameDeclarationMerging update baselines Fix private identifier ++/-- numeric coercion Remove 'downleveled' from super error Fix bad comments in helper call emit Error on private identifiers in JSX tag names Add more private identifier tests Add es2015 target for private name destructured binding test Add privateNameConstructorSignature test Add test for navigation bar w private identifier Remove spurious line from test // in js file class A { exports.#foo = 3; // should error } The above line failed to produce an error when run using the test harness. When using tsc or the language server, we got the expected error message. Removing the troublesome line, as it seems to say more about the test runner than about the code it is testing. Fix inefficient constructor generation dts: don't emit type for private-identified field Do not emit types for private-identified fields when generating declaration files. // example.ts export class A { #foo: number; } // example.d.ts export declare class A { #foo; } **This is not ideal!** The following would be better: // example.d.ts export declare unique class A { #foo; } See discussion: https://github.com/microsoft/TypeScript/pull/33038#issuecomment-530321165 notice when private-identified field unused Notice when private-identified fields are unused, and implement the same behavior as for unused private-modified fields. This is used in the language server to make things grayed out. This case generates an error when --noUnusedLocals flag is passed to tsc: - "<name> is declared but never used" accept baselines Revert "dts: don't emit type for private-identified field" This reverts commit e50305df5fb88121486291abad14478f5339a455. Instead of just excluding the type from private identifier emit, only emit a single private identifier per class. This accomplishes nominality while hiding implementation detail that is irrelevant to .d.ts consumers only emit a single private identifier in dts In dts emit, emit at most one private identifier, and rename it to `#private`. refactor getPrivateIdentifierPropertyOfType - safer check for wehther is parse tree node - return undefined when not found (instead of a Debug.fail) Incorporate PR feedback Don't rely on parent pointers in transform Passes context about whether the postfix unary expression value is discarded down the tree into the visitPostfixUnaryExpression function. Remove orphaned test baseline files remove unreachable if Check `any`-typed private identified fields Update private identifier incompatible modifier checks - disallow 'abstract' with private identifiers - s/private-named-property/private identifier Add additional call expression test cases Fix disallow 'abstract' with private identifier Static private identifiers not inherited Including this in the PR for private instance fields even though static private identifiers are banned. Reason: this change improves quality of error messages, see test case. Thanks Ron! Simplifiy private identifier 'any' type handling Error on private identifier declarations for ES5/ES3 Bind `this` for private identifier property tagged template literals Fix target for jsdocPrivateName1 test Update getPrivateIdentifierPropertyOfType API Make it easier to use by accepting a string and location, rather than a PrivateIdentifier. Thanks Ron! remove orphaned tests rename test remove duplicate tests Remove unrelated error from test update diagnostic message 'private identifier' The nodes for hash private fields are now called 'private identifier'. Update one last diagnostic message to use the new terminology. refine solution for static private identifier fields - use `continue` instead of `filter` for perf - better naming - make it clear the current solution will need to be altered when we lift the ban on static private identifier fields, including a test case that should change in the future Fix display of private identifiers in lang server Fix bug where private identifiers in completion tooltips in the playground were showing up as the symbol table entries (with underscores and such). https://github.com/microsoft/TypeScript/pull/30829#issuecomment-534157560 Signed-off-by: Max Heiber <max.heiber@gmail.com> * fix privateIdentifier w !useDefineForClassFields Signed-off-by: Max Heiber <max.heiber@gmail.com> * Disallow PrivateIdentifier in Optional Chains Signed-off-by: Max Heiber <max.heiber@gmail.com> * restrict privateIdentifier completions correctly Don't autocomplete privateIdentifiers in places where they are not allowed. Signed-off-by: Max Heiber <max.heiber@gmail.com> * make PrivateIdentifier not a PropertyNameLiteral Signed-off-by: Max Heiber <max.heiber@gmail.com> * Added test. * Accepted baselines. * Update symbol serializer to understand private fields in JS `.d.ts` emit. * Accepted baselines. * fix for private field no initializer esnext Signed-off-by: Max Heiber <max.heiber@gmail.com> * fix private fields .d.ts emit for JS w expando fix bug where the following in a JS file would lead to a `#private` in the .d.ts: ```js class C { constructor () { this.a = 3 } } ``` Co-authored-by: Joey Watts <joey.watts.96@gmail.com> Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
2369 lines
112 KiB
TypeScript
2369 lines
112 KiB
TypeScript
namespace ts {
|
|
/** The version of the language service API */
|
|
export const servicesVersion = "0.8";
|
|
|
|
function createNode<TKind extends SyntaxKind>(kind: TKind, pos: number, end: number, parent: Node): NodeObject | TokenObject<TKind> | IdentifierObject | PrivateIdentifierObject {
|
|
const node = isNodeKind(kind) ? new NodeObject(kind, pos, end) :
|
|
kind === SyntaxKind.Identifier ? new IdentifierObject(SyntaxKind.Identifier, pos, end) :
|
|
kind === SyntaxKind.PrivateIdentifier ? new PrivateIdentifierObject(SyntaxKind.PrivateIdentifier, pos, end) :
|
|
new TokenObject(kind, pos, end);
|
|
node.parent = parent;
|
|
node.flags = parent.flags & NodeFlags.ContextFlags;
|
|
return node;
|
|
}
|
|
|
|
class NodeObject implements Node {
|
|
public kind: SyntaxKind;
|
|
public pos: number;
|
|
public end: number;
|
|
public flags: NodeFlags;
|
|
public modifierFlagsCache: ModifierFlags;
|
|
public transformFlags: TransformFlags;
|
|
public parent: Node;
|
|
public symbol!: Symbol; // Actually optional, but it was too annoying to access `node.symbol!` everywhere since in many cases we know it must be defined
|
|
public jsDoc?: JSDoc[];
|
|
public original?: Node;
|
|
private _children: Node[] | undefined;
|
|
|
|
constructor(kind: SyntaxKind, pos: number, end: number) {
|
|
this.pos = pos;
|
|
this.end = end;
|
|
this.flags = NodeFlags.None;
|
|
this.modifierFlagsCache = ModifierFlags.None;
|
|
this.transformFlags = TransformFlags.None;
|
|
this.parent = undefined!;
|
|
this.kind = kind;
|
|
}
|
|
|
|
private assertHasRealPosition(message?: string) {
|
|
// eslint-disable-next-line debug-assert
|
|
Debug.assert(!positionIsSynthesized(this.pos) && !positionIsSynthesized(this.end), message || "Node must have a real position for this operation");
|
|
}
|
|
|
|
public getSourceFile(): SourceFile {
|
|
return getSourceFileOfNode(this);
|
|
}
|
|
|
|
public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number {
|
|
this.assertHasRealPosition();
|
|
return getTokenPosOfNode(this, sourceFile, includeJsDocComment);
|
|
}
|
|
|
|
public getFullStart(): number {
|
|
this.assertHasRealPosition();
|
|
return this.pos;
|
|
}
|
|
|
|
public getEnd(): number {
|
|
this.assertHasRealPosition();
|
|
return this.end;
|
|
}
|
|
|
|
public getWidth(sourceFile?: SourceFile): number {
|
|
this.assertHasRealPosition();
|
|
return this.getEnd() - this.getStart(sourceFile);
|
|
}
|
|
|
|
public getFullWidth(): number {
|
|
this.assertHasRealPosition();
|
|
return this.end - this.pos;
|
|
}
|
|
|
|
public getLeadingTriviaWidth(sourceFile?: SourceFile): number {
|
|
this.assertHasRealPosition();
|
|
return this.getStart(sourceFile) - this.pos;
|
|
}
|
|
|
|
public getFullText(sourceFile?: SourceFile): string {
|
|
this.assertHasRealPosition();
|
|
return (sourceFile || this.getSourceFile()).text.substring(this.pos, this.end);
|
|
}
|
|
|
|
public getText(sourceFile?: SourceFile): string {
|
|
this.assertHasRealPosition();
|
|
if (!sourceFile) {
|
|
sourceFile = this.getSourceFile();
|
|
}
|
|
return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd());
|
|
}
|
|
|
|
public getChildCount(sourceFile?: SourceFile): number {
|
|
return this.getChildren(sourceFile).length;
|
|
}
|
|
|
|
public getChildAt(index: number, sourceFile?: SourceFile): Node {
|
|
return this.getChildren(sourceFile)[index];
|
|
}
|
|
|
|
public getChildren(sourceFile?: SourceFileLike): Node[] {
|
|
this.assertHasRealPosition("Node without a real position cannot be scanned and thus has no token nodes - use forEachChild and collect the result if that's fine");
|
|
return this._children || (this._children = createChildren(this, sourceFile));
|
|
}
|
|
|
|
public getFirstToken(sourceFile?: SourceFileLike): Node | undefined {
|
|
this.assertHasRealPosition();
|
|
const children = this.getChildren(sourceFile);
|
|
if (!children.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const child = find(children, kid => kid.kind < SyntaxKind.FirstJSDocNode || kid.kind > SyntaxKind.LastJSDocNode)!;
|
|
return child.kind < SyntaxKind.FirstNode ?
|
|
child :
|
|
child.getFirstToken(sourceFile);
|
|
}
|
|
|
|
public getLastToken(sourceFile?: SourceFileLike): Node | undefined {
|
|
this.assertHasRealPosition();
|
|
const children = this.getChildren(sourceFile);
|
|
|
|
const child = lastOrUndefined(children);
|
|
if (!child) {
|
|
return undefined;
|
|
}
|
|
|
|
return child.kind < SyntaxKind.FirstNode ? child : child.getLastToken(sourceFile);
|
|
}
|
|
|
|
public forEachChild<T>(cbNode: (node: Node) => T, cbNodeArray?: (nodes: NodeArray<Node>) => T): T | undefined {
|
|
return forEachChild(this, cbNode, cbNodeArray);
|
|
}
|
|
}
|
|
|
|
function createChildren(node: Node, sourceFile: SourceFileLike | undefined): Node[] {
|
|
if (!isNodeKind(node.kind)) {
|
|
return emptyArray;
|
|
}
|
|
|
|
const children: Node[] = [];
|
|
|
|
if (isJSDocCommentContainingNode(node)) {
|
|
/** Don't add trivia for "tokens" since this is in a comment. */
|
|
node.forEachChild(child => { children.push(child); });
|
|
return children;
|
|
}
|
|
|
|
scanner.setText((sourceFile || node.getSourceFile()).text);
|
|
let pos = node.pos;
|
|
const processNode = (child: Node) => {
|
|
addSyntheticNodes(children, pos, child.pos, node);
|
|
children.push(child);
|
|
pos = child.end;
|
|
};
|
|
const processNodes = (nodes: NodeArray<Node>) => {
|
|
addSyntheticNodes(children, pos, nodes.pos, node);
|
|
children.push(createSyntaxList(nodes, node));
|
|
pos = nodes.end;
|
|
};
|
|
// jsDocComments need to be the first children
|
|
forEach((node as JSDocContainer).jsDoc, processNode);
|
|
// For syntactic classifications, all trivia are classified 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 = node.pos;
|
|
node.forEachChild(processNode, processNodes);
|
|
addSyntheticNodes(children, pos, node.end, node);
|
|
scanner.setText(undefined);
|
|
return children;
|
|
}
|
|
|
|
function addSyntheticNodes(nodes: Push<Node>, pos: number, end: number, parent: Node): void {
|
|
scanner.setTextPos(pos);
|
|
while (pos < end) {
|
|
const token = scanner.scan();
|
|
const textPos = scanner.getTextPos();
|
|
if (textPos <= end) {
|
|
if (token === SyntaxKind.Identifier) {
|
|
Debug.fail(`Did not expect ${Debug.formatSyntaxKind(parent.kind)} to have an Identifier in its trivia`);
|
|
}
|
|
nodes.push(createNode(token, pos, textPos, parent));
|
|
}
|
|
pos = textPos;
|
|
if (token === SyntaxKind.EndOfFileToken) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function createSyntaxList(nodes: NodeArray<Node>, parent: Node): Node {
|
|
const list = createNode(SyntaxKind.SyntaxList, nodes.pos, nodes.end, parent) as any as SyntaxList;
|
|
list._children = [];
|
|
let pos = nodes.pos;
|
|
for (const node of nodes) {
|
|
addSyntheticNodes(list._children, pos, node.pos, parent);
|
|
list._children.push(node);
|
|
pos = node.end;
|
|
}
|
|
addSyntheticNodes(list._children, pos, nodes.end, parent);
|
|
return list;
|
|
}
|
|
|
|
class TokenOrIdentifierObject implements Node {
|
|
public kind!: SyntaxKind;
|
|
public pos: number;
|
|
public end: number;
|
|
public flags: NodeFlags;
|
|
public modifierFlagsCache: ModifierFlags;
|
|
public transformFlags: TransformFlags;
|
|
public parent: Node;
|
|
public symbol!: Symbol;
|
|
public jsDocComments?: JSDoc[];
|
|
|
|
constructor(pos: number, end: number) {
|
|
// Set properties in same order as NodeObject
|
|
this.pos = pos;
|
|
this.end = end;
|
|
this.flags = NodeFlags.None;
|
|
this.modifierFlagsCache = ModifierFlags.None;
|
|
this.transformFlags = TransformFlags.None;
|
|
this.parent = undefined!;
|
|
}
|
|
|
|
public getSourceFile(): SourceFile {
|
|
return getSourceFileOfNode(this);
|
|
}
|
|
|
|
public getStart(sourceFile?: SourceFileLike, 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 {
|
|
if (!sourceFile) {
|
|
sourceFile = this.getSourceFile();
|
|
}
|
|
return sourceFile.text.substring(this.getStart(sourceFile), this.getEnd());
|
|
}
|
|
|
|
public getChildCount(): number {
|
|
return 0;
|
|
}
|
|
|
|
public getChildAt(): Node {
|
|
return undefined!; // TODO: GH#18217
|
|
}
|
|
|
|
public getChildren(): Node[] {
|
|
return this.kind === SyntaxKind.EndOfFileToken ? (this as EndOfFileToken).jsDoc || emptyArray : emptyArray;
|
|
}
|
|
|
|
public getFirstToken(): Node | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
public getLastToken(): Node | undefined {
|
|
return undefined;
|
|
}
|
|
|
|
public forEachChild<T>(): T | undefined {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class SymbolObject implements Symbol {
|
|
flags: SymbolFlags;
|
|
escapedName: __String;
|
|
declarations!: Declaration[];
|
|
valueDeclaration!: Declaration;
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no doc comment, then the empty array will be returned.
|
|
documentationComment?: SymbolDisplayPart[];
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no JSDoc tags, then the empty array will be returned.
|
|
tags?: JSDocTagInfo[];
|
|
|
|
constructor(flags: SymbolFlags, name: __String) {
|
|
this.flags = flags;
|
|
this.escapedName = name;
|
|
}
|
|
|
|
getFlags(): SymbolFlags {
|
|
return this.flags;
|
|
}
|
|
|
|
get name(): string {
|
|
return symbolName(this);
|
|
}
|
|
|
|
getEscapedName(): __String {
|
|
return this.escapedName;
|
|
}
|
|
|
|
getName(): string {
|
|
return this.name;
|
|
}
|
|
|
|
getDeclarations(): Declaration[] | undefined {
|
|
return this.declarations;
|
|
}
|
|
|
|
getDocumentationComment(checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
|
if (!this.documentationComment) {
|
|
this.documentationComment = emptyArray; // Set temporarily to avoid an infinite loop finding inherited docs
|
|
this.documentationComment = getDocumentationComment(this.declarations, checker);
|
|
}
|
|
return this.documentationComment;
|
|
}
|
|
|
|
getJsDocTags(): JSDocTagInfo[] {
|
|
if (this.tags === undefined) {
|
|
this.tags = JsDoc.getJsDocTagsFromDeclarations(this.declarations);
|
|
}
|
|
|
|
return this.tags;
|
|
}
|
|
}
|
|
|
|
class TokenObject<TKind extends SyntaxKind> extends TokenOrIdentifierObject implements Token<TKind> {
|
|
public kind: TKind;
|
|
|
|
constructor(kind: TKind, pos: number, end: number) {
|
|
super(pos, end);
|
|
this.kind = kind;
|
|
}
|
|
}
|
|
|
|
class IdentifierObject extends TokenOrIdentifierObject implements Identifier {
|
|
public kind: SyntaxKind.Identifier = SyntaxKind.Identifier;
|
|
public escapedText!: __String;
|
|
public autoGenerateFlags!: GeneratedIdentifierFlags;
|
|
_primaryExpressionBrand: any;
|
|
_memberExpressionBrand: any;
|
|
_leftHandSideExpressionBrand: any;
|
|
_updateExpressionBrand: any;
|
|
_unaryExpressionBrand: any;
|
|
_expressionBrand: any;
|
|
_declarationBrand: any;
|
|
/*@internal*/typeArguments!: NodeArray<TypeNode>;
|
|
constructor(_kind: SyntaxKind.Identifier, pos: number, end: number) {
|
|
super(pos, end);
|
|
}
|
|
|
|
get text(): string {
|
|
return idText(this);
|
|
}
|
|
}
|
|
IdentifierObject.prototype.kind = SyntaxKind.Identifier;
|
|
class PrivateIdentifierObject extends TokenOrIdentifierObject implements PrivateIdentifier {
|
|
public kind!: SyntaxKind.PrivateIdentifier;
|
|
public escapedText!: __String;
|
|
public symbol!: Symbol;
|
|
constructor(_kind: SyntaxKind.PrivateIdentifier, pos: number, end: number) {
|
|
super(pos, end);
|
|
}
|
|
|
|
get text(): string {
|
|
return idText(this);
|
|
}
|
|
}
|
|
PrivateIdentifierObject.prototype.kind = SyntaxKind.PrivateIdentifier;
|
|
|
|
class TypeObject implements Type {
|
|
checker: TypeChecker;
|
|
flags: TypeFlags;
|
|
objectFlags?: ObjectFlags;
|
|
id!: number;
|
|
symbol!: Symbol;
|
|
constructor(checker: TypeChecker, flags: TypeFlags) {
|
|
this.checker = checker;
|
|
this.flags = flags;
|
|
}
|
|
getFlags(): TypeFlags {
|
|
return this.flags;
|
|
}
|
|
getSymbol(): Symbol | undefined {
|
|
return this.symbol;
|
|
}
|
|
getProperties(): Symbol[] {
|
|
return this.checker.getPropertiesOfType(this);
|
|
}
|
|
getProperty(propertyName: string): Symbol | undefined {
|
|
return this.checker.getPropertyOfType(this, propertyName);
|
|
}
|
|
getApparentProperties(): Symbol[] {
|
|
return this.checker.getAugmentedPropertiesOfType(this);
|
|
}
|
|
getCallSignatures(): readonly Signature[] {
|
|
return this.checker.getSignaturesOfType(this, SignatureKind.Call);
|
|
}
|
|
getConstructSignatures(): readonly Signature[] {
|
|
return this.checker.getSignaturesOfType(this, SignatureKind.Construct);
|
|
}
|
|
getStringIndexType(): Type | undefined {
|
|
return this.checker.getIndexTypeOfType(this, IndexKind.String);
|
|
}
|
|
getNumberIndexType(): Type | undefined {
|
|
return this.checker.getIndexTypeOfType(this, IndexKind.Number);
|
|
}
|
|
getBaseTypes(): BaseType[] | undefined {
|
|
return this.isClassOrInterface() ? this.checker.getBaseTypes(this) : undefined;
|
|
}
|
|
isNullableType(): boolean {
|
|
return this.checker.isNullableType(this);
|
|
}
|
|
getNonNullableType(): Type {
|
|
return this.checker.getNonNullableType(this);
|
|
}
|
|
getNonOptionalType(): Type {
|
|
return this.checker.getNonOptionalType(this);
|
|
}
|
|
getConstraint(): Type | undefined {
|
|
return this.checker.getBaseConstraintOfType(this);
|
|
}
|
|
getDefault(): Type | undefined {
|
|
return this.checker.getDefaultFromTypeParameter(this);
|
|
}
|
|
|
|
isUnion(): this is UnionType {
|
|
return !!(this.flags & TypeFlags.Union);
|
|
}
|
|
isIntersection(): this is IntersectionType {
|
|
return !!(this.flags & TypeFlags.Intersection);
|
|
}
|
|
isUnionOrIntersection(): this is UnionOrIntersectionType {
|
|
return !!(this.flags & TypeFlags.UnionOrIntersection);
|
|
}
|
|
isLiteral(): this is LiteralType {
|
|
return !!(this.flags & TypeFlags.StringOrNumberLiteral);
|
|
}
|
|
isStringLiteral(): this is StringLiteralType {
|
|
return !!(this.flags & TypeFlags.StringLiteral);
|
|
}
|
|
isNumberLiteral(): this is NumberLiteralType {
|
|
return !!(this.flags & TypeFlags.NumberLiteral);
|
|
}
|
|
isTypeParameter(): this is TypeParameter {
|
|
return !!(this.flags & TypeFlags.TypeParameter);
|
|
}
|
|
isClassOrInterface(): this is InterfaceType {
|
|
return !!(getObjectFlags(this) & ObjectFlags.ClassOrInterface);
|
|
}
|
|
isClass(): this is InterfaceType {
|
|
return !!(getObjectFlags(this) & ObjectFlags.Class);
|
|
}
|
|
/**
|
|
* This polyfills `referenceType.typeArguments` for API consumers
|
|
*/
|
|
get typeArguments() {
|
|
if (getObjectFlags(this) & ObjectFlags.Reference) {
|
|
return this.checker.getTypeArguments(this as Type as TypeReference);
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class SignatureObject implements Signature {
|
|
flags: SignatureFlags;
|
|
checker: TypeChecker;
|
|
declaration!: SignatureDeclaration;
|
|
typeParameters?: TypeParameter[];
|
|
parameters!: Symbol[];
|
|
thisParameter!: Symbol;
|
|
resolvedReturnType!: Type;
|
|
resolvedTypePredicate: TypePredicate | undefined;
|
|
minTypeArgumentCount!: number;
|
|
minArgumentCount!: number;
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no doc comment, then the empty array will be returned.
|
|
documentationComment?: SymbolDisplayPart[];
|
|
|
|
// Undefined is used to indicate the value has not been computed. If, after computing, the
|
|
// symbol has no doc comment, then the empty array will be returned.
|
|
jsDocTags?: JSDocTagInfo[];
|
|
|
|
constructor(checker: TypeChecker, flags: SignatureFlags) {
|
|
this.checker = checker;
|
|
this.flags = flags;
|
|
}
|
|
|
|
getDeclaration(): SignatureDeclaration {
|
|
return this.declaration;
|
|
}
|
|
getTypeParameters(): TypeParameter[] | undefined {
|
|
return this.typeParameters;
|
|
}
|
|
getParameters(): Symbol[] {
|
|
return this.parameters;
|
|
}
|
|
getReturnType(): Type {
|
|
return this.checker.getReturnTypeOfSignature(this);
|
|
}
|
|
|
|
getDocumentationComment(): SymbolDisplayPart[] {
|
|
return this.documentationComment || (this.documentationComment = getDocumentationComment(singleElementArray(this.declaration), this.checker));
|
|
}
|
|
|
|
getJsDocTags(): JSDocTagInfo[] {
|
|
if (this.jsDocTags === undefined) {
|
|
this.jsDocTags = this.declaration ? JsDoc.getJsDocTagsFromDeclarations([this.declaration]) : [];
|
|
}
|
|
|
|
return this.jsDocTags;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the given node has a JSDoc "inheritDoc" tag on it.
|
|
* @param node the Node in question.
|
|
* @returns `true` if `node` has a JSDoc "inheritDoc" tag on it, otherwise `false`.
|
|
*/
|
|
function hasJSDocInheritDocTag(node: Node) {
|
|
return getJSDocTags(node).some(tag => tag.tagName.text === "inheritDoc");
|
|
}
|
|
|
|
function getDocumentationComment(declarations: readonly Declaration[] | undefined, checker: TypeChecker | undefined): SymbolDisplayPart[] {
|
|
if (!declarations) return emptyArray;
|
|
|
|
let doc = JsDoc.getJsDocCommentsFromDeclarations(declarations);
|
|
if (doc.length === 0 || declarations.some(hasJSDocInheritDocTag)) {
|
|
forEachUnique(declarations, declaration => {
|
|
const inheritedDocs = findInheritedJSDocComments(declaration, declaration.symbol.name, checker!); // TODO: GH#18217
|
|
// TODO: GH#16312 Return a ReadonlyArray, avoid copying inheritedDocs
|
|
if (inheritedDocs) doc = doc.length === 0 ? inheritedDocs.slice() : inheritedDocs.concat(lineBreakPart(), doc);
|
|
});
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
/**
|
|
* Attempts to find JSDoc comments for possibly-inherited properties. Checks superclasses then traverses
|
|
* implemented interfaces until a symbol is found with the same name and with documentation.
|
|
* @param declaration The possibly-inherited declaration to find comments for.
|
|
* @param propertyName The name of the possibly-inherited property.
|
|
* @param typeChecker A TypeChecker, used to find inherited properties.
|
|
* @returns A filled array of documentation comments if any were found, otherwise an empty array.
|
|
*/
|
|
function findInheritedJSDocComments(declaration: Declaration, propertyName: string, typeChecker: TypeChecker): readonly SymbolDisplayPart[] | undefined {
|
|
return firstDefined(declaration.parent ? getAllSuperTypeNodes(declaration.parent) : emptyArray, superTypeNode => {
|
|
const superType = typeChecker.getTypeAtLocation(superTypeNode);
|
|
const baseProperty = superType && typeChecker.getPropertyOfType(superType, propertyName);
|
|
const inheritedDocs = baseProperty && baseProperty.getDocumentationComment(typeChecker);
|
|
return inheritedDocs && inheritedDocs.length ? inheritedDocs : undefined;
|
|
});
|
|
}
|
|
|
|
class SourceFileObject extends NodeObject implements SourceFile {
|
|
public kind: SyntaxKind.SourceFile = SyntaxKind.SourceFile;
|
|
public _declarationBrand: any;
|
|
public fileName!: string;
|
|
public path!: Path;
|
|
public resolvedPath!: Path;
|
|
public originalFileName!: string;
|
|
public text!: string;
|
|
public scriptSnapshot!: IScriptSnapshot;
|
|
public lineMap!: readonly number[];
|
|
|
|
public statements!: NodeArray<Statement>;
|
|
public endOfFileToken!: Token<SyntaxKind.EndOfFileToken>;
|
|
|
|
public amdDependencies!: { name: string; path: string }[];
|
|
public moduleName!: string;
|
|
public referencedFiles!: FileReference[];
|
|
public typeReferenceDirectives!: FileReference[];
|
|
public libReferenceDirectives!: FileReference[];
|
|
|
|
public syntacticDiagnostics!: DiagnosticWithLocation[];
|
|
public parseDiagnostics!: DiagnosticWithLocation[];
|
|
public bindDiagnostics!: DiagnosticWithLocation[];
|
|
public bindSuggestionDiagnostics?: DiagnosticWithLocation[];
|
|
|
|
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<string>;
|
|
public nameTable: UnderscoreEscapedMap<number> | undefined;
|
|
public resolvedModules: Map<ResolvedModuleFull> | undefined;
|
|
public resolvedTypeReferenceDirectiveNames!: Map<ResolvedTypeReferenceDirective>;
|
|
public imports!: readonly StringLiteralLike[];
|
|
public moduleAugmentations!: StringLiteral[];
|
|
private namedDeclarations: Map<Declaration[]> | undefined;
|
|
public ambientModuleNames!: string[];
|
|
public checkJsDirective: CheckJsDirective | undefined;
|
|
public possiblyContainDynamicImport?: boolean;
|
|
public pragmas!: PragmaMap;
|
|
public localJsxFactory: EntityName | undefined;
|
|
public localJsxNamespace: __String | undefined;
|
|
|
|
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 getLineAndCharacterOfPosition(this, position);
|
|
}
|
|
|
|
public getLineStarts(): readonly number[] {
|
|
return getLineStarts(this);
|
|
}
|
|
|
|
public getPositionOfLineAndCharacter(line: number, character: number, allowEdits?: true): number {
|
|
return computePositionOfLineAndCharacter(getLineStarts(this), line, character, this.text, allowEdits);
|
|
}
|
|
|
|
public getLineEndOfPosition(pos: number): number {
|
|
const { line } = this.getLineAndCharacterOfPosition(pos);
|
|
const lineStarts = this.getLineStarts();
|
|
|
|
let lastCharPos: number | undefined;
|
|
if (line + 1 >= lineStarts.length) {
|
|
lastCharPos = this.getEnd();
|
|
}
|
|
if (!lastCharPos) {
|
|
lastCharPos = lineStarts[line + 1] - 1;
|
|
}
|
|
|
|
const fullText = this.getFullText();
|
|
// if the new line is "\r\n", we should return the last non-new-line-character position
|
|
return fullText[lastCharPos] === "\n" && fullText[lastCharPos - 1] === "\r" ? lastCharPos - 1 : lastCharPos;
|
|
}
|
|
|
|
public getNamedDeclarations(): Map<Declaration[]> {
|
|
if (!this.namedDeclarations) {
|
|
this.namedDeclarations = this.computeNamedDeclarations();
|
|
}
|
|
|
|
return this.namedDeclarations;
|
|
}
|
|
|
|
private computeNamedDeclarations(): Map<Declaration[]> {
|
|
const result = createMultiMap<Declaration>();
|
|
|
|
this.forEachChild(visit);
|
|
|
|
return result;
|
|
|
|
function addDeclaration(declaration: Declaration) {
|
|
const name = getDeclarationName(declaration);
|
|
if (name) {
|
|
result.add(name, declaration);
|
|
}
|
|
}
|
|
|
|
function getDeclarations(name: string) {
|
|
let declarations = result.get(name);
|
|
if (!declarations) {
|
|
result.set(name, declarations = []);
|
|
}
|
|
return declarations;
|
|
}
|
|
|
|
function getDeclarationName(declaration: Declaration) {
|
|
const name = getNonAssignedNameOfDeclaration(declaration);
|
|
return name && (isComputedPropertyName(name) && isPropertyAccessExpression(name.expression) ? name.expression.name.text
|
|
: isPropertyName(name) ? getNameFromPropertyName(name) : undefined);
|
|
}
|
|
|
|
function visit(node: Node): void {
|
|
switch (node.kind) {
|
|
case SyntaxKind.FunctionDeclaration:
|
|
case SyntaxKind.FunctionExpression:
|
|
case SyntaxKind.MethodDeclaration:
|
|
case SyntaxKind.MethodSignature:
|
|
const functionDeclaration = <FunctionLikeDeclaration>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 && !(<FunctionLikeDeclaration>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.ImportClause:
|
|
case SyntaxKind.NamespaceImport:
|
|
case SyntaxKind.GetAccessor:
|
|
case SyntaxKind.SetAccessor:
|
|
case SyntaxKind.TypeLiteral:
|
|
addDeclaration(<Declaration>node);
|
|
forEachChild(node, visit);
|
|
break;
|
|
|
|
case SyntaxKind.Parameter:
|
|
// Only consider parameter properties
|
|
if (!hasModifier(node, ModifierFlags.ParameterPropertyModifier)) {
|
|
break;
|
|
}
|
|
// falls through
|
|
|
|
case SyntaxKind.VariableDeclaration:
|
|
case SyntaxKind.BindingElement: {
|
|
const decl = <VariableDeclaration>node;
|
|
if (isBindingPattern(decl.name)) {
|
|
forEachChild(decl.name, visit);
|
|
break;
|
|
}
|
|
if (decl.initializer) {
|
|
visit(decl.initializer);
|
|
}
|
|
}
|
|
// falls through
|
|
case SyntaxKind.EnumMember:
|
|
case SyntaxKind.PropertyDeclaration:
|
|
case SyntaxKind.PropertySignature:
|
|
addDeclaration(<Declaration>node);
|
|
break;
|
|
|
|
case SyntaxKind.ExportDeclaration:
|
|
// Handle named exports case e.g.:
|
|
// export {a, b as B} from "mod";
|
|
const exportDeclaration = (<ExportDeclaration>node);
|
|
if (exportDeclaration.exportClause) {
|
|
if (isNamedExports(exportDeclaration.exportClause)) {
|
|
forEach(exportDeclaration.exportClause.elements, visit);
|
|
}
|
|
else {
|
|
visit(exportDeclaration.exportClause.name);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SyntaxKind.ImportDeclaration:
|
|
const importClause = (<ImportDeclaration>node).importClause;
|
|
if (importClause) {
|
|
// Handle default import case e.g.:
|
|
// import d from "mod";
|
|
if (importClause.name) {
|
|
addDeclaration(importClause.name);
|
|
}
|
|
|
|
// 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;
|
|
|
|
case SyntaxKind.BinaryExpression:
|
|
if (getAssignmentDeclarationKind(node as BinaryExpression) !== AssignmentDeclarationKind.None) {
|
|
addDeclaration(node as BinaryExpression);
|
|
}
|
|
// falls through
|
|
|
|
default:
|
|
forEachChild(node, visit);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class SourceMapSourceObject implements SourceMapSource {
|
|
lineMap!: number[];
|
|
constructor(public fileName: string, public text: string, public skipTrivia?: (pos: number) => number) { }
|
|
|
|
public getLineAndCharacterOfPosition(pos: number): LineAndCharacter {
|
|
return getLineAndCharacterOfPosition(this, pos);
|
|
}
|
|
}
|
|
|
|
function getServicesObjectAllocator(): ObjectAllocator {
|
|
return {
|
|
getNodeConstructor: () => NodeObject,
|
|
getTokenConstructor: () => TokenObject,
|
|
|
|
getIdentifierConstructor: () => IdentifierObject,
|
|
getPrivateIdentifierConstructor: () => PrivateIdentifierObject,
|
|
getSourceFileConstructor: () => SourceFileObject,
|
|
getSymbolConstructor: () => SymbolObject,
|
|
getTypeConstructor: () => TypeObject,
|
|
getSignatureConstructor: () => SignatureObject,
|
|
getSourceMapSourceConstructor: () => SourceMapSourceObject,
|
|
};
|
|
}
|
|
|
|
/// Language Service
|
|
|
|
// Information about a specific host file.
|
|
interface HostFileInformation {
|
|
hostFileName: string;
|
|
version: string;
|
|
scriptSnapshot: IScriptSnapshot;
|
|
scriptKind: ScriptKind;
|
|
}
|
|
|
|
/* @internal */
|
|
export interface DisplayPartsSymbolWriter extends EmitTextWriter {
|
|
displayParts(): SymbolDisplayPart[];
|
|
}
|
|
|
|
/* @internal */
|
|
export function toEditorSettings(options: FormatCodeOptions | FormatCodeSettings): FormatCodeSettings;
|
|
export function toEditorSettings(options: EditorOptions | EditorSettings): EditorSettings;
|
|
export function toEditorSettings(optionsAsMap: MapLike<any>): MapLike<any> {
|
|
let allPropertiesAreCamelCased = true;
|
|
for (const key in optionsAsMap) {
|
|
if (hasProperty(optionsAsMap, key) && !isCamelCase(key)) {
|
|
allPropertiesAreCamelCased = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allPropertiesAreCamelCased) {
|
|
return optionsAsMap;
|
|
}
|
|
const settings: MapLike<any> = {};
|
|
for (const key in optionsAsMap) {
|
|
if (hasProperty(optionsAsMap, key)) {
|
|
const newKey = isCamelCase(key) ? key : key.charAt(0).toLowerCase() + key.substr(1);
|
|
settings[newKey] = optionsAsMap[key];
|
|
}
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
function isCamelCase(s: string) {
|
|
return !s.length || s.charAt(0) === s.charAt(0).toLowerCase();
|
|
}
|
|
|
|
export function displayPartsToString(displayParts: SymbolDisplayPart[] | undefined) {
|
|
if (displayParts) {
|
|
return map(displayParts, displayPart => displayPart.text).join("");
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
export function getDefaultCompilerOptions(): CompilerOptions {
|
|
// Always default to "ScriptTarget.ES5" for the language service
|
|
return {
|
|
target: ScriptTarget.ES5,
|
|
jsx: JsxEmit.Preserve
|
|
};
|
|
}
|
|
|
|
export function getSupportedCodeFixes() {
|
|
return codefix.getSupportedErrorCodes();
|
|
}
|
|
|
|
// Either it will be file name if host doesnt have file or it will be the host's file information
|
|
type CachedHostFileInformation = HostFileInformation | string;
|
|
|
|
// Cache host information about script Should be refreshed
|
|
// at each language service public entry point, since we don't know when
|
|
// the set of scripts handled by the host changes.
|
|
class HostCache {
|
|
private fileNameToEntry: Map<CachedHostFileInformation>;
|
|
private _compilationSettings: CompilerOptions;
|
|
private currentDirectory: string;
|
|
|
|
constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) {
|
|
// script id => script index
|
|
this.currentDirectory = host.getCurrentDirectory();
|
|
this.fileNameToEntry = createMap<CachedHostFileInformation>();
|
|
|
|
// Initialize the list with the root file names
|
|
const rootFileNames = host.getScriptFileNames();
|
|
for (const fileName of rootFileNames) {
|
|
this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
|
|
}
|
|
|
|
// store the compilation settings
|
|
this._compilationSettings = host.getCompilationSettings() || getDefaultCompilerOptions();
|
|
}
|
|
|
|
public compilationSettings() {
|
|
return this._compilationSettings;
|
|
}
|
|
|
|
public getProjectReferences(): readonly ProjectReference[] | undefined {
|
|
return this.host.getProjectReferences && this.host.getProjectReferences();
|
|
}
|
|
|
|
private createEntry(fileName: string, path: Path) {
|
|
let entry: CachedHostFileInformation;
|
|
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
|
|
if (scriptSnapshot) {
|
|
entry = {
|
|
hostFileName: fileName,
|
|
version: this.host.getScriptVersion(fileName),
|
|
scriptSnapshot,
|
|
scriptKind: getScriptKind(fileName, this.host)
|
|
};
|
|
}
|
|
else {
|
|
entry = fileName;
|
|
}
|
|
|
|
this.fileNameToEntry.set(path, entry);
|
|
return entry;
|
|
}
|
|
|
|
public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
|
|
return this.fileNameToEntry.get(path);
|
|
}
|
|
|
|
public getHostFileInformation(path: Path): HostFileInformation | undefined {
|
|
const entry = this.fileNameToEntry.get(path);
|
|
return !isString(entry) ? entry : undefined;
|
|
}
|
|
|
|
public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
|
|
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
|
|
return isString(info) ? undefined! : info; // TODO: GH#18217
|
|
}
|
|
|
|
public getRootFileNames(): string[] {
|
|
const names: string[] = [];
|
|
this.fileNameToEntry.forEach(entry => {
|
|
if (isString(entry)) {
|
|
names.push(entry);
|
|
}
|
|
else {
|
|
names.push(entry.hostFileName);
|
|
}
|
|
});
|
|
return names;
|
|
}
|
|
|
|
public getVersion(path: Path): string {
|
|
const file = this.getHostFileInformation(path);
|
|
return (file && file.version)!; // TODO: GH#18217
|
|
}
|
|
|
|
public getScriptSnapshot(path: Path): IScriptSnapshot {
|
|
const file = this.getHostFileInformation(path);
|
|
return (file && file.scriptSnapshot)!; // TODO: GH#18217
|
|
}
|
|
}
|
|
|
|
class SyntaxTreeCache {
|
|
// For our syntactic only features, we also keep a cache of the syntax tree for the
|
|
// currently edited file.
|
|
private currentFileName: string | undefined;
|
|
private currentFileVersion: string | undefined;
|
|
private currentFileScriptSnapshot: IScriptSnapshot | undefined;
|
|
private currentSourceFile: SourceFile | undefined;
|
|
|
|
constructor(private host: LanguageServiceHost) {
|
|
}
|
|
|
|
public getCurrentSourceFile(fileName: string): SourceFile {
|
|
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
|
|
if (!scriptSnapshot) {
|
|
// The host does not know about this file.
|
|
throw new Error("Could not find file: '" + fileName + "'.");
|
|
}
|
|
|
|
const scriptKind = getScriptKind(fileName, this.host);
|
|
const version = this.host.getScriptVersion(fileName);
|
|
let sourceFile: SourceFile | undefined;
|
|
|
|
if (this.currentFileName !== fileName) {
|
|
// This is a new file, just parse it
|
|
sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind);
|
|
}
|
|
else if (this.currentFileVersion !== version) {
|
|
// This is the same file, just a newer version. Incrementally parse the file.
|
|
const editRange = scriptSnapshot.getChangeRange(this.currentFileScriptSnapshot!);
|
|
sourceFile = updateLanguageServiceSourceFile(this.currentSourceFile!, scriptSnapshot, version, editRange);
|
|
}
|
|
|
|
if (sourceFile) {
|
|
// All done, ensure state is up to date
|
|
this.currentFileVersion = version;
|
|
this.currentFileName = fileName;
|
|
this.currentFileScriptSnapshot = scriptSnapshot;
|
|
this.currentSourceFile = sourceFile;
|
|
}
|
|
|
|
return this.currentSourceFile!;
|
|
}
|
|
}
|
|
|
|
function setSourceFileFields(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string) {
|
|
sourceFile.version = version;
|
|
sourceFile.scriptSnapshot = scriptSnapshot;
|
|
}
|
|
|
|
export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile {
|
|
const sourceFile = createSourceFile(fileName, getSnapshotText(scriptSnapshot), scriptTarget, setNodeParents, scriptKind);
|
|
setSourceFileFields(sourceFile, scriptSnapshot, version);
|
|
return sourceFile;
|
|
}
|
|
|
|
export function updateLanguageServiceSourceFile(sourceFile: SourceFile, scriptSnapshot: IScriptSnapshot, version: string, textChangeRange: TextChangeRange | undefined, aggressiveChecks?: boolean): SourceFile {
|
|
// If we were given a text change range, and our version or open-ness changed, then
|
|
// incrementally parse this file.
|
|
if (textChangeRange) {
|
|
if (version !== sourceFile.version) {
|
|
let newText: string;
|
|
|
|
// grab the fragment from the beginning of the original text to the beginning of the span
|
|
const prefix = textChangeRange.span.start !== 0
|
|
? sourceFile.text.substr(0, textChangeRange.span.start)
|
|
: "";
|
|
|
|
// grab the fragment from the end of the span till the end of the original text
|
|
const suffix = textSpanEnd(textChangeRange.span) !== sourceFile.text.length
|
|
? sourceFile.text.substr(textSpanEnd(textChangeRange.span))
|
|
: "";
|
|
|
|
if (textChangeRange.newLength === 0) {
|
|
// edit was a deletion - just combine prefix and suffix
|
|
newText = prefix && suffix ? prefix + suffix : prefix || suffix;
|
|
}
|
|
else {
|
|
// it was actual edit, fetch the fragment of new text that correspond to new span
|
|
const changedText = scriptSnapshot.getText(textChangeRange.span.start, textChangeRange.span.start + textChangeRange.newLength);
|
|
// combine prefix, changed text and suffix
|
|
newText = prefix && suffix
|
|
? prefix + changedText + suffix
|
|
: prefix
|
|
? (prefix + changedText)
|
|
: (changedText + suffix);
|
|
}
|
|
|
|
const newSourceFile = updateSourceFile(sourceFile, newText, textChangeRange, aggressiveChecks);
|
|
setSourceFileFields(newSourceFile, scriptSnapshot, version);
|
|
// after incremental parsing nameTable might not be up-to-date
|
|
// drop it so it can be lazily recreated later
|
|
newSourceFile.nameTable = undefined;
|
|
|
|
// dispose all resources held by old script snapshot
|
|
if (sourceFile !== newSourceFile && sourceFile.scriptSnapshot) {
|
|
if (sourceFile.scriptSnapshot.dispose) {
|
|
sourceFile.scriptSnapshot.dispose();
|
|
}
|
|
|
|
sourceFile.scriptSnapshot = undefined;
|
|
}
|
|
|
|
return newSourceFile;
|
|
}
|
|
}
|
|
|
|
// Otherwise, just create a new source file.
|
|
return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true, sourceFile.scriptKind);
|
|
}
|
|
|
|
class CancellationTokenObject implements CancellationToken {
|
|
constructor(private cancellationToken: HostCancellationToken | undefined) {
|
|
}
|
|
|
|
public isCancellationRequested(): boolean {
|
|
return !!this.cancellationToken && this.cancellationToken.isCancellationRequested();
|
|
}
|
|
|
|
public throwIfCancellationRequested(): void {
|
|
if (this.isCancellationRequested()) {
|
|
throw new OperationCanceledException();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* @internal */
|
|
/** A cancellation that throttles calls to the host */
|
|
export class ThrottledCancellationToken implements CancellationToken {
|
|
// Store when we last tried to cancel. Checking cancellation can be expensive (as we have
|
|
// to marshall over to the host layer). So we only bother actually checking once enough
|
|
// time has passed.
|
|
private lastCancellationCheckTime = 0;
|
|
|
|
constructor(private hostCancellationToken: HostCancellationToken, private readonly throttleWaitMilliseconds = 20) {
|
|
}
|
|
|
|
public isCancellationRequested(): boolean {
|
|
const time = timestamp();
|
|
const duration = Math.abs(time - this.lastCancellationCheckTime);
|
|
if (duration >= this.throttleWaitMilliseconds) {
|
|
// Check no more than once every throttle wait milliseconds
|
|
this.lastCancellationCheckTime = time;
|
|
return this.hostCancellationToken.isCancellationRequested();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public throwIfCancellationRequested(): void {
|
|
if (this.isCancellationRequested()) {
|
|
throw new OperationCanceledException();
|
|
}
|
|
}
|
|
}
|
|
|
|
export function createLanguageService(
|
|
host: LanguageServiceHost,
|
|
documentRegistry: DocumentRegistry = createDocumentRegistry(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames(), host.getCurrentDirectory()),
|
|
syntaxOnly = false): LanguageService {
|
|
|
|
const syntaxTreeCache: SyntaxTreeCache = new SyntaxTreeCache(host);
|
|
let program: Program;
|
|
let lastProjectVersion: string;
|
|
let lastTypesRootVersion = 0;
|
|
|
|
const cancellationToken = new CancellationTokenObject(host.getCancellationToken && host.getCancellationToken());
|
|
|
|
const currentDirectory = host.getCurrentDirectory();
|
|
// Check if the localized messages json is set, otherwise query the host for it
|
|
if (!localizedDiagnosticMessages && host.getLocalizedDiagnosticMessages) {
|
|
setLocalizedDiagnosticMessages(host.getLocalizedDiagnosticMessages());
|
|
}
|
|
|
|
function log(message: string) {
|
|
if (host.log) {
|
|
host.log(message);
|
|
}
|
|
}
|
|
|
|
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
|
|
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
|
|
|
|
const sourceMapper = getSourceMapper({
|
|
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
|
|
getCurrentDirectory: () => currentDirectory,
|
|
getProgram,
|
|
fileExists: maybeBind(host, host.fileExists),
|
|
readFile: maybeBind(host, host.readFile),
|
|
getDocumentPositionMapper: maybeBind(host, host.getDocumentPositionMapper),
|
|
getSourceFileLike: maybeBind(host, host.getSourceFileLike),
|
|
log
|
|
});
|
|
|
|
function getValidSourceFile(fileName: string): SourceFile {
|
|
const sourceFile = program.getSourceFile(fileName);
|
|
if (!sourceFile) {
|
|
const error: Error & PossibleProgramFileInfo = new Error(`Could not find source file: '${fileName}'.`);
|
|
|
|
// We've been having trouble debugging this, so attach sidecar data for the tsserver log.
|
|
// See https://github.com/microsoft/TypeScript/issues/30180.
|
|
error.ProgramFiles = program.getSourceFiles().map(f => f.fileName);
|
|
|
|
throw error;
|
|
}
|
|
return sourceFile;
|
|
}
|
|
|
|
function synchronizeHostData(): void {
|
|
Debug.assert(!syntaxOnly);
|
|
|
|
// perform fast check if host supports it
|
|
if (host.getProjectVersion) {
|
|
const hostProjectVersion = host.getProjectVersion();
|
|
if (hostProjectVersion) {
|
|
if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames) {
|
|
return;
|
|
}
|
|
|
|
lastProjectVersion = hostProjectVersion;
|
|
}
|
|
}
|
|
|
|
const typeRootsVersion = host.getTypeRootsVersion ? host.getTypeRootsVersion() : 0;
|
|
if (lastTypesRootVersion !== typeRootsVersion) {
|
|
log("TypeRoots version has changed; provide new program");
|
|
program = undefined!; // TODO: GH#18217
|
|
lastTypesRootVersion = typeRootsVersion;
|
|
}
|
|
|
|
// Get a fresh cache of the host information
|
|
let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName);
|
|
const rootFileNames = hostCache.getRootFileNames();
|
|
|
|
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
|
|
const projectReferences = hostCache.getProjectReferences();
|
|
|
|
// If the program is already up-to-date, we can reuse it
|
|
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache!.getVersion(path), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) {
|
|
return;
|
|
}
|
|
|
|
// IMPORTANT - It is critical from this moment onward that we do not check
|
|
// cancellation tokens. We are about to mutate source files from a previous program
|
|
// instance. If we cancel midway through, we may end up in an inconsistent state where
|
|
// the program points to old source files that have been invalidated because of
|
|
// incremental parsing.
|
|
|
|
const newSettings = hostCache.compilationSettings();
|
|
|
|
// Now create a new compiler
|
|
const compilerHost: CompilerHost = {
|
|
getSourceFile: getOrCreateSourceFile,
|
|
getSourceFileByPath: getOrCreateSourceFileByPath,
|
|
getCancellationToken: () => cancellationToken,
|
|
getCanonicalFileName,
|
|
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
|
|
getNewLine: () => getNewLineCharacter(newSettings, () => getNewLineOrDefaultFromHost(host)),
|
|
getDefaultLibFileName: (options) => host.getDefaultLibFileName(options),
|
|
writeFile: noop,
|
|
getCurrentDirectory: () => currentDirectory,
|
|
fileExists,
|
|
readFile,
|
|
realpath: host.realpath && (path => host.realpath!(path)),
|
|
directoryExists: directoryName => {
|
|
return directoryProbablyExists(directoryName, host);
|
|
},
|
|
getDirectories: path => {
|
|
return host.getDirectories ? host.getDirectories(path) : [];
|
|
},
|
|
readDirectory(path, extensions, exclude, include, depth) {
|
|
Debug.assertDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
|
|
return host.readDirectory!(path, extensions, exclude, include, depth);
|
|
},
|
|
onReleaseOldSourceFile,
|
|
hasInvalidatedResolution,
|
|
hasChangedAutomaticTypeDirectiveNames: host.hasChangedAutomaticTypeDirectiveNames
|
|
};
|
|
if (host.trace) {
|
|
compilerHost.trace = message => host.trace!(message);
|
|
}
|
|
|
|
if (host.resolveModuleNames) {
|
|
compilerHost.resolveModuleNames = (...args) => host.resolveModuleNames!(...args);
|
|
}
|
|
if (host.resolveTypeReferenceDirectives) {
|
|
compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args);
|
|
}
|
|
if (host.setResolvedProjectReferenceCallbacks) {
|
|
compilerHost.setResolvedProjectReferenceCallbacks = callbacks => host.setResolvedProjectReferenceCallbacks!(callbacks);
|
|
}
|
|
if (host.useSourceOfProjectReferenceRedirect) {
|
|
compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!();
|
|
}
|
|
|
|
const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
|
|
const options: CreateProgramOptions = {
|
|
rootNames: rootFileNames,
|
|
options: newSettings,
|
|
host: compilerHost,
|
|
oldProgram: program,
|
|
projectReferences
|
|
};
|
|
program = createProgram(options);
|
|
|
|
// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
|
|
// It needs to be cleared to allow all collected snapshots to be released
|
|
hostCache = undefined;
|
|
|
|
// We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above,
|
|
// Because it only functions until `hostCache` is cleared, while we'll potentially need the functionality to lazily read sourcemap files during
|
|
// the course of whatever called `synchronizeHostData`
|
|
sourceMapper.clearCache();
|
|
|
|
// Make sure all the nodes in the program are both bound, and have their parent
|
|
// pointers set property.
|
|
program.getTypeChecker();
|
|
return;
|
|
|
|
function fileExists(fileName: string): boolean {
|
|
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
|
const entry = hostCache && hostCache.getEntryByPath(path);
|
|
return entry ?
|
|
!isString(entry) :
|
|
(!!host.fileExists && host.fileExists(fileName));
|
|
}
|
|
|
|
function readFile(fileName: string) {
|
|
// stub missing host functionality
|
|
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
|
|
const entry = hostCache && hostCache.getEntryByPath(path);
|
|
if (entry) {
|
|
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
|
|
}
|
|
return host.readFile && host.readFile(fileName);
|
|
}
|
|
|
|
// Release any files we have acquired in the old program but are
|
|
// not part of the new program.
|
|
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
|
|
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions);
|
|
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey);
|
|
}
|
|
|
|
function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
|
|
return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
|
|
}
|
|
|
|
function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
|
|
Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
|
|
// The program is asking for this file, check first if the host can locate it.
|
|
// If the host can not locate the file, then it does not exist. return undefined
|
|
// to the program to allow reporting of errors for missing files.
|
|
const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path);
|
|
if (!hostFileInformation) {
|
|
return undefined;
|
|
}
|
|
|
|
// Check if the language version has changed since we last created a program; if they are the same,
|
|
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
|
|
// can not be reused. we have to dump all syntax trees and create new ones.
|
|
if (!shouldCreateNewSourceFile) {
|
|
// Check if the old program had this file already
|
|
const oldSourceFile = program && program.getSourceFileByPath(path);
|
|
if (oldSourceFile) {
|
|
// We already had a source file for this file name. Go to the registry to
|
|
// ensure that we get the right up to date version of it. We need this to
|
|
// address the following race-condition. Specifically, say we have the following:
|
|
//
|
|
// LS1
|
|
// \
|
|
// DocumentRegistry
|
|
// /
|
|
// LS2
|
|
//
|
|
// Each LS has a reference to file 'foo.ts' at version 1. LS2 then updates
|
|
// it's version of 'foo.ts' to version 2. This will cause LS2 and the
|
|
// DocumentRegistry to have version 2 of the document. HOwever, LS1 will
|
|
// have version 1. And *importantly* this source file will be *corrupt*.
|
|
// The act of creating version 2 of the file irrevocably damages the version
|
|
// 1 file.
|
|
//
|
|
// So, later when we call into LS1, we need to make sure that it doesn't use
|
|
// it's source file any more, and instead defers to DocumentRegistry to get
|
|
// either version 1, version 2 (or some other version) depending on what the
|
|
// host says should be used.
|
|
|
|
// We do not support the scenario where a host can modify a registered
|
|
// file's script kind, i.e. in one project some file is treated as ".ts"
|
|
// and in another as ".js"
|
|
Debug.assertEqual(hostFileInformation.scriptKind, oldSourceFile.scriptKind, "Registered script kind should match new script kind.", path);
|
|
|
|
return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
|
|
}
|
|
|
|
// We didn't already have the file. Fall through and acquire it from the registry.
|
|
}
|
|
|
|
// Could not find this file in the old program, create a new SourceFile for it.
|
|
return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
|
|
}
|
|
}
|
|
|
|
// TODO: GH#18217 frequently asserted as defined
|
|
function getProgram(): Program | undefined {
|
|
if (syntaxOnly) {
|
|
Debug.assert(program === undefined);
|
|
return undefined;
|
|
}
|
|
|
|
synchronizeHostData();
|
|
|
|
return program;
|
|
}
|
|
|
|
function cleanupSemanticCache(): void {
|
|
program = undefined!; // TODO: GH#18217
|
|
}
|
|
|
|
function dispose(): void {
|
|
if (program) {
|
|
forEach(program.getSourceFiles(), f =>
|
|
documentRegistry.releaseDocument(f.fileName, program.getCompilerOptions()));
|
|
program = undefined!; // TODO: GH#18217
|
|
}
|
|
host = undefined!;
|
|
}
|
|
|
|
/// Diagnostics
|
|
function getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[] {
|
|
synchronizeHostData();
|
|
|
|
return program.getSyntacticDiagnostics(getValidSourceFile(fileName), cancellationToken).slice();
|
|
}
|
|
|
|
/**
|
|
* getSemanticDiagnostics return array of Diagnostics. If '-d' is not enabled, only report semantic errors
|
|
* If '-d' enabled, report both semantic and emitter errors
|
|
*/
|
|
function getSemanticDiagnostics(fileName: string): Diagnostic[] {
|
|
synchronizeHostData();
|
|
|
|
const targetSourceFile = getValidSourceFile(fileName);
|
|
|
|
// Only perform the action per file regardless of '-out' flag as LanguageServiceHost is expected to call this function per file.
|
|
// Therefore only get diagnostics for given file.
|
|
|
|
const semanticDiagnostics = program.getSemanticDiagnostics(targetSourceFile, cancellationToken);
|
|
if (!getEmitDeclarations(program.getCompilerOptions())) {
|
|
return semanticDiagnostics.slice();
|
|
}
|
|
|
|
// If '-d' is enabled, check for emitter error. One example of emitter error is export class implements non-export interface
|
|
const declarationDiagnostics = program.getDeclarationDiagnostics(targetSourceFile, cancellationToken);
|
|
return [...semanticDiagnostics, ...declarationDiagnostics];
|
|
}
|
|
|
|
function getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[] {
|
|
synchronizeHostData();
|
|
return computeSuggestionDiagnostics(getValidSourceFile(fileName), program, cancellationToken);
|
|
}
|
|
|
|
function getCompilerOptionsDiagnostics() {
|
|
synchronizeHostData();
|
|
return [...program.getOptionsDiagnostics(cancellationToken), ...program.getGlobalDiagnostics(cancellationToken)];
|
|
}
|
|
|
|
function getCompletionsAtPosition(fileName: string, position: number, options: GetCompletionsAtPositionOptions = emptyOptions): CompletionInfo | undefined {
|
|
// Convert from deprecated options names to new names
|
|
const fullPreferences: UserPreferences = {
|
|
...identity<UserPreferences>(options), // avoid excess property check
|
|
includeCompletionsForModuleExports: options.includeCompletionsForModuleExports || options.includeExternalModuleExports,
|
|
includeCompletionsWithInsertText: options.includeCompletionsWithInsertText || options.includeInsertTextCompletions,
|
|
};
|
|
synchronizeHostData();
|
|
return Completions.getCompletionsAtPosition(
|
|
host,
|
|
program,
|
|
log,
|
|
getValidSourceFile(fileName),
|
|
position,
|
|
fullPreferences,
|
|
options.triggerCharacter);
|
|
}
|
|
|
|
function getCompletionEntryDetails(fileName: string, position: number, name: string, formattingOptions: FormatCodeSettings | undefined, source: string | undefined, preferences: UserPreferences = emptyOptions): CompletionEntryDetails | undefined {
|
|
synchronizeHostData();
|
|
return Completions.getCompletionEntryDetails(
|
|
program,
|
|
log,
|
|
getValidSourceFile(fileName),
|
|
position,
|
|
{ name, source },
|
|
host,
|
|
(formattingOptions && formatting.getFormatContext(formattingOptions))!, // TODO: GH#18217
|
|
preferences,
|
|
cancellationToken,
|
|
);
|
|
}
|
|
|
|
function getCompletionEntrySymbol(fileName: string, position: number, name: string, source?: string): Symbol | undefined {
|
|
synchronizeHostData();
|
|
return Completions.getCompletionEntrySymbol(program, log, getValidSourceFile(fileName), position, { name, source }, host);
|
|
}
|
|
|
|
function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo | undefined {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const node = getTouchingPropertyName(sourceFile, position);
|
|
if (node === sourceFile) {
|
|
// Avoid giving quickInfo for the sourceFile as a whole.
|
|
return undefined;
|
|
}
|
|
|
|
const typeChecker = program.getTypeChecker();
|
|
const nodeForQuickInfo = getNodeForQuickInfo(node);
|
|
const symbol = getSymbolAtLocationForQuickInfo(nodeForQuickInfo, typeChecker);
|
|
|
|
if (!symbol || typeChecker.isUnknownSymbol(symbol)) {
|
|
const type = shouldGetType(sourceFile, nodeForQuickInfo, position) ? typeChecker.getTypeAtLocation(nodeForQuickInfo) : undefined;
|
|
return type && {
|
|
kind: ScriptElementKind.unknown,
|
|
kindModifiers: ScriptElementKindModifier.none,
|
|
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
|
|
displayParts: typeChecker.runWithCancellationToken(cancellationToken, typeChecker => typeToDisplayParts(typeChecker, type, getContainerNode(nodeForQuickInfo))),
|
|
documentation: type.symbol ? type.symbol.getDocumentationComment(typeChecker) : undefined,
|
|
tags: type.symbol ? type.symbol.getJsDocTags() : undefined
|
|
};
|
|
}
|
|
|
|
const { symbolKind, displayParts, documentation, tags } = typeChecker.runWithCancellationToken(cancellationToken, typeChecker =>
|
|
SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, getContainerNode(nodeForQuickInfo), nodeForQuickInfo)
|
|
);
|
|
return {
|
|
kind: symbolKind,
|
|
kindModifiers: SymbolDisplay.getSymbolModifiers(symbol),
|
|
textSpan: createTextSpanFromNode(nodeForQuickInfo, sourceFile),
|
|
displayParts,
|
|
documentation,
|
|
tags,
|
|
};
|
|
}
|
|
|
|
function getNodeForQuickInfo(node: Node): Node {
|
|
if (isNewExpression(node.parent) && node.pos === node.parent.pos) {
|
|
return node.parent.expression;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
function shouldGetType(sourceFile: SourceFile, node: Node, position: number): boolean {
|
|
switch (node.kind) {
|
|
case SyntaxKind.Identifier:
|
|
return !isLabelName(node) && !isTagName(node);
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
case SyntaxKind.QualifiedName:
|
|
// Don't return quickInfo if inside the comment in `a/**/.b`
|
|
return !isInComment(sourceFile, position);
|
|
case SyntaxKind.ThisKeyword:
|
|
case SyntaxKind.ThisType:
|
|
case SyntaxKind.SuperKeyword:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Goto definition
|
|
function getDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getDefinitionAtPosition(program, getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getDefinitionAndBoundSpan(fileName: string, position: number): DefinitionInfoAndBoundSpan | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getDefinitionAndBoundSpan(program, getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getTypeDefinitionAtPosition(fileName: string, position: number): readonly DefinitionInfo[] | undefined {
|
|
synchronizeHostData();
|
|
return GoToDefinition.getTypeDefinitionAtPosition(program.getTypeChecker(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
/// Goto implementation
|
|
|
|
function getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] | undefined {
|
|
synchronizeHostData();
|
|
return FindAllReferences.getImplementationsAtPosition(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
/// References and Occurrences
|
|
function getOccurrencesAtPosition(fileName: string, position: number): readonly ReferenceEntry[] | undefined {
|
|
return flatMap(
|
|
getDocumentHighlights(fileName, position, [fileName]),
|
|
entry => entry.highlightSpans.map<ReferenceEntry>(highlightSpan => ({
|
|
fileName: entry.fileName,
|
|
textSpan: highlightSpan.textSpan,
|
|
isWriteAccess: highlightSpan.kind === HighlightSpanKind.writtenReference,
|
|
isDefinition: false,
|
|
...highlightSpan.isInString && { isInString: true },
|
|
...highlightSpan.contextSpan && { contextSpan: highlightSpan.contextSpan }
|
|
}))
|
|
);
|
|
}
|
|
|
|
function getDocumentHighlights(fileName: string, position: number, filesToSearch: readonly string[]): DocumentHighlights[] | undefined {
|
|
const normalizedFileName = normalizePath(fileName);
|
|
Debug.assert(filesToSearch.some(f => normalizePath(f) === normalizedFileName));
|
|
synchronizeHostData();
|
|
const sourceFilesToSearch = mapDefined(filesToSearch, fileName => program.getSourceFile(fileName));
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
return DocumentHighlights.getDocumentHighlights(program, cancellationToken, sourceFile, position, sourceFilesToSearch);
|
|
}
|
|
|
|
function findRenameLocations(fileName: string, position: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename?: boolean): RenameLocation[] | undefined {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const node = getTouchingPropertyName(sourceFile, position);
|
|
if (isIdentifier(node) && (isJsxOpeningElement(node.parent) || isJsxClosingElement(node.parent)) && isIntrinsicJsxName(node.escapedText)) {
|
|
const { openingElement, closingElement } = node.parent.parent;
|
|
return [openingElement, closingElement].map((node): RenameLocation => {
|
|
const textSpan = createTextSpanFromNode(node.tagName, sourceFile);
|
|
return {
|
|
fileName: sourceFile.fileName,
|
|
textSpan,
|
|
...FindAllReferences.toContextSpan(textSpan, sourceFile, node.parent)
|
|
};
|
|
});
|
|
}
|
|
else {
|
|
return getReferencesWorker(node, position, { findInStrings, findInComments, providePrefixAndSuffixTextForRename, isForRename: true },
|
|
(entry, originalNode, checker) => FindAllReferences.toRenameLocation(entry, originalNode, checker, providePrefixAndSuffixTextForRename || false));
|
|
}
|
|
}
|
|
|
|
function getReferencesAtPosition(fileName: string, position: number): ReferenceEntry[] | undefined {
|
|
synchronizeHostData();
|
|
return getReferencesWorker(getTouchingPropertyName(getValidSourceFile(fileName), position), position, {}, FindAllReferences.toReferenceEntry);
|
|
}
|
|
|
|
function getReferencesWorker<T>(node: Node, position: number, options: FindAllReferences.Options, cb: FindAllReferences.ToReferenceOrRenameEntry<T>): T[] | undefined {
|
|
synchronizeHostData();
|
|
|
|
// Exclude default library when renaming as commonly user don't want to change that file.
|
|
const sourceFiles = options && options.isForRename
|
|
? program.getSourceFiles().filter(sourceFile => !program.isSourceFileDefaultLibrary(sourceFile))
|
|
: program.getSourceFiles();
|
|
|
|
return FindAllReferences.findReferenceOrRenameEntries(program, cancellationToken, sourceFiles, node, position, options, cb);
|
|
}
|
|
|
|
function findReferences(fileName: string, position: number): ReferencedSymbol[] | undefined {
|
|
synchronizeHostData();
|
|
return FindAllReferences.findReferencedSymbols(program, cancellationToken, program.getSourceFiles(), getValidSourceFile(fileName), position);
|
|
}
|
|
|
|
function getNavigateToItems(searchValue: string, maxResultCount?: number, fileName?: string, excludeDtsFiles = false): NavigateToItem[] {
|
|
synchronizeHostData();
|
|
const sourceFiles = fileName ? [getValidSourceFile(fileName)] : program.getSourceFiles();
|
|
return NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles);
|
|
}
|
|
|
|
function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, forceDtsEmit?: boolean) {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const customTransformers = host.getCustomTransformers && host.getCustomTransformers();
|
|
return getFileEmitOutput(program, sourceFile, !!emitOnlyDtsFiles, cancellationToken, customTransformers, forceDtsEmit);
|
|
}
|
|
|
|
// Signature help
|
|
/**
|
|
* This is a semantic operation.
|
|
*/
|
|
function getSignatureHelpItems(fileName: string, position: number, { triggerReason }: SignatureHelpItemsOptions = emptyOptions): SignatureHelpItems | undefined {
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
|
|
return SignatureHelp.getSignatureHelpItems(program, sourceFile, position, triggerReason, cancellationToken);
|
|
}
|
|
|
|
/// Syntactic features
|
|
function getNonBoundSourceFile(fileName: string): SourceFile {
|
|
return syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
}
|
|
|
|
function getNameOrDottedNameSpan(fileName: string, startPos: number, _endPos: number): TextSpan | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
// Get node at the location
|
|
const node = getTouchingPropertyName(sourceFile, startPos);
|
|
|
|
if (node === sourceFile) {
|
|
return undefined;
|
|
}
|
|
|
|
switch (node.kind) {
|
|
case SyntaxKind.PropertyAccessExpression:
|
|
case SyntaxKind.QualifiedName:
|
|
case SyntaxKind.StringLiteral:
|
|
case SyntaxKind.FalseKeyword:
|
|
case SyntaxKind.TrueKeyword:
|
|
case SyntaxKind.NullKeyword:
|
|
case SyntaxKind.SuperKeyword:
|
|
case SyntaxKind.ThisKeyword:
|
|
case SyntaxKind.ThisType:
|
|
case SyntaxKind.Identifier:
|
|
break;
|
|
|
|
// Cant create the text span
|
|
default:
|
|
return undefined;
|
|
}
|
|
|
|
let nodeForStartPos = node;
|
|
while (true) {
|
|
if (isRightSideOfPropertyAccess(nodeForStartPos) || isRightSideOfQualifiedName(nodeForStartPos)) {
|
|
// If on the span is in right side of the the property or qualified name, return the span from the qualified name pos to end of this node
|
|
nodeForStartPos = nodeForStartPos.parent;
|
|
}
|
|
else if (isNameOfModuleDeclaration(nodeForStartPos)) {
|
|
// If this is name of a module declarations, check if this is right side of dotted module name
|
|
// If parent of the module declaration which is parent of this node is module declaration and its body is the module declaration that this node is name of
|
|
// Then this name is name from dotted module
|
|
if (nodeForStartPos.parent.parent.kind === SyntaxKind.ModuleDeclaration &&
|
|
(<ModuleDeclaration>nodeForStartPos.parent.parent).body === nodeForStartPos.parent) {
|
|
// Use parent module declarations name for start pos
|
|
nodeForStartPos = (<ModuleDeclaration>nodeForStartPos.parent.parent).name;
|
|
}
|
|
else {
|
|
// We have to use this name for start pos
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
// Is not a member expression so we have found the node for start pos
|
|
break;
|
|
}
|
|
}
|
|
|
|
return createTextSpanFromBounds(nodeForStartPos.getStart(), node.getEnd());
|
|
}
|
|
|
|
function getBreakpointStatementAtPosition(fileName: string, position: number): TextSpan | undefined {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
return BreakpointResolver.spanInSourceFileAtLocation(sourceFile, position);
|
|
}
|
|
|
|
function getNavigationBarItems(fileName: string): NavigationBarItem[] {
|
|
return NavigationBar.getNavigationBarItems(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken);
|
|
}
|
|
|
|
function getNavigationTree(fileName: string): NavigationTree {
|
|
return NavigationBar.getNavigationTree(syntaxTreeCache.getCurrentSourceFile(fileName), cancellationToken);
|
|
}
|
|
|
|
function isTsOrTsxFile(fileName: string): boolean {
|
|
const kind = getScriptKind(fileName, host);
|
|
return kind === ScriptKind.TS || kind === ScriptKind.TSX;
|
|
}
|
|
|
|
function getSemanticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] {
|
|
if (!isTsOrTsxFile(fileName)) {
|
|
// do not run semantic classification on non-ts-or-tsx files
|
|
return [];
|
|
}
|
|
synchronizeHostData();
|
|
return ts.getSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span);
|
|
}
|
|
|
|
function getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications {
|
|
if (!isTsOrTsxFile(fileName)) {
|
|
// do not run semantic classification on non-ts-or-tsx files
|
|
return { spans: [], endOfLineState: EndOfLineState.None };
|
|
}
|
|
synchronizeHostData();
|
|
return ts.getEncodedSemanticClassifications(program.getTypeChecker(), cancellationToken, getValidSourceFile(fileName), program.getClassifiableNames(), span);
|
|
}
|
|
|
|
function getSyntacticClassifications(fileName: string, span: TextSpan): ClassifiedSpan[] {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
return ts.getSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span);
|
|
}
|
|
|
|
function getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
return ts.getEncodedSyntacticClassifications(cancellationToken, syntaxTreeCache.getCurrentSourceFile(fileName), span);
|
|
}
|
|
|
|
function getOutliningSpans(fileName: string): OutliningSpan[] {
|
|
// doesn't use compiler - no need to synchronize with host
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
return OutliningElementsCollector.collectElements(sourceFile, cancellationToken);
|
|
}
|
|
|
|
const braceMatching = createMapFromTemplate({
|
|
[SyntaxKind.OpenBraceToken]: SyntaxKind.CloseBraceToken,
|
|
[SyntaxKind.OpenParenToken]: SyntaxKind.CloseParenToken,
|
|
[SyntaxKind.OpenBracketToken]: SyntaxKind.CloseBracketToken,
|
|
[SyntaxKind.GreaterThanToken]: SyntaxKind.LessThanToken,
|
|
});
|
|
braceMatching.forEach((value, key) => braceMatching.set(value.toString(), Number(key) as SyntaxKind));
|
|
|
|
function getBraceMatchingAtPosition(fileName: string, position: number): TextSpan[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const token = getTouchingToken(sourceFile, position);
|
|
const matchKind = token.getStart(sourceFile) === position ? braceMatching.get(token.kind.toString()) : undefined;
|
|
const match = matchKind && findChildOfKind(token.parent, matchKind, sourceFile);
|
|
// We want to order the braces when we return the result.
|
|
return match ? [createTextSpanFromNode(token, sourceFile), createTextSpanFromNode(match, sourceFile)].sort((a, b) => a.start - b.start) : emptyArray;
|
|
}
|
|
|
|
function getIndentationAtPosition(fileName: string, position: number, editorOptions: EditorOptions | EditorSettings) {
|
|
let start = timestamp();
|
|
const settings = toEditorSettings(editorOptions);
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
log("getIndentationAtPosition: getCurrentSourceFile: " + (timestamp() - start));
|
|
|
|
start = timestamp();
|
|
|
|
const result = formatting.SmartIndenter.getIndentation(position, sourceFile, settings);
|
|
log("getIndentationAtPosition: computeIndentation : " + (timestamp() - start));
|
|
|
|
return result;
|
|
}
|
|
|
|
function getFormattingEditsForRange(fileName: string, start: number, end: number, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
return formatting.formatSelection(start, end, sourceFile, formatting.getFormatContext(toEditorSettings(options)));
|
|
}
|
|
|
|
function getFormattingEditsForDocument(fileName: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
return formatting.formatDocument(syntaxTreeCache.getCurrentSourceFile(fileName), formatting.getFormatContext(toEditorSettings(options)));
|
|
}
|
|
|
|
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const formatContext = formatting.getFormatContext(toEditorSettings(options));
|
|
|
|
if (!isInComment(sourceFile, position)) {
|
|
switch (key) {
|
|
case "{":
|
|
return formatting.formatOnOpeningCurly(position, sourceFile, formatContext);
|
|
case "}":
|
|
return formatting.formatOnClosingCurly(position, sourceFile, formatContext);
|
|
case ";":
|
|
return formatting.formatOnSemicolon(position, sourceFile, formatContext);
|
|
case "\n":
|
|
return formatting.formatOnEnter(position, sourceFile, formatContext);
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly CodeFixAction[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const span = createTextSpanFromBounds(start, end);
|
|
const formatContext = formatting.getFormatContext(formatOptions);
|
|
|
|
return flatMap(deduplicate<number>(errorCodes, equateValues, compareValues), errorCode => {
|
|
cancellationToken.throwIfCancellationRequested();
|
|
return codefix.getFixes({ errorCode, sourceFile, span, program, host, cancellationToken, formatContext, preferences });
|
|
});
|
|
}
|
|
|
|
function getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): CombinedCodeActions {
|
|
synchronizeHostData();
|
|
Debug.assert(scope.type === "file");
|
|
const sourceFile = getValidSourceFile(scope.fileName);
|
|
const formatContext = formatting.getFormatContext(formatOptions);
|
|
|
|
return codefix.getAllFixes({ fixId, sourceFile, program, host, cancellationToken, formatContext, preferences });
|
|
}
|
|
|
|
function organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] {
|
|
synchronizeHostData();
|
|
Debug.assert(scope.type === "file");
|
|
const sourceFile = getValidSourceFile(scope.fileName);
|
|
const formatContext = formatting.getFormatContext(formatOptions);
|
|
|
|
return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences);
|
|
}
|
|
|
|
function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = emptyOptions): readonly FileTextChanges[] {
|
|
return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences, sourceMapper);
|
|
}
|
|
|
|
function applyCodeActionCommand(action: CodeActionCommand, formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult>;
|
|
function applyCodeActionCommand(action: CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(action: CodeActionCommand | CodeActionCommand[], formatSettings?: FormatCodeSettings): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;
|
|
function applyCodeActionCommand(fileName: Path, action: CodeActionCommand[]): Promise<ApplyCodeActionCommandResult[]>;
|
|
function applyCodeActionCommand(fileName: Path | CodeActionCommand | CodeActionCommand[], actionOrFormatSettingsOrUndefined?: CodeActionCommand | CodeActionCommand[] | FormatCodeSettings): Promise<ApplyCodeActionCommandResult | ApplyCodeActionCommandResult[]> {
|
|
const action = typeof fileName === "string" ? actionOrFormatSettingsOrUndefined as CodeActionCommand | CodeActionCommand[] : fileName as CodeActionCommand[];
|
|
return isArray(action) ? Promise.all(action.map(a => applySingleCodeActionCommand(a))) : applySingleCodeActionCommand(action);
|
|
}
|
|
|
|
function applySingleCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult> {
|
|
const getPath = (path: string): Path => toPath(path, currentDirectory, getCanonicalFileName);
|
|
Debug.assertEqual(action.type, "install package");
|
|
return host.installPackage
|
|
? host.installPackage({ fileName: getPath(action.file), packageName: action.packageName })
|
|
: Promise.reject("Host does not implement `installPackage`");
|
|
}
|
|
|
|
function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion | undefined {
|
|
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:
|
|
// var x = new foo<| ( with class foo<T>{} )
|
|
// or
|
|
// var y = 3 <|
|
|
if (openingBrace === CharacterCodes.lessThan) {
|
|
return false;
|
|
}
|
|
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
|
|
// Check if in a context where we don't want to perform any insertion
|
|
if (isInString(sourceFile, position)) {
|
|
return false;
|
|
}
|
|
|
|
if (isInsideJsxElementOrAttribute(sourceFile, position)) {
|
|
return openingBrace === CharacterCodes.openBrace;
|
|
}
|
|
|
|
if (isInTemplateString(sourceFile, position)) {
|
|
return false;
|
|
}
|
|
|
|
switch (openingBrace) {
|
|
case CharacterCodes.singleQuote:
|
|
case CharacterCodes.doubleQuote:
|
|
case CharacterCodes.backtick:
|
|
return !isInComment(sourceFile, position);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getJsxClosingTagAtPosition(fileName: string, position: number): JsxClosingTagInfo | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const token = findPrecedingToken(position, sourceFile);
|
|
if (!token) return undefined;
|
|
const element = token.kind === SyntaxKind.GreaterThanToken && isJsxOpeningElement(token.parent) ? token.parent.parent
|
|
: isJsxText(token) ? token.parent : undefined;
|
|
if (element && isUnclosedTag(element)) {
|
|
return { newText: `</${element.openingElement.tagName.getText(sourceFile)}>` };
|
|
}
|
|
}
|
|
|
|
function isUnclosedTag({ openingElement, closingElement, parent }: JsxElement): boolean {
|
|
return !tagNamesAreEquivalent(openingElement.tagName, closingElement.tagName) ||
|
|
isJsxElement(parent) && tagNamesAreEquivalent(openingElement.tagName, parent.openingElement.tagName) && isUnclosedTag(parent);
|
|
}
|
|
|
|
function getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan | undefined {
|
|
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
|
const range = formatting.getRangeOfEnclosingComment(sourceFile, position);
|
|
return range && (!onlyMultiLine || range.kind === SyntaxKind.MultiLineCommentTrivia) ? createTextSpanFromRange(range) : undefined;
|
|
}
|
|
|
|
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
|
|
// this on every single file. If we treat this syntactically, then that will cause
|
|
// us to populate and throw away the tree in our syntax tree cache for each file. By
|
|
// treating this as a semantic operation, we can access any tree without throwing
|
|
// anything away.
|
|
synchronizeHostData();
|
|
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
|
|
cancellationToken.throwIfCancellationRequested();
|
|
|
|
const fileContents = sourceFile.text;
|
|
const result: TodoComment[] = [];
|
|
|
|
// Exclude node_modules files as we don't want to show the todos of external libraries.
|
|
if (descriptors.length > 0 && !isNodeModulesFile(sourceFile.fileName)) {
|
|
const regExp = getTodoCommentsRegExp();
|
|
|
|
let matchArray: RegExpExecArray | null;
|
|
while (matchArray = regExp.exec(fileContents)) {
|
|
cancellationToken.throwIfCancellationRequested();
|
|
|
|
// If we got a match, here is what the match array will look like. Say the source text is:
|
|
//
|
|
// " // hack 1"
|
|
//
|
|
// The result array with the regexp: will be:
|
|
//
|
|
// ["// hack 1", "// ", "hack 1", undefined, "hack"]
|
|
//
|
|
// Here are the relevant capture groups:
|
|
// 0) The full match for the entire regexp.
|
|
// 1) The preamble to the message portion.
|
|
// 2) The message portion.
|
|
// 3...N) The descriptor that was matched - by index. 'undefined' for each
|
|
// descriptor that didn't match. an actual value if it did match.
|
|
//
|
|
// i.e. 'undefined' in position 3 above means TODO(jason) didn't match.
|
|
// "hack" in position 4 means HACK did match.
|
|
const firstDescriptorCaptureIndex = 3;
|
|
Debug.assert(matchArray.length === descriptors.length + firstDescriptorCaptureIndex);
|
|
|
|
const preamble = matchArray[1];
|
|
const matchPosition = matchArray.index + preamble.length;
|
|
|
|
// OK, we have found a match in the file. This is only an acceptable match if
|
|
// it is contained within a comment.
|
|
if (!isInComment(sourceFile, matchPosition)) {
|
|
continue;
|
|
}
|
|
|
|
let descriptor: TodoCommentDescriptor | undefined;
|
|
for (let i = 0; i < descriptors.length; i++) {
|
|
if (matchArray[i + firstDescriptorCaptureIndex]) {
|
|
descriptor = descriptors[i];
|
|
}
|
|
}
|
|
if (descriptor === undefined) return Debug.fail();
|
|
|
|
// We don't want to match something like 'TODOBY', so we make sure a non
|
|
// letter/digit follows the match.
|
|
if (isLetterOrDigit(fileContents.charCodeAt(matchPosition + descriptor.text.length))) {
|
|
continue;
|
|
}
|
|
|
|
const message = matchArray[2];
|
|
result.push({ descriptor, message, position: matchPosition });
|
|
}
|
|
}
|
|
|
|
return result;
|
|
|
|
function escapeRegExp(str: string): string {
|
|
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
|
}
|
|
|
|
function getTodoCommentsRegExp(): RegExp {
|
|
// NOTE: `?:` means 'non-capture group'. It allows us to have groups without having to
|
|
// filter them out later in the final result array.
|
|
|
|
// TODO comments can appear in one of the following forms:
|
|
//
|
|
// 1) // TODO or /////////// TODO
|
|
//
|
|
// 2) /* TODO or /********** TODO
|
|
//
|
|
// 3) /*
|
|
// * TODO
|
|
// */
|
|
//
|
|
// The following three regexps are used to match the start of the text up to the TODO
|
|
// comment portion.
|
|
const singleLineCommentStart = /(?:\/\/+\s*)/.source;
|
|
const multiLineCommentStart = /(?:\/\*+\s*)/.source;
|
|
const anyNumberOfSpacesAndAsterisksAtStartOfLine = /(?:^(?:\s|\*)*)/.source;
|
|
|
|
// Match any of the above three TODO comment start regexps.
|
|
// Note that the outermost group *is* a capture group. We want to capture the preamble
|
|
// so that we can determine the starting position of the TODO comment match.
|
|
const preamble = "(" + anyNumberOfSpacesAndAsterisksAtStartOfLine + "|" + singleLineCommentStart + "|" + multiLineCommentStart + ")";
|
|
|
|
// Takes the descriptors and forms a regexp that matches them as if they were literals.
|
|
// For example, if the descriptors are "TODO(jason)" and "HACK", then this will be:
|
|
//
|
|
// (?:(TODO\(jason\))|(HACK))
|
|
//
|
|
// Note that the outermost group is *not* a capture group, but the innermost groups
|
|
// *are* capture groups. By capturing the inner literals we can determine after
|
|
// matching which descriptor we are dealing with.
|
|
const literals = "(?:" + map(descriptors, d => "(" + escapeRegExp(d.text) + ")").join("|") + ")";
|
|
|
|
// After matching a descriptor literal, the following regexp matches the rest of the
|
|
// text up to the end of the line (or */).
|
|
const endOfLineOrEndOfComment = /(?:$|\*\/)/.source;
|
|
const messageRemainder = /(?:.*?)/.source;
|
|
|
|
// This is the portion of the match we'll return as part of the TODO comment result. We
|
|
// match the literal portion up to the end of the line or end of comment.
|
|
const messagePortion = "(" + literals + messageRemainder + ")";
|
|
const regExpString = preamble + messagePortion + endOfLineOrEndOfComment;
|
|
|
|
// The final regexp will look like this:
|
|
// /((?:\/\/+\s*)|(?:\/\*+\s*)|(?:^(?:\s|\*)*))((?:(TODO\(jason\))|(HACK))(?:.*?))(?:$|\*\/)/gim
|
|
|
|
// The flags of the regexp are important here.
|
|
// 'g' is so that we are doing a global search and can find matches several times
|
|
// in the input.
|
|
//
|
|
// 'i' is for case insensitivity (We do this to match C# TODO comment code).
|
|
//
|
|
// 'm' is so we can find matches in a multi-line input.
|
|
return new RegExp(regExpString, "gim");
|
|
}
|
|
|
|
function isLetterOrDigit(char: number): boolean {
|
|
return (char >= CharacterCodes.a && char <= CharacterCodes.z) ||
|
|
(char >= CharacterCodes.A && char <= CharacterCodes.Z) ||
|
|
(char >= CharacterCodes._0 && char <= CharacterCodes._9);
|
|
}
|
|
|
|
function isNodeModulesFile(path: string): boolean {
|
|
return stringContains(path, "/node_modules/");
|
|
}
|
|
}
|
|
|
|
function getRenameInfo(fileName: string, position: number, options?: RenameInfoOptions): RenameInfo {
|
|
synchronizeHostData();
|
|
return Rename.getRenameInfo(program, getValidSourceFile(fileName), position, options);
|
|
}
|
|
|
|
function getRefactorContext(file: SourceFile, positionOrRange: number | TextRange, preferences: UserPreferences, formatOptions?: FormatCodeSettings): RefactorContext {
|
|
const [startPosition, endPosition] = typeof positionOrRange === "number" ? [positionOrRange, undefined] : [positionOrRange.pos, positionOrRange.end];
|
|
return {
|
|
file,
|
|
startPosition,
|
|
endPosition,
|
|
program: getProgram()!,
|
|
host,
|
|
formatContext: formatting.getFormatContext(formatOptions!), // TODO: GH#18217
|
|
cancellationToken,
|
|
preferences,
|
|
};
|
|
}
|
|
|
|
function getSmartSelectionRange(fileName: string, position: number): SelectionRange {
|
|
return SmartSelectionRange.getSmartSelectionRange(position, syntaxTreeCache.getCurrentSourceFile(fileName));
|
|
}
|
|
|
|
function getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences = emptyOptions): ApplicableRefactorInfo[] {
|
|
synchronizeHostData();
|
|
const file = getValidSourceFile(fileName);
|
|
return refactor.getApplicableRefactors(getRefactorContext(file, positionOrRange, preferences));
|
|
}
|
|
|
|
function getEditsForRefactor(
|
|
fileName: string,
|
|
formatOptions: FormatCodeSettings,
|
|
positionOrRange: number | TextRange,
|
|
refactorName: string,
|
|
actionName: string,
|
|
preferences: UserPreferences = emptyOptions,
|
|
): RefactorEditInfo | undefined {
|
|
synchronizeHostData();
|
|
const file = getValidSourceFile(fileName);
|
|
return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName);
|
|
}
|
|
|
|
function prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined {
|
|
synchronizeHostData();
|
|
const declarations = CallHierarchy.resolveCallHierarchyDeclaration(program, getTouchingPropertyName(getValidSourceFile(fileName), position));
|
|
return declarations && mapOneOrMany(declarations, declaration => CallHierarchy.createCallHierarchyItem(program, declaration));
|
|
}
|
|
|
|
function provideCallHierarchyIncomingCalls(fileName: string, position: number): CallHierarchyIncomingCall[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)));
|
|
return declaration ? CallHierarchy.getIncomingCalls(program, declaration, cancellationToken) : [];
|
|
}
|
|
|
|
function provideCallHierarchyOutgoingCalls(fileName: string, position: number): CallHierarchyOutgoingCall[] {
|
|
synchronizeHostData();
|
|
const sourceFile = getValidSourceFile(fileName);
|
|
const declaration = firstOrOnly(CallHierarchy.resolveCallHierarchyDeclaration(program, position === 0 ? sourceFile : getTouchingPropertyName(sourceFile, position)));
|
|
return declaration ? CallHierarchy.getOutgoingCalls(program, declaration) : [];
|
|
}
|
|
|
|
return {
|
|
dispose,
|
|
cleanupSemanticCache,
|
|
getSyntacticDiagnostics,
|
|
getSemanticDiagnostics,
|
|
getSuggestionDiagnostics,
|
|
getCompilerOptionsDiagnostics,
|
|
getSyntacticClassifications,
|
|
getSemanticClassifications,
|
|
getEncodedSyntacticClassifications,
|
|
getEncodedSemanticClassifications,
|
|
getCompletionsAtPosition,
|
|
getCompletionEntryDetails,
|
|
getCompletionEntrySymbol,
|
|
getSignatureHelpItems,
|
|
getQuickInfoAtPosition,
|
|
getDefinitionAtPosition,
|
|
getDefinitionAndBoundSpan,
|
|
getImplementationAtPosition,
|
|
getTypeDefinitionAtPosition,
|
|
getReferencesAtPosition,
|
|
findReferences,
|
|
getOccurrencesAtPosition,
|
|
getDocumentHighlights,
|
|
getNameOrDottedNameSpan,
|
|
getBreakpointStatementAtPosition,
|
|
getNavigateToItems,
|
|
getRenameInfo,
|
|
getSmartSelectionRange,
|
|
findRenameLocations,
|
|
getNavigationBarItems,
|
|
getNavigationTree,
|
|
getOutliningSpans,
|
|
getTodoComments,
|
|
getBraceMatchingAtPosition,
|
|
getIndentationAtPosition,
|
|
getFormattingEditsForRange,
|
|
getFormattingEditsForDocument,
|
|
getFormattingEditsAfterKeystroke,
|
|
getDocCommentTemplateAtPosition,
|
|
isValidBraceCompletionAtPosition,
|
|
getJsxClosingTagAtPosition,
|
|
getSpanOfEnclosingComment,
|
|
getCodeFixesAtPosition,
|
|
getCombinedCodeFix,
|
|
applyCodeActionCommand,
|
|
organizeImports,
|
|
getEditsForFileRename,
|
|
getEmitOutput,
|
|
getNonBoundSourceFile,
|
|
getProgram,
|
|
getApplicableRefactors,
|
|
getEditsForRefactor,
|
|
toLineColumnOffset: sourceMapper.toLineColumnOffset,
|
|
getSourceMapper: () => sourceMapper,
|
|
prepareCallHierarchy,
|
|
provideCallHierarchyIncomingCalls,
|
|
provideCallHierarchyOutgoingCalls
|
|
};
|
|
}
|
|
|
|
/* @internal */
|
|
/** Names in the name table are escaped, so an identifier `__foo` will have a name table entry `___foo`. */
|
|
export function getNameTable(sourceFile: SourceFile): UnderscoreEscapedMap<number> {
|
|
if (!sourceFile.nameTable) {
|
|
initializeNameTable(sourceFile);
|
|
}
|
|
|
|
return sourceFile.nameTable!; // TODO: GH#18217
|
|
}
|
|
|
|
function initializeNameTable(sourceFile: SourceFile): void {
|
|
const nameTable = sourceFile.nameTable = createUnderscoreEscapedMap<number>();
|
|
sourceFile.forEachChild(function walk(node) {
|
|
if (isIdentifier(node) && !isTagName(node) && node.escapedText || isStringOrNumericLiteralLike(node) && literalIsName(node)) {
|
|
const text = getEscapedTextOfIdentifierOrLiteral(node);
|
|
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
|
|
}
|
|
else if (isPrivateIdentifier(node)) {
|
|
const text = node.escapedText;
|
|
nameTable.set(text, nameTable.get(text) === undefined ? node.pos : -1);
|
|
}
|
|
|
|
forEachChild(node, walk);
|
|
if (hasJSDocNodes(node)) {
|
|
for (const jsDoc of node.jsDoc!) {
|
|
forEachChild(jsDoc, walk);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* We want to store any numbers/strings if they were a name that could be
|
|
* related to a declaration. So, if we have 'import x = require("something")'
|
|
* then we want 'something' to be in the name table. Similarly, if we have
|
|
* "a['propname']" then we want to store "propname" in the name table.
|
|
*/
|
|
function literalIsName(node: StringLiteralLike | NumericLiteral): boolean {
|
|
return isDeclarationName(node) ||
|
|
node.parent.kind === SyntaxKind.ExternalModuleReference ||
|
|
isArgumentOfElementAccessExpression(node) ||
|
|
isLiteralComputedPropertyDeclarationName(node);
|
|
}
|
|
|
|
/**
|
|
* Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 }
|
|
*/
|
|
/* @internal */
|
|
export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElementWithName | undefined {
|
|
const element = getContainingObjectLiteralElementWorker(node);
|
|
return element && (isObjectLiteralExpression(element.parent) || isJsxAttributes(element.parent)) ? element as ObjectLiteralElementWithName : undefined;
|
|
}
|
|
function getContainingObjectLiteralElementWorker(node: Node): ObjectLiteralElement | undefined {
|
|
switch (node.kind) {
|
|
case SyntaxKind.StringLiteral:
|
|
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
case SyntaxKind.NumericLiteral:
|
|
if (node.parent.kind === SyntaxKind.ComputedPropertyName) {
|
|
return isObjectLiteralElement(node.parent.parent) ? node.parent.parent : undefined;
|
|
}
|
|
// falls through
|
|
|
|
case SyntaxKind.Identifier:
|
|
return isObjectLiteralElement(node.parent) &&
|
|
(node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) &&
|
|
node.parent.name === node ? node.parent : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/* @internal */
|
|
export type ObjectLiteralElementWithName = ObjectLiteralElement & { name: PropertyName; parent: ObjectLiteralExpression | JsxAttributes };
|
|
|
|
function getSymbolAtLocationForQuickInfo(node: Node, checker: TypeChecker): Symbol | undefined {
|
|
const object = getContainingObjectLiteralElement(node);
|
|
if (object) {
|
|
const contextualType = checker.getContextualType(object.parent);
|
|
const properties = contextualType && getPropertySymbolsFromContextualType(object, checker, contextualType, /*unionSymbolOk*/ false);
|
|
if (properties && properties.length === 1) {
|
|
return first(properties);
|
|
}
|
|
}
|
|
return checker.getSymbolAtLocation(node);
|
|
}
|
|
|
|
/** Gets all symbols for one property. Does not get symbols for every property. */
|
|
/* @internal */
|
|
export function getPropertySymbolsFromContextualType(node: ObjectLiteralElementWithName, checker: TypeChecker, contextualType: Type, unionSymbolOk: boolean): readonly Symbol[] {
|
|
const name = getNameFromPropertyName(node.name);
|
|
if (!name) return emptyArray;
|
|
if (!contextualType.isUnion()) {
|
|
const symbol = contextualType.getProperty(name);
|
|
return symbol ? [symbol] : emptyArray;
|
|
}
|
|
|
|
const discriminatedPropertySymbols = mapDefined(contextualType.types, t => isObjectLiteralExpression(node.parent) && checker.isTypeInvalidDueToUnionDiscriminant(t, node.parent) ? undefined : t.getProperty(name));
|
|
if (unionSymbolOk && (discriminatedPropertySymbols.length === 0 || discriminatedPropertySymbols.length === contextualType.types.length)) {
|
|
const symbol = contextualType.getProperty(name);
|
|
if (symbol) return [symbol];
|
|
}
|
|
if (discriminatedPropertySymbols.length === 0) {
|
|
// Bad discriminant -- do again without discriminating
|
|
return mapDefined(contextualType.types, t => t.getProperty(name));
|
|
}
|
|
return discriminatedPropertySymbols;
|
|
}
|
|
|
|
function isArgumentOfElementAccessExpression(node: Node) {
|
|
return node &&
|
|
node.parent &&
|
|
node.parent.kind === SyntaxKind.ElementAccessExpression &&
|
|
(<ElementAccessExpression>node.parent).argumentExpression === node;
|
|
}
|
|
|
|
/// getDefaultLibraryFilePath
|
|
declare const __dirname: string;
|
|
|
|
/**
|
|
* Get the path of the default library files (lib.d.ts) as distributed with the typescript
|
|
* node package.
|
|
* The functionality is not supported if the ts module is consumed outside of a node module.
|
|
*/
|
|
export function getDefaultLibFilePath(options: CompilerOptions): string {
|
|
// Check __dirname is defined and that we are on a node.js system.
|
|
if (typeof __dirname !== "undefined") {
|
|
return __dirname + directorySeparator + getDefaultLibFileName(options);
|
|
}
|
|
|
|
throw new Error("getDefaultLibFilePath is only supported when consumed as a node module. ");
|
|
}
|
|
|
|
setObjectAllocator(getServicesObjectAllocator());
|
|
}
|