Skip to content

Commit bfb734e

Browse files
authored
Merge pull request github#3832 from asger-semmle/js/typescript-in-html-files3
Approved by erik-krogh
2 parents 2bd84a3 + 7a2c65f commit bfb734e

25 files changed

+595
-157
lines changed

change-notes/1.25/analysis-javascript.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
* TypeScript 3.9 is now supported.
3131

32+
* TypeScript code embedded in HTML and Vue files is now extracted and analyzed.
33+
3234
* The analysis of sanitizers has improved, leading to more accurate
3335
results from the security queries.
3436

javascript/extractor/lib/typescript/src/common.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class Project {
4848
public load(): void {
4949
const { config, host } = this;
5050
this.program = ts.createProgram(config.fileNames, config.options, host);
51-
this.typeTable.setProgram(this.program);
51+
this.typeTable.setProgram(this.program, this.virtualSourceRoot);
5252
}
5353

5454
/**
@@ -71,10 +71,19 @@ export class Project {
7171
redirectedReference: ts.ResolvedProjectReference,
7272
options: ts.CompilerOptions) {
7373

74+
let oppositePath =
75+
this.virtualSourceRoot.toVirtualPath(containingFile) ||
76+
this.virtualSourceRoot.fromVirtualPath(containingFile);
77+
7478
const { host, resolutionCache } = this;
7579
return moduleNames.map((moduleName) => {
7680
let redirected = this.redirectModuleName(moduleName, containingFile, options);
7781
if (redirected != null) return redirected;
82+
if (oppositePath != null) {
83+
// If the containing file is in the virtual source root, try resolving from the real source root, and vice versa.
84+
redirected = ts.resolveModuleName(moduleName, oppositePath, options, host, resolutionCache).resolvedModule;
85+
if (redirected != null) return redirected;
86+
}
7887
return ts.resolveModuleName(moduleName, containingFile, options, host, resolutionCache).resolvedModule;
7988
});
8089
}
@@ -90,15 +99,7 @@ export class Project {
9099

91100
// Get the overridden location of this package, if one exists.
92101
let packageEntryPoint = this.packageEntryPoints.get(packageName);
93-
if (packageEntryPoint == null) {
94-
// The package is not overridden, but we have established that it begins with a valid package name.
95-
// Do a lookup in the virtual source root (where dependencies are installed) by changing the 'containing file'.
96-
let virtualContainingFile = this.virtualSourceRoot.toVirtualPath(containingFile);
97-
if (virtualContainingFile != null) {
98-
return ts.resolveModuleName(moduleName, virtualContainingFile, options, this.host, this.resolutionCache).resolvedModule;
99-
}
100-
return null;
101-
}
102+
if (packageEntryPoint == null) return null;
102103

103104
// If the requested module name is exactly the overridden package name,
104105
// return the entry point file (it is not necessarily called `index.ts`).

javascript/extractor/lib/typescript/src/main.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,25 @@ function loadTsConfig(command: LoadCommand): LoadedConfig {
414414
*/
415415
let parseConfigHost: ts.ParseConfigHost = {
416416
useCaseSensitiveFileNames: true,
417-
readDirectory: ts.sys.readDirectory, // No need to override traversal/glob matching
417+
readDirectory: (rootDir, extensions, excludes?, includes?, depth?) => {
418+
// Perform the glob matching in both real and virtual source roots.
419+
let exclusions = excludes == null ? [] : [...excludes];
420+
if (virtualSourceRoot.virtualSourceRoot != null) {
421+
// qltest puts the virtual source root inside the real source root (.testproj).
422+
// Make sure we don't find files inside the virtual source root in this pass.
423+
exclusions.push(virtualSourceRoot.virtualSourceRoot);
424+
}
425+
let originalResults = ts.sys.readDirectory(rootDir, extensions, exclusions, includes, depth)
426+
let virtualDir = virtualSourceRoot.toVirtualPath(rootDir);
427+
if (virtualDir == null) {
428+
return originalResults;
429+
}
430+
// Make sure glob matching does not to discover anything in node_modules.
431+
let virtualExclusions = excludes == null ? [] : [...excludes];
432+
virtualExclusions.push('**/node_modules/**/*');
433+
let virtualResults = ts.sys.readDirectory(virtualDir, extensions, virtualExclusions, includes, depth)
434+
return [ ...originalResults, ...virtualResults ];
435+
},
418436
fileExists: (path: string) => {
419437
return ts.sys.fileExists(path)
420438
|| virtualSourceRoot.toVirtualPathIfFileExists(path) != null

javascript/extractor/lib/typescript/src/type_table.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as ts from "./typescript";
2+
import { VirtualSourceRoot } from "./virtual_source_root";
23

34
interface AugmentedSymbol extends ts.Symbol {
45
parent?: AugmentedSymbol;
@@ -379,12 +380,15 @@ export class TypeTable {
379380
*/
380381
public restrictedExpansion = false;
381382

383+
private virtualSourceRoot: VirtualSourceRoot;
384+
382385
/**
383386
* Called when a new compiler instance has started.
384387
*/
385-
public setProgram(program: ts.Program) {
388+
public setProgram(program: ts.Program, virtualSourceRoot: VirtualSourceRoot) {
386389
this.typeChecker = program.getTypeChecker();
387390
this.arbitraryAstNode = program.getSourceFiles()[0];
391+
this.virtualSourceRoot = virtualSourceRoot;
388392
}
389393

390394
/**
@@ -703,14 +707,21 @@ export class TypeTable {
703707
private getSymbolString(symbol: AugmentedSymbol): string {
704708
let parent = symbol.parent;
705709
if (parent == null || parent.escapedName === ts.InternalSymbolName.Global) {
706-
return "root;" + this.getSymbolDeclarationString(symbol) + ";;" + symbol.name;
710+
return "root;" + this.getSymbolDeclarationString(symbol) + ";;" + this.rewriteSymbolName(symbol);
707711
} else if (parent.exports != null && parent.exports.get(symbol.escapedName) === symbol) {
708-
return "member;;" + this.getSymbolId(parent) + ";" + symbol.name;
712+
return "member;;" + this.getSymbolId(parent) + ";" + this.rewriteSymbolName(symbol);
709713
} else {
710-
return "other;" + this.getSymbolDeclarationString(symbol) + ";" + this.getSymbolId(parent) + ";" + symbol.name;
714+
return "other;" + this.getSymbolDeclarationString(symbol) + ";" + this.getSymbolId(parent) + ";" + this.rewriteSymbolName(symbol);
711715
}
712716
}
713717

718+
private rewriteSymbolName(symbol: AugmentedSymbol) {
719+
let { virtualSourceRoot, sourceRoot } = this.virtualSourceRoot;
720+
let { name } = symbol;
721+
if (virtualSourceRoot == null || sourceRoot == null) return name;
722+
return name.replace(virtualSourceRoot, sourceRoot);
723+
}
724+
714725
/**
715726
* Gets a string that distinguishes the given symbol from symbols with different
716727
* lexical roots, or an empty string if the symbol is not a lexical root.

javascript/extractor/lib/typescript/src/virtual_source_root.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,42 @@ import * as ts from "./typescript";
77
*/
88
export class VirtualSourceRoot {
99
constructor(
10-
private sourceRoot: string | null,
10+
public sourceRoot: string | null,
1111

1212
/**
1313
* Directory whose folder structure mirrors the real source root, but with `node_modules` installed,
1414
* or undefined if no virtual source root exists.
1515
*/
16-
private virtualSourceRoot: string | null,
16+
public virtualSourceRoot: string | null,
1717
) {}
1818

19+
private static translate(oldRoot: string, newRoot: string, path: string) {
20+
if (!oldRoot || !newRoot) return null;
21+
let relative = pathlib.relative(oldRoot, path);
22+
if (relative.startsWith('..') || pathlib.isAbsolute(relative)) return null;
23+
return pathlib.join(newRoot, relative);
24+
}
25+
1926
/**
2027
* Maps a path under the real source root to the corresponding path in the virtual source root.
28+
*
29+
* Returns `null` for paths already in the virtual source root.
2130
*/
2231
public toVirtualPath(path: string) {
23-
if (!this.virtualSourceRoot || !this.sourceRoot) return null;
24-
let relative = pathlib.relative(this.sourceRoot, path);
25-
if (relative.startsWith('..') || pathlib.isAbsolute(relative)) return null;
26-
return pathlib.join(this.virtualSourceRoot, relative);
32+
let { virtualSourceRoot } = this;
33+
if (path.startsWith(virtualSourceRoot)) {
34+
// 'qltest' creates a virtual source root inside the real source root.
35+
// Make sure such files don't appear to be inside the real source root.
36+
return null;
37+
}
38+
return VirtualSourceRoot.translate(this.sourceRoot, virtualSourceRoot, path);
39+
}
40+
41+
/**
42+
* Maps a path under the virtual source root to the corresponding path in the real source root.
43+
*/
44+
public fromVirtualPath(path: string) {
45+
return VirtualSourceRoot.translate(this.virtualSourceRoot, this.sourceRoot, path);
2746
}
2847

2948
/**

0 commit comments

Comments
 (0)