Skip to content

Commit 3938856

Browse files
committed
JS: Make this work in qltest
1 parent 1a16d73 commit 3938856

File tree

11 files changed

+126
-20
lines changed

11 files changed

+126
-20
lines changed

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

Lines changed: 1 addition & 1 deletion
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
/**

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,14 +416,21 @@ function loadTsConfig(command: LoadCommand): LoadedConfig {
416416
useCaseSensitiveFileNames: true,
417417
readDirectory: (rootDir, extensions, excludes?, includes?, depth?) => {
418418
// Perform the glob matching in both real and virtual source roots.
419-
let originalResults = ts.sys.readDirectory(rootDir, extensions, excludes, includes, depth)
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)
420426
let virtualDir = virtualSourceRoot.toVirtualPath(rootDir);
421427
if (virtualDir == null) {
422428
return originalResults;
423429
}
424430
// Make sure glob matching does not to discover anything in node_modules.
425-
let virtualExcludes = [ ...(excludes || []), '**/node_modules/**/*' ];
426-
let virtualResults = ts.sys.readDirectory(virtualDir, extensions, virtualExcludes, includes, depth)
431+
let virtualExclusions = excludes == null ? [] : [...excludes];
432+
virtualExclusions.push('**/node_modules/**/*');
433+
let virtualResults = ts.sys.readDirectory(virtualDir, extensions, virtualExclusions, includes, depth)
427434
return [ ...originalResults, ...virtualResults ];
428435
},
429436
fileExists: (path: string) => {

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: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ 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

1919
private static translate(oldRoot: string, newRoot: string, path: string) {
@@ -25,9 +25,17 @@ export class VirtualSourceRoot {
2525

2626
/**
2727
* 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.
2830
*/
2931
public toVirtualPath(path: string) {
30-
return VirtualSourceRoot.translate(this.sourceRoot, this.virtualSourceRoot, path);
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);
3139
}
3240

3341
/**

javascript/extractor/src/com/semmle/js/extractor/Main.java

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.io.File;
44
import java.io.IOException;
5+
import java.nio.file.Path;
6+
import java.nio.file.Paths;
57
import java.util.ArrayList;
68
import java.util.LinkedHashSet;
79
import java.util.List;
@@ -31,6 +33,8 @@
3133
import com.semmle.util.language.LegacyLanguage;
3234
import com.semmle.util.process.ArgsParser;
3335
import com.semmle.util.process.ArgsParser.FileMode;
36+
import com.semmle.util.process.Env;
37+
import com.semmle.util.process.Env.Var;
3438
import com.semmle.util.trap.TrapWriter;
3539

3640
/** The main entry point of the JavaScript extractor. */
@@ -134,12 +138,6 @@ public void run(String[] args) {
134138
return;
135139
}
136140

137-
TypeScriptParser tsParser = extractorState.getTypeScriptParser();
138-
tsParser.setTypescriptRam(extractorConfig.getTypeScriptRam());
139-
if (containsTypeScriptFiles()) {
140-
tsParser.verifyInstallation(!ap.has(P_QUIET));
141-
}
142-
143141
// Sort files for determinism
144142
projectFiles = projectFiles.stream()
145143
.sorted(AutoBuild.FILE_ORDERING)
@@ -149,16 +147,30 @@ public void run(String[] args) {
149147
.sorted(AutoBuild.FILE_ORDERING)
150148
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()));
151149

150+
// Extract HTML files first, as they may contain embedded TypeScript code
151+
for (File file : files) {
152+
if (FileType.forFile(file, extractorConfig) == FileType.HTML) {
153+
ensureFileIsExtracted(file, ap);
154+
}
155+
}
156+
157+
TypeScriptParser tsParser = extractorState.getTypeScriptParser();
158+
tsParser.setTypescriptRam(extractorConfig.getTypeScriptRam());
159+
if (containsTypeScriptFiles()) {
160+
tsParser.verifyInstallation(!ap.has(P_QUIET));
161+
}
162+
152163
for (File projectFile : projectFiles) {
153164

154165
long start = verboseLogStartTimer(ap, "Opening project " + projectFile);
155-
ParsedProject project = tsParser.openProject(projectFile, DependencyInstallationResult.empty, VirtualSourceRoot.none);
166+
ParsedProject project = tsParser.openProject(projectFile, DependencyInstallationResult.empty, extractorConfig.getVirtualSourceRoot());
156167
verboseLogEndTimer(ap, start);
157168
// Extract all files belonging to this project which are also matched
158169
// by our include/exclude filters.
159170
List<File> filesToExtract = new ArrayList<>();
160171
for (File sourceFile : project.getOwnFiles()) {
161-
if (files.contains(normalizeFile(sourceFile))
172+
File normalizedFile = normalizeFile(sourceFile);
173+
if ((files.contains(normalizedFile) || extractorState.getSnippets().containsKey(normalizedFile.toPath()))
162174
&& !extractedFiles.contains(sourceFile.getAbsoluteFile())
163175
&& FileType.TYPESCRIPT.getExtensions().contains(FileUtil.extension(sourceFile))) {
164176
filesToExtract.add(sourceFile);
@@ -287,10 +299,14 @@ private boolean containsTypeScriptFiles() {
287299
}
288300

289301
public void collectFiles(ArgsParser ap) {
290-
for (File f : ap.getOneOrMoreFiles("files", FileMode.FILE_OR_DIRECTORY_MUST_EXIST))
302+
for (File f : getFilesArg(ap))
291303
collectFiles(f, true);
292304
}
293305

306+
private List<File> getFilesArg(ArgsParser ap) {
307+
return ap.getOneOrMoreFiles("files", FileMode.FILE_OR_DIRECTORY_MUST_EXIST);
308+
}
309+
294310
public void setupMatchers(ArgsParser ap) {
295311
Set<String> includes = new LinkedHashSet<>();
296312

@@ -444,6 +460,21 @@ private static TypeScriptMode getTypeScriptMode(ArgsParser ap) {
444460
if (ap.has(P_TYPESCRIPT)) return TypeScriptMode.BASIC;
445461
return TypeScriptMode.NONE;
446462
}
463+
464+
private Path inferSourceRoot(ArgsParser ap) {
465+
List<File> files = getFilesArg(ap);
466+
Path sourceRoot = files.iterator().next().toPath().toAbsolutePath().getParent();
467+
for (File file : files) {
468+
Path path = file.toPath().toAbsolutePath().getParent();
469+
for (int i = 0; i < sourceRoot.getNameCount(); ++i) {
470+
if (!(i < path.getNameCount() && path.getName(i).equals(sourceRoot.getName(i)))) {
471+
sourceRoot = sourceRoot.subpath(0, i);
472+
break;
473+
}
474+
}
475+
}
476+
return sourceRoot;
477+
}
447478

448479
private ExtractorConfig parseJSOptions(ArgsParser ap) {
449480
ExtractorConfig cfg =
@@ -466,6 +497,17 @@ private ExtractorConfig parseJSOptions(ArgsParser ap) {
466497
? UnitParser.parseOpt(ap.getString(P_TYPESCRIPT_RAM), UnitParser.MEGABYTES)
467498
: 0);
468499
if (ap.has(P_DEFAULT_ENCODING)) cfg = cfg.withDefaultEncoding(ap.getString(P_DEFAULT_ENCODING));
500+
501+
// Make a usable virtual source root mapping.
502+
// The concept of source root and scratch directory do not exist in the legacy extractor,
503+
// so we construct these based on what we have.
504+
String odasaDbDir = Env.systemEnv().getNonEmpty(Var.ODASA_DB);
505+
VirtualSourceRoot virtualSourceRoot =
506+
odasaDbDir == null
507+
? VirtualSourceRoot.none
508+
: new VirtualSourceRoot(inferSourceRoot(ap), Paths.get(odasaDbDir, "working"));
509+
cfg = cfg.withVirtualSourceRoot(virtualSourceRoot);
510+
469511
return cfg;
470512
}
471513

javascript/extractor/src/com/semmle/js/extractor/VirtualSourceRoot.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ private static Path translate(Path oldRoot, Path newRoot, Path file) {
4343
}
4444

4545
public Path toVirtualFile(Path file) {
46+
if (file.startsWith(virtualSourceRoot)) {
47+
// 'qltest' creates a virtual source root inside the real source root.
48+
// Make sure such files don't appear to be inside the real source root.
49+
return null;
50+
}
4651
return translate(sourceRoot, virtualSourceRoot, file);
4752
}
4853

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
11
classDeclaration
2+
| test.vue:3:18:5:3 | class M ... er;\\n } |
23
exprType
4+
| other.ts:1:8:1:16 | Component | typeof default in library-tests/TypeScript/EmbeddedInScript/test.vue |
5+
| other.ts:1:23:1:34 | "./test.vue" | any |
6+
| other.ts:3:1:3:15 | new Component() | MyComponent |
7+
| other.ts:3:5:3:13 | Component | typeof default in library-tests/TypeScript/EmbeddedInScript/test.vue |
8+
| other.ts:5:17:5:19 | foo | () => void |
9+
| test.vue:2:15:2:19 | other | typeof library-tests/TypeScript/EmbeddedInScript/other.ts |
10+
| test.vue:2:26:2:34 | "./other" | any |
11+
| test.vue:3:24:3:34 | MyComponent | MyComponent |
12+
| test.vue:4:7:4:7 | x | number |
13+
symbols
14+
| other.ts:1:1:6:0 | <toplevel> | library-tests/TypeScript/EmbeddedInScript/other.ts |
15+
| test.vue:2:3:6:0 | <toplevel> | library-tests/TypeScript/EmbeddedInScript/test.vue |
16+
importTarget
17+
| other.ts:1:1:1:35 | import ... t.vue"; | test.vue:2:3:6:0 | <toplevel> |
18+
| test.vue:2:3:2:35 | import ... other"; | other.ts:1:1:6:0 | <toplevel> |

javascript/ql/test/library-tests/TypeScript/EmbeddedInScript/Test.ql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ import javascript
33
query ClassDefinition classDeclaration() { any() }
44

55
query Type exprType(Expr e) { result = e.getType() }
6+
7+
query predicate symbols(Module mod, CanonicalName name) {
8+
ast_node_symbol(mod, name)
9+
}
10+
11+
query predicate importTarget(Import imprt, Module mod) {
12+
imprt.getImportedModule() = mod
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Component from "./test.vue";
2+
3+
new Component();
4+
5+
export function foo() {};

javascript/ql/test/library-tests/TypeScript/EmbeddedInScript/test.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script lang='ts'>
2+
import * as other from "./other";
23
export default class MyComponent {
34
x!: number;
45
}

0 commit comments

Comments
 (0)