Skip to content

JS: Disable type extraction #19640

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
de9dab9
JS: Move some predicates into NameResolution
asgerf Jun 4, 2025
2a0c7c8
JS: Add classHasGlobalName into NameResolution
asgerf Jun 4, 2025
b82e849
JS: Add public API
asgerf Jun 4, 2025
17a687b
JS: Update type usage in Nest library model
asgerf Jun 2, 2025
9d4c38b
JS: Update type usage in definitions.qll
asgerf Jun 3, 2025
ace8b09
JS: Update type usage in ClassValidator.qll
asgerf Jun 11, 2025
b71d096
JS: Update type usage in Electron model
asgerf Jun 2, 2025
8b2a424
JS: Update type usage use in Express model
asgerf Jun 2, 2025
fb92d9b
JS: Update type usage in UnreachableMethodOverloads
asgerf Jun 2, 2025
e459884
JS: Update API usage in ViewComponentInput
asgerf Jun 4, 2025
fcb6882
JS: Update API usage in MissingAwait
asgerf Jun 4, 2025
6d389c3
JS: Update an outdated QLDoc comment
asgerf Jun 2, 2025
f5ac3fd
JS: Remove old metric-meta query TypedExprs.ql
asgerf Jun 3, 2025
ee9c4fa
JS: Deprecate everything that depends on type extraction
asgerf Jun 2, 2025
f5f12c2
JS: Delete or simplify TypeScript type-specific tests
asgerf Jun 2, 2025
1cab992
JS: Remove unneeded integration test
asgerf Jun 2, 2025
07f84a5
JS: Remove an unnecessary import
asgerf Jun 4, 2025
e323833
JS: Fix qldoc coverage
asgerf Jun 4, 2025
8efa38b
JS: Change default TypeScript extraction mode to basic
asgerf Jun 4, 2025
74b817b
JS: Remove code path for TypeScript full extraction
asgerf Jun 24, 2025
488da14
JS: Don't try to augment invalid files
asgerf Jun 24, 2025
92dd5bd
JS: Add deprecation comment to qldoc
asgerf Jun 4, 2025
7cc2487
JS: Add test for dynamic imports
asgerf Jun 10, 2025
b1d4776
JS: Handle name resolution through dynamic imports
asgerf Jun 10, 2025
c8b2674
JS: Add support for index expressions
asgerf Jun 12, 2025
aef3621
JS: Change notes
asgerf Jun 24, 2025
02cdde1
JS: Fix imprecise condition
asgerf Jun 25, 2025
5289e4f
JS: Fix a bug in a unit test
asgerf Jun 25, 2025
2aad147
JS: Remove TypeScriptMode
asgerf Jul 2, 2025
4b2025d
JS: Remove obsolete unit tests
asgerf Jul 2, 2025
47a90c8
Merge branch 'main' into js/no-type-extraction
asgerf Jul 2, 2025
d858384
JS: Update Nest model
asgerf Jul 2, 2025
98319ce
Apply suggestions from code review
asgerf Jul 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 1 addition & 205 deletions javascript/extractor/lib/typescript/src/ast_extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,13 @@ export interface AugmentedSourceFile extends ts.SourceFile {
/** Internal property that we expose as a workaround. */
redirectInfo?: object | null;
$tokens?: Token[];
$symbol?: number;
$lineStarts?: ReadonlyArray<number>;
}

export interface AugmentedNode extends ts.Node {
$pos?: any;
$end?: any;
$declarationKind?: string;
$type?: number;
$symbol?: number;
$resolvedSignature?: number;
$overloadIndex?: number;
$declaredSignature?: number;
}

export type AugmentedPos = number;
Expand Down Expand Up @@ -73,7 +67,7 @@ function tryGetTypeOfNode(typeChecker: ts.TypeChecker, node: AugmentedNode): ts.
} catch (e) {
let sourceFile = node.getSourceFile();
let { line, character } = sourceFile.getLineAndCharacterOfPosition(node.pos);
console.warn(`Could not compute type of ${ts.SyntaxKind[node.kind]} at ${sourceFile.fileName}:${line+1}:${character+1}`);
console.warn(`Could not compute type of ${ts.SyntaxKind[node.kind]} at ${sourceFile.fileName}:${line + 1}:${character + 1}`);
return null;
}
}
Expand Down Expand Up @@ -157,17 +151,6 @@ export function augmentAst(ast: AugmentedSourceFile, code: string, project: Proj
});
}

let typeChecker = project && project.program.getTypeChecker();
let typeTable = project && project.typeTable;

// Associate a symbol with the AST node root, in case it is a module.
if (typeTable != null) {
let symbol = typeChecker.getSymbolAtLocation(ast);
if (symbol != null) {
ast.$symbol = typeTable.getSymbolId(symbol);
}
}

visitAstNode(ast);
function visitAstNode(node: AugmentedNode) {
ts.forEachChild(node, visitAstNode);
Expand All @@ -190,192 +173,5 @@ export function augmentAst(ast: AugmentedSourceFile, code: string, project: Proj
node.$declarationKind = "var";
}
}

if (typeChecker != null) {
if (isTypedNode(node) && !typeTable.skipExtractingTypes) {
let contextualType = isContextuallyTypedNode(node)
? typeChecker.getContextualType(node)
: null;
let type = contextualType || tryGetTypeOfNode(typeChecker, node);
if (type != null) {
let parent = node.parent;
let unfoldAlias = ts.isTypeAliasDeclaration(parent) && node === parent.type;
let id = typeTable.buildType(type, unfoldAlias);
if (id != null) {
node.$type = id;
}
}
// Extract the target call signature of a function call.
// In case the callee is overloaded or generic, this is not something we can
// derive from the callee type in QL.
if (ts.isCallOrNewExpression(node)) {
let kind = ts.isCallExpression(node) ? ts.SignatureKind.Call : ts.SignatureKind.Construct;
let resolvedSignature = typeChecker.getResolvedSignature(node);
if (resolvedSignature != null) {
let resolvedId = typeTable.getSignatureId(kind, resolvedSignature);
if (resolvedId != null) {
(node as AugmentedNode).$resolvedSignature = resolvedId;
}
let declaration = resolvedSignature.declaration;
if (declaration != null) {
// Find the generic signature, i.e. without call-site type arguments substituted,
// but with overloading resolved.
let calleeType = typeChecker.getTypeAtLocation(node.expression);
if (calleeType != null && declaration != null) {
let calleeSignatures = typeChecker.getSignaturesOfType(calleeType, kind);
for (let i = 0; i < calleeSignatures.length; ++i) {
if (calleeSignatures[i].declaration === declaration) {
(node as AugmentedNode).$overloadIndex = i;
break;
}
}
}
// Extract the symbol so the declaration can be found from QL.
let name = (declaration as any).name;
let symbol = name && typeChecker.getSymbolAtLocation(name);
if (symbol != null) {
(node as AugmentedNode).$symbol = typeTable.getSymbolId(symbol);
}
}
}
}
}
let symbolNode =
isNamedNodeWithSymbol(node) ? node.name :
ts.isImportDeclaration(node) ? node.moduleSpecifier :
ts.isExternalModuleReference(node) ? node.expression :
null;
if (symbolNode != null) {
let symbol = typeChecker.getSymbolAtLocation(symbolNode);
if (symbol != null) {
node.$symbol = typeTable.getSymbolId(symbol);
}
}
if (ts.isTypeReferenceNode(node)) {
// For type references we inject a symbol on each part of the name.
// We traverse each node in the name here since we know these are part of
// a type annotation. This means we don't have to do it for all identifiers
// and qualified names, which would extract more information than we need.
let namePart: (ts.EntityName & AugmentedNode) = node.typeName;
while (ts.isQualifiedName(namePart)) {
let symbol = typeChecker.getSymbolAtLocation(namePart.right);
if (symbol != null) {
namePart.$symbol = typeTable.getSymbolId(symbol);
}

// Traverse into the prefix.
namePart = namePart.left;
}
let symbol = typeChecker.getSymbolAtLocation(namePart);
if (symbol != null) {
namePart.$symbol = typeTable.getSymbolId(symbol);
}
}
if (ts.isFunctionLike(node)) {
let signature = typeChecker.getSignatureFromDeclaration(node);
if (signature != null) {
let kind = ts.isConstructSignatureDeclaration(node) || ts.isConstructorDeclaration(node)
? ts.SignatureKind.Construct : ts.SignatureKind.Call;
let id = typeTable.getSignatureId(kind, signature);
if (id != null) {
(node as AugmentedNode).$declaredSignature = id;
}
}
}
}
}
}

type NamedNodeWithSymbol = AugmentedNode & (ts.ClassDeclaration | ts.InterfaceDeclaration
| ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.EnumMember | ts.ModuleDeclaration | ts.FunctionDeclaration
| ts.MethodDeclaration | ts.MethodSignature);

/**
* True if the given AST node has a name, and should be associated with a symbol.
*/
function isNamedNodeWithSymbol(node: ts.Node): node is NamedNodeWithSymbol {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.TypeAliasDeclaration:
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.EnumMember:
case ts.SyntaxKind.ModuleDeclaration:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
return true;
}
return false;
}

/**
* True if the given AST node has a type.
*/
function isTypedNode(node: ts.Node): boolean {
switch (node.kind) {
case ts.SyntaxKind.ArrayLiteralExpression:
case ts.SyntaxKind.ArrowFunction:
case ts.SyntaxKind.AsExpression:
case ts.SyntaxKind.SatisfiesExpression:
case ts.SyntaxKind.AwaitExpression:
case ts.SyntaxKind.BinaryExpression:
case ts.SyntaxKind.CallExpression:
case ts.SyntaxKind.ClassExpression:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.CommaListExpression:
case ts.SyntaxKind.ConditionalExpression:
case ts.SyntaxKind.Constructor:
case ts.SyntaxKind.DeleteExpression:
case ts.SyntaxKind.ElementAccessExpression:
case ts.SyntaxKind.ExpressionStatement:
case ts.SyntaxKind.ExpressionWithTypeArguments:
case ts.SyntaxKind.FalseKeyword:
case ts.SyntaxKind.FunctionDeclaration:
case ts.SyntaxKind.FunctionExpression:
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.Identifier:
case ts.SyntaxKind.IndexSignature:
case ts.SyntaxKind.JsxExpression:
case ts.SyntaxKind.LiteralType:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
case ts.SyntaxKind.NewExpression:
case ts.SyntaxKind.NonNullExpression:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.NumericLiteral:
case ts.SyntaxKind.ObjectKeyword:
case ts.SyntaxKind.ObjectLiteralExpression:
case ts.SyntaxKind.OmittedExpression:
case ts.SyntaxKind.ParenthesizedExpression:
case ts.SyntaxKind.PartiallyEmittedExpression:
case ts.SyntaxKind.PostfixUnaryExpression:
case ts.SyntaxKind.PrefixUnaryExpression:
case ts.SyntaxKind.PropertyAccessExpression:
case ts.SyntaxKind.RegularExpressionLiteral:
case ts.SyntaxKind.SetAccessor:
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.TaggedTemplateExpression:
case ts.SyntaxKind.TemplateExpression:
case ts.SyntaxKind.TemplateHead:
case ts.SyntaxKind.TemplateMiddle:
case ts.SyntaxKind.TemplateSpan:
case ts.SyntaxKind.TemplateTail:
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.TypeAssertionExpression:
case ts.SyntaxKind.TypeLiteral:
case ts.SyntaxKind.TypeOfExpression:
case ts.SyntaxKind.VoidExpression:
case ts.SyntaxKind.YieldExpression:
return true;
default:
return ts.isTypeNode(node);
}
}

type ContextuallyTypedNode = (ts.ArrayLiteralExpression | ts.ObjectLiteralExpression) & AugmentedNode;

function isContextuallyTypedNode(node: ts.Node): node is ContextuallyTypedNode {
let kind = node.kind;
return kind === ts.SyntaxKind.ArrayLiteralExpression || kind === ts.SyntaxKind.ObjectLiteralExpression;
}
104 changes: 3 additions & 101 deletions javascript/extractor/lib/typescript/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,26 @@
import * as ts from "./typescript";
import { TypeTable } from "./type_table";
import * as pathlib from "path";
import { VirtualSourceRoot } from "./virtual_source_root";

/**
* Extracts the package name from the prefix of an import string.
*/
const packageNameRex = /^(?:@[\w.-]+[/\\]+)?\w[\w.-]*(?=[/\\]|$)/;
const extensions = ['.ts', '.tsx', '.d.ts', '.js', '.jsx'];

function getPackageName(importString: string) {
let packageNameMatch = packageNameRex.exec(importString);
if (packageNameMatch == null) return null;
let packageName = packageNameMatch[0];
if (packageName.charAt(0) === '@') {
packageName = packageName.replace(/[/\\]+/g, '/'); // Normalize slash after the scope.
}
return packageName;
}

export class Project {
public program: ts.Program = null;
private host: ts.CompilerHost;
private resolutionCache: ts.ModuleResolutionCache;

constructor(
public tsConfig: string,
public config: ts.ParsedCommandLine,
public typeTable: TypeTable,
public packageEntryPoints: Map<string, string>,
public virtualSourceRoot: VirtualSourceRoot) {

this.resolveModuleNames = this.resolveModuleNames.bind(this);
public tsConfig: string,
public config: ts.ParsedCommandLine,
public packageEntryPoints: Map<string, string>) {

this.resolutionCache = ts.createModuleResolutionCache(pathlib.dirname(tsConfig), ts.sys.realpath, config.options);
let host = ts.createCompilerHost(config.options, true);
host.resolveModuleNames = this.resolveModuleNames;
host.trace = undefined; // Disable tracing which would otherwise go to standard out
this.host = host;
}

public unload(): void {
this.typeTable.releaseProgram();
this.program = null;
}

public load(): void {
const { config, host } = this;
this.program = ts.createProgram(config.fileNames, config.options, host);
this.typeTable.setProgram(this.program, this.virtualSourceRoot);
}

/**
Expand All @@ -60,74 +32,4 @@ export class Project {
this.unload();
this.load();
}

/**
* Override for module resolution in the TypeScript compiler host.
*/
private resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[],
redirectedReference: ts.ResolvedProjectReference,
options: ts.CompilerOptions) {

let oppositePath =
this.virtualSourceRoot.toVirtualPath(containingFile) ||
this.virtualSourceRoot.fromVirtualPath(containingFile);

const { host, resolutionCache } = this;
return moduleNames.map((moduleName) => {
let redirected = this.redirectModuleName(moduleName, containingFile, options);
if (redirected != null) return redirected;
if (oppositePath != null) {
// If the containing file is in the virtual source root, try resolving from the real source root, and vice versa.
redirected = ts.resolveModuleName(moduleName, oppositePath, options, host, resolutionCache).resolvedModule;
if (redirected != null) return redirected;
}
return ts.resolveModuleName(moduleName, containingFile, options, host, resolutionCache).resolvedModule;
});
}

/**
* Returns the path that the given import string should be redirected to, or null if it should
* fall back to standard module resolution.
*/
private redirectModuleName(moduleName: string, containingFile: string, options: ts.CompilerOptions): ts.ResolvedModule {
// Get a package name from the leading part of the module name, e.g. '@scope/foo' from '@scope/foo/bar'.
let packageName = getPackageName(moduleName);
if (packageName == null) return null;

// Get the overridden location of this package, if one exists.
let packageEntryPoint = this.packageEntryPoints.get(packageName);
if (packageEntryPoint == null) return null;

// If the requested module name is exactly the overridden package name,
// return the entry point file (it is not necessarily called `index.ts`).
if (moduleName === packageName) {
return { resolvedFileName: packageEntryPoint, isExternalLibraryImport: true };
}

// Get the suffix after the package name, e.g. the '/bar' in '@scope/foo/bar'.
let suffix = moduleName.substring(packageName.length);

// Resolve the suffix relative to the package directory.
let packageDir = pathlib.dirname(packageEntryPoint);
let joinedPath = pathlib.join(packageDir, suffix);

// Add implicit '/index'
if (ts.sys.directoryExists(joinedPath)) {
joinedPath = pathlib.join(joinedPath, 'index');
}

// Try each recognized extension. We must not return a file whose extension is not
// recognized by TypeScript.
for (let ext of extensions) {
let candidate = joinedPath.endsWith(ext) ? joinedPath : (joinedPath + ext);
if (ts.sys.fileExists(candidate)) {
return { resolvedFileName: candidate, isExternalLibraryImport: true };
}
}

return null;
}
}
Loading