|
| 1 | +import { ts } from '../ts'; |
| 2 | + |
| 3 | +export type ValueDeclaration = |
| 4 | + | ts.BinaryExpression |
| 5 | + | ts.ArrowFunction |
| 6 | + | ts.BindingElement |
| 7 | + | ts.ClassDeclaration |
| 8 | + | ts.ClassExpression |
| 9 | + | ts.ClassStaticBlockDeclaration |
| 10 | + | ts.ConstructorDeclaration |
| 11 | + | ts.EnumDeclaration |
| 12 | + | ts.EnumMember |
| 13 | + | ts.ExportSpecifier |
| 14 | + | ts.FunctionDeclaration |
| 15 | + | ts.FunctionExpression |
| 16 | + | ts.GetAccessorDeclaration |
| 17 | + | ts.JsxAttribute |
| 18 | + | ts.MethodDeclaration |
| 19 | + | ts.ModuleDeclaration |
| 20 | + | ts.ParameterDeclaration |
| 21 | + | ts.PropertyAssignment |
| 22 | + | ts.PropertyDeclaration |
| 23 | + | ts.SetAccessorDeclaration |
| 24 | + | ts.ShorthandPropertyAssignment |
| 25 | + | ts.VariableDeclaration; |
| 26 | + |
| 27 | +export type ValueOfDeclaration = |
| 28 | + | ts.ClassExpression |
| 29 | + | ts.ClassDeclaration |
| 30 | + | ts.ArrowFunction |
| 31 | + | ts.ClassStaticBlockDeclaration |
| 32 | + | ts.ConstructorDeclaration |
| 33 | + | ts.EnumDeclaration |
| 34 | + | ts.FunctionDeclaration |
| 35 | + | ts.GetAccessorDeclaration |
| 36 | + | ts.SetAccessorDeclaration |
| 37 | + | ts.MethodDeclaration |
| 38 | + | ts.Expression; |
| 39 | + |
| 40 | +/** Checks if a node is a `ts.Declaration` and a value. |
| 41 | + * @remarks |
| 42 | + * This checks if a given node is a value declaration only, |
| 43 | + * excluding import/export specifiers, type declarations, and |
| 44 | + * ambient declarations. |
| 45 | + * All declarations that aren't JS(x) nodes will be discarded. |
| 46 | + * This is based on `ts.isDeclarationKind`. |
| 47 | + */ |
| 48 | +export function isValueDeclaration(node: ts.Node): node is ValueDeclaration { |
| 49 | + switch (node.kind) { |
| 50 | + case ts.SyntaxKind.BinaryExpression: |
| 51 | + case ts.SyntaxKind.ArrowFunction: |
| 52 | + case ts.SyntaxKind.BindingElement: |
| 53 | + case ts.SyntaxKind.ClassDeclaration: |
| 54 | + case ts.SyntaxKind.ClassExpression: |
| 55 | + case ts.SyntaxKind.ClassStaticBlockDeclaration: |
| 56 | + case ts.SyntaxKind.Constructor: |
| 57 | + case ts.SyntaxKind.EnumDeclaration: |
| 58 | + case ts.SyntaxKind.EnumMember: |
| 59 | + case ts.SyntaxKind.FunctionDeclaration: |
| 60 | + case ts.SyntaxKind.FunctionExpression: |
| 61 | + case ts.SyntaxKind.GetAccessor: |
| 62 | + case ts.SyntaxKind.JsxAttribute: |
| 63 | + case ts.SyntaxKind.MethodDeclaration: |
| 64 | + case ts.SyntaxKind.Parameter: |
| 65 | + case ts.SyntaxKind.PropertyAssignment: |
| 66 | + case ts.SyntaxKind.PropertyDeclaration: |
| 67 | + case ts.SyntaxKind.SetAccessor: |
| 68 | + case ts.SyntaxKind.ShorthandPropertyAssignment: |
| 69 | + case ts.SyntaxKind.VariableDeclaration: |
| 70 | + return true; |
| 71 | + default: |
| 72 | + return false; |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +/** Returns true if operator assigns a value unchanged */ |
| 77 | +function isAssignmentOperator(token: ts.BinaryOperatorToken): boolean { |
| 78 | + switch (token.kind) { |
| 79 | + case ts.SyntaxKind.EqualsToken: |
| 80 | + case ts.SyntaxKind.BarBarEqualsToken: |
| 81 | + case ts.SyntaxKind.AmpersandAmpersandEqualsToken: |
| 82 | + case ts.SyntaxKind.QuestionQuestionEqualsToken: |
| 83 | + return true; |
| 84 | + default: |
| 85 | + return false; |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +/** Evaluates to the declaration's value initializer or itself if it declares a value */ |
| 90 | +export function getValueOfValueDeclaration( |
| 91 | + node: ValueDeclaration |
| 92 | +): ValueOfDeclaration | undefined { |
| 93 | + switch (node.kind) { |
| 94 | + case ts.SyntaxKind.ClassExpression: |
| 95 | + case ts.SyntaxKind.ClassDeclaration: |
| 96 | + case ts.SyntaxKind.ArrowFunction: |
| 97 | + case ts.SyntaxKind.ClassStaticBlockDeclaration: |
| 98 | + case ts.SyntaxKind.Constructor: |
| 99 | + case ts.SyntaxKind.EnumDeclaration: |
| 100 | + case ts.SyntaxKind.FunctionDeclaration: |
| 101 | + case ts.SyntaxKind.FunctionExpression: |
| 102 | + case ts.SyntaxKind.GetAccessor: |
| 103 | + case ts.SyntaxKind.SetAccessor: |
| 104 | + case ts.SyntaxKind.MethodDeclaration: |
| 105 | + return node; |
| 106 | + case ts.SyntaxKind.BindingElement: |
| 107 | + case ts.SyntaxKind.EnumMember: |
| 108 | + case ts.SyntaxKind.JsxAttribute: |
| 109 | + case ts.SyntaxKind.Parameter: |
| 110 | + case ts.SyntaxKind.PropertyAssignment: |
| 111 | + case ts.SyntaxKind.PropertyDeclaration: |
| 112 | + case ts.SyntaxKind.VariableDeclaration: |
| 113 | + return node.initializer; |
| 114 | + case ts.SyntaxKind.BinaryExpression: |
| 115 | + return isAssignmentOperator(node.operatorToken) ? node.right : undefined; |
| 116 | + case ts.SyntaxKind.ShorthandPropertyAssignment: |
| 117 | + return node.objectAssignmentInitializer; |
| 118 | + default: |
| 119 | + return undefined; |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L652-L654 |
| 124 | +function climbPastPropertyOrElementAccess(node: ts.Node): ts.Node { |
| 125 | + if ( |
| 126 | + node.parent && |
| 127 | + ts.isPropertyAccessExpression(node.parent) && |
| 128 | + node.parent.name === node |
| 129 | + ) { |
| 130 | + return node.parent; |
| 131 | + } else if ( |
| 132 | + node.parent && |
| 133 | + ts.isElementAccessExpression(node.parent) && |
| 134 | + node.parent.argumentExpression === node |
| 135 | + ) { |
| 136 | + return node.parent; |
| 137 | + } else { |
| 138 | + return node; |
| 139 | + } |
| 140 | +} |
| 141 | + |
| 142 | +// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L602-L605 |
| 143 | +function isNewExpressionTarget(node: ts.Node): node is ts.NewExpression { |
| 144 | + const target = climbPastPropertyOrElementAccess(node).parent; |
| 145 | + return ts.isNewExpression(target) && target.expression === node; |
| 146 | +} |
| 147 | + |
| 148 | +// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L607-L610 |
| 149 | +function isCallOrNewExpressionTarget( |
| 150 | + node: ts.Node |
| 151 | +): node is ts.CallExpression | ts.NewExpression { |
| 152 | + const target = climbPastPropertyOrElementAccess(node).parent; |
| 153 | + return ts.isCallOrNewExpression(target) && target.expression === node; |
| 154 | +} |
| 155 | + |
| 156 | +// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L716-L719 |
| 157 | +function isNameOfFunctionDeclaration(node: ts.Node): boolean { |
| 158 | + return ( |
| 159 | + ts.isIdentifier(node) && |
| 160 | + node.parent && |
| 161 | + ts.isFunctionLike(node.parent) && |
| 162 | + node.parent.name === node |
| 163 | + ); |
| 164 | +} |
| 165 | + |
| 166 | +// See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/utilities.ts#L2441-L2447 |
| 167 | +function getNameFromPropertyName(name: ts.PropertyName): string | undefined { |
| 168 | + if (ts.isComputedPropertyName(name)) { |
| 169 | + return ts.isStringLiteralLike(name.expression) || |
| 170 | + ts.isNumericLiteral(name.expression) |
| 171 | + ? name.expression.text |
| 172 | + : undefined; |
| 173 | + } else if (ts.isPrivateIdentifier(name) || ts.isMemberName(name)) { |
| 174 | + return ts.idText(name); |
| 175 | + } else { |
| 176 | + return name.text; |
| 177 | + } |
| 178 | +} |
| 179 | + |
| 180 | +/** Resolves the declaration of an identifier. |
| 181 | + * @remarks |
| 182 | + * This returns the declaration node first found for an identifier by resolving an identifier's |
| 183 | + * symbol via the type checker. |
| 184 | + * @privateRemarks |
| 185 | + * This mirrors the implementation of `getDefinitionAtPosition` in TS' language service. However, |
| 186 | + * it removes all cases that aren't applicable to identifiers and removes the intermediary positional |
| 187 | + * data structure, instead returning raw AST nodes. |
| 188 | + */ |
| 189 | +export function getDeclarationOfIdentifier( |
| 190 | + node: ts.Identifier, |
| 191 | + checker: ts.TypeChecker |
| 192 | +): ValueDeclaration | undefined { |
| 193 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L523-L540 |
| 194 | + let symbol = checker.getSymbolAtLocation(node); |
| 195 | + if ( |
| 196 | + symbol?.declarations?.[0] && |
| 197 | + symbol.flags & ts.SymbolFlags.Alias && |
| 198 | + (node.parent === symbol?.declarations?.[0] || |
| 199 | + !ts.isNamespaceImport(symbol.declarations[0])) |
| 200 | + ) { |
| 201 | + // Resolve alias symbols, excluding self-referential symbols |
| 202 | + const aliased = checker.getAliasedSymbol(symbol); |
| 203 | + if (aliased.declarations) symbol = aliased; |
| 204 | + } |
| 205 | + |
| 206 | + if (symbol && ts.isShorthandPropertyAssignment(node.parent)) { |
| 207 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L248-L257 |
| 208 | + // Resolve shorthand property assignments |
| 209 | + const shorthandSymbol = checker.getShorthandAssignmentValueSymbol( |
| 210 | + symbol.valueDeclaration |
| 211 | + ); |
| 212 | + if (shorthandSymbol) symbol = shorthandSymbol; |
| 213 | + } else if ( |
| 214 | + ts.isBindingElement(node.parent) && |
| 215 | + ts.isObjectBindingPattern(node.parent.parent) && |
| 216 | + node === (node.parent.propertyName || node.parent.name) |
| 217 | + ) { |
| 218 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L259-L280 |
| 219 | + // Resolve symbol of property in shorthand assignments |
| 220 | + const name = getNameFromPropertyName(node); |
| 221 | + const prop = name |
| 222 | + ? checker.getTypeAtLocation(node.parent.parent).getProperty(name) |
| 223 | + : undefined; |
| 224 | + if (prop) symbol = prop; |
| 225 | + } else if ( |
| 226 | + ts.isObjectLiteralElement(node.parent) && |
| 227 | + (ts.isObjectLiteralExpression(node.parent.parent) || |
| 228 | + ts.isJsxAttributes(node.parent.parent)) && |
| 229 | + node.parent.name === node |
| 230 | + ) { |
| 231 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L298-L316 |
| 232 | + // Resolve symbol of property in object literal destructre expressions |
| 233 | + const name = getNameFromPropertyName(node); |
| 234 | + const prop = name |
| 235 | + ? checker.getContextualType(node.parent.parent)?.getProperty(name) |
| 236 | + : undefined; |
| 237 | + if (prop) symbol = prop; |
| 238 | + } |
| 239 | + |
| 240 | + if (symbol && symbol.declarations?.length) { |
| 241 | + if ( |
| 242 | + symbol.flags & ts.SymbolFlags.Class && |
| 243 | + !(symbol.flags & (ts.SymbolFlags.Function | ts.SymbolFlags.Variable)) && |
| 244 | + isNewExpressionTarget(node) |
| 245 | + ) { |
| 246 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L603-L610 |
| 247 | + // Resolve first class-like declaration for new expressions |
| 248 | + for (const declaration of symbol.declarations) { |
| 249 | + if (ts.isClassLike(declaration)) return declaration; |
| 250 | + } |
| 251 | + } else if ( |
| 252 | + isCallOrNewExpressionTarget(node) || |
| 253 | + isNameOfFunctionDeclaration(node) |
| 254 | + ) { |
| 255 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L612-L616 |
| 256 | + // Resolve first function-like declaration for call expressions or named functions |
| 257 | + for (const declaration of symbol.declarations) { |
| 258 | + if ( |
| 259 | + ts.isFunctionLike(declaration) && |
| 260 | + !!(declaration as ts.FunctionLikeDeclaration).body && |
| 261 | + isValueDeclaration(declaration) |
| 262 | + ) { |
| 263 | + return declaration; |
| 264 | + } |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + // Account for assignments to property access expressions |
| 269 | + // This resolves property access expressions to binding element parents |
| 270 | + if ( |
| 271 | + symbol.valueDeclaration && |
| 272 | + ts.isPropertyAccessExpression(symbol.valueDeclaration) |
| 273 | + ) { |
| 274 | + const parent = symbol.valueDeclaration.parent; |
| 275 | + if ( |
| 276 | + parent && |
| 277 | + ts.isBinaryExpression(parent) && |
| 278 | + parent.left === symbol.valueDeclaration |
| 279 | + ) { |
| 280 | + return parent; |
| 281 | + } |
| 282 | + } |
| 283 | + |
| 284 | + if ( |
| 285 | + symbol.valueDeclaration && |
| 286 | + isValueDeclaration(symbol.valueDeclaration) |
| 287 | + ) { |
| 288 | + // NOTE: We prefer value declarations, since the checker may have already applied conditions |
| 289 | + // similar to `isValueDeclaration` and selected it beforehand |
| 290 | + // Only use value declarations if they're not type/ambient declarations or imports/exports |
| 291 | + return symbol.valueDeclaration; |
| 292 | + } |
| 293 | + |
| 294 | + // Selecting the first available result, if any |
| 295 | + // NOTE: We left out `!isExpandoDeclaration` as a condition, since `valueDeclaration` above |
| 296 | + // should handle some of these cases, and we don't have to care about this subtlety as much for identifiers |
| 297 | + // See: https://github.com/microsoft/TypeScript/blob/a5eec24/src/services/goToDefinition.ts#L582-L590 |
| 298 | + for (const declaration of symbol.declarations) { |
| 299 | + // Only use declarations if they're not type/ambient declarations or imports/exports |
| 300 | + if (isValueDeclaration(declaration)) return declaration; |
| 301 | + } |
| 302 | + } |
| 303 | + |
| 304 | + return undefined; |
| 305 | +} |
| 306 | + |
| 307 | +/** Loops {@link getDeclarationOfIdentifier} until a value of the identifier is found */ |
| 308 | +export function getValueOfIdentifier( |
| 309 | + node: ts.Identifier, |
| 310 | + checker: ts.TypeChecker |
| 311 | +): ValueOfDeclaration | undefined { |
| 312 | + while (ts.isIdentifier(node)) { |
| 313 | + const declaration = getDeclarationOfIdentifier(node, checker); |
| 314 | + if (!declaration) { |
| 315 | + return undefined; |
| 316 | + } else { |
| 317 | + const value = getValueOfValueDeclaration(declaration); |
| 318 | + if (value && ts.isIdentifier(value) && value !== node) { |
| 319 | + // If the resolved value is another identifiers, we continue searching, if the |
| 320 | + // identifier isn't self-referential |
| 321 | + node = value; |
| 322 | + } else { |
| 323 | + return value; |
| 324 | + } |
| 325 | + } |
| 326 | + } |
| 327 | +} |
| 328 | + |
| 329 | +/** Resolves exressions that might not influence the target identifier */ |
| 330 | +export function getIdentifierOfChainExpression( |
| 331 | + node: ts.Expression |
| 332 | +): ts.Identifier | undefined { |
| 333 | + let target: ts.Expression | undefined = node; |
| 334 | + while (target) { |
| 335 | + if (ts.isPropertyAccessExpression(target)) { |
| 336 | + target = target.name; |
| 337 | + } else if ( |
| 338 | + ts.isAsExpression(target) || |
| 339 | + ts.isSatisfiesExpression(target) || |
| 340 | + ts.isNonNullExpression(target) || |
| 341 | + ts.isParenthesizedExpression(target) || |
| 342 | + ts.isExpressionWithTypeArguments(target) |
| 343 | + ) { |
| 344 | + target = target.expression; |
| 345 | + } else if (ts.isCommaListExpression(target)) { |
| 346 | + target = target.elements[target.elements.length - 1]; |
| 347 | + } else if (ts.isIdentifier(target)) { |
| 348 | + return target; |
| 349 | + } else { |
| 350 | + return; |
| 351 | + } |
| 352 | + } |
| 353 | +} |
0 commit comments