Merge pull request #3832 from asger-semmle/js/typescript-in-html-files3

Approved by erik-krogh
This commit is contained in:
semmle-qlci
2020-07-02 08:30:45 +01:00
committed by GitHub
25 changed files with 595 additions and 157 deletions

View File

@@ -29,6 +29,8 @@
* TypeScript 3.9 is now supported. * TypeScript 3.9 is now supported.
* TypeScript code embedded in HTML and Vue files is now extracted and analyzed.
* The analysis of sanitizers has improved, leading to more accurate * The analysis of sanitizers has improved, leading to more accurate
results from the security queries. results from the security queries.

View File

@@ -48,7 +48,7 @@ export class Project {
public load(): void { public load(): void {
const { config, host } = this; const { config, host } = this;
this.program = ts.createProgram(config.fileNames, config.options, host); this.program = ts.createProgram(config.fileNames, config.options, host);
this.typeTable.setProgram(this.program); this.typeTable.setProgram(this.program, this.virtualSourceRoot);
} }
/** /**
@@ -71,10 +71,19 @@ export class Project {
redirectedReference: ts.ResolvedProjectReference, redirectedReference: ts.ResolvedProjectReference,
options: ts.CompilerOptions) { options: ts.CompilerOptions) {
let oppositePath =
this.virtualSourceRoot.toVirtualPath(containingFile) ||
this.virtualSourceRoot.fromVirtualPath(containingFile);
const { host, resolutionCache } = this; const { host, resolutionCache } = this;
return moduleNames.map((moduleName) => { return moduleNames.map((moduleName) => {
let redirected = this.redirectModuleName(moduleName, containingFile, options); let redirected = this.redirectModuleName(moduleName, containingFile, options);
if (redirected != null) return redirected; 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; return ts.resolveModuleName(moduleName, containingFile, options, host, resolutionCache).resolvedModule;
}); });
} }
@@ -90,15 +99,7 @@ export class Project {
// Get the overridden location of this package, if one exists. // Get the overridden location of this package, if one exists.
let packageEntryPoint = this.packageEntryPoints.get(packageName); let packageEntryPoint = this.packageEntryPoints.get(packageName);
if (packageEntryPoint == null) { if (packageEntryPoint == null) return null;
// The package is not overridden, but we have established that it begins with a valid package name.
// Do a lookup in the virtual source root (where dependencies are installed) by changing the 'containing file'.
let virtualContainingFile = this.virtualSourceRoot.toVirtualPath(containingFile);
if (virtualContainingFile != null) {
return ts.resolveModuleName(moduleName, virtualContainingFile, options, this.host, this.resolutionCache).resolvedModule;
}
return null;
}
// If the requested module name is exactly the overridden package name, // If the requested module name is exactly the overridden package name,
// return the entry point file (it is not necessarily called `index.ts`). // return the entry point file (it is not necessarily called `index.ts`).

View File

@@ -414,7 +414,25 @@ function loadTsConfig(command: LoadCommand): LoadedConfig {
*/ */
let parseConfigHost: ts.ParseConfigHost = { let parseConfigHost: ts.ParseConfigHost = {
useCaseSensitiveFileNames: true, useCaseSensitiveFileNames: true,
readDirectory: ts.sys.readDirectory, // No need to override traversal/glob matching readDirectory: (rootDir, extensions, excludes?, includes?, depth?) => {
// Perform the glob matching in both real and virtual source roots.
let exclusions = excludes == null ? [] : [...excludes];
if (virtualSourceRoot.virtualSourceRoot != null) {
// qltest puts the virtual source root inside the real source root (.testproj).
// Make sure we don't find files inside the virtual source root in this pass.
exclusions.push(virtualSourceRoot.virtualSourceRoot);
}
let originalResults = ts.sys.readDirectory(rootDir, extensions, exclusions, includes, depth)
let virtualDir = virtualSourceRoot.toVirtualPath(rootDir);
if (virtualDir == null) {
return originalResults;
}
// Make sure glob matching does not to discover anything in node_modules.
let virtualExclusions = excludes == null ? [] : [...excludes];
virtualExclusions.push('**/node_modules/**/*');
let virtualResults = ts.sys.readDirectory(virtualDir, extensions, virtualExclusions, includes, depth)
return [ ...originalResults, ...virtualResults ];
},
fileExists: (path: string) => { fileExists: (path: string) => {
return ts.sys.fileExists(path) return ts.sys.fileExists(path)
|| virtualSourceRoot.toVirtualPathIfFileExists(path) != null || virtualSourceRoot.toVirtualPathIfFileExists(path) != null

View File

@@ -1,4 +1,5 @@
import * as ts from "./typescript"; import * as ts from "./typescript";
import { VirtualSourceRoot } from "./virtual_source_root";
interface AugmentedSymbol extends ts.Symbol { interface AugmentedSymbol extends ts.Symbol {
parent?: AugmentedSymbol; parent?: AugmentedSymbol;
@@ -379,12 +380,15 @@ export class TypeTable {
*/ */
public restrictedExpansion = false; public restrictedExpansion = false;
private virtualSourceRoot: VirtualSourceRoot;
/** /**
* Called when a new compiler instance has started. * Called when a new compiler instance has started.
*/ */
public setProgram(program: ts.Program) { public setProgram(program: ts.Program, virtualSourceRoot: VirtualSourceRoot) {
this.typeChecker = program.getTypeChecker(); this.typeChecker = program.getTypeChecker();
this.arbitraryAstNode = program.getSourceFiles()[0]; this.arbitraryAstNode = program.getSourceFiles()[0];
this.virtualSourceRoot = virtualSourceRoot;
} }
/** /**
@@ -703,14 +707,21 @@ export class TypeTable {
private getSymbolString(symbol: AugmentedSymbol): string { private getSymbolString(symbol: AugmentedSymbol): string {
let parent = symbol.parent; let parent = symbol.parent;
if (parent == null || parent.escapedName === ts.InternalSymbolName.Global) { if (parent == null || parent.escapedName === ts.InternalSymbolName.Global) {
return "root;" + this.getSymbolDeclarationString(symbol) + ";;" + symbol.name; return "root;" + this.getSymbolDeclarationString(symbol) + ";;" + this.rewriteSymbolName(symbol);
} else if (parent.exports != null && parent.exports.get(symbol.escapedName) === symbol) { } else if (parent.exports != null && parent.exports.get(symbol.escapedName) === symbol) {
return "member;;" + this.getSymbolId(parent) + ";" + symbol.name; return "member;;" + this.getSymbolId(parent) + ";" + this.rewriteSymbolName(symbol);
} else { } else {
return "other;" + this.getSymbolDeclarationString(symbol) + ";" + this.getSymbolId(parent) + ";" + symbol.name; return "other;" + this.getSymbolDeclarationString(symbol) + ";" + this.getSymbolId(parent) + ";" + this.rewriteSymbolName(symbol);
} }
} }
private rewriteSymbolName(symbol: AugmentedSymbol) {
let { virtualSourceRoot, sourceRoot } = this.virtualSourceRoot;
let { name } = symbol;
if (virtualSourceRoot == null || sourceRoot == null) return name;
return name.replace(virtualSourceRoot, sourceRoot);
}
/** /**
* Gets a string that distinguishes the given symbol from symbols with different * Gets a string that distinguishes the given symbol from symbols with different
* lexical roots, or an empty string if the symbol is not a lexical root. * lexical roots, or an empty string if the symbol is not a lexical root.

View File

@@ -7,23 +7,42 @@ import * as ts from "./typescript";
*/ */
export class VirtualSourceRoot { export class VirtualSourceRoot {
constructor( constructor(
private sourceRoot: string | null, public sourceRoot: string | null,
/** /**
* Directory whose folder structure mirrors the real source root, but with `node_modules` installed, * Directory whose folder structure mirrors the real source root, but with `node_modules` installed,
* or undefined if no virtual source root exists. * or undefined if no virtual source root exists.
*/ */
private virtualSourceRoot: string | null, public virtualSourceRoot: string | null,
) {} ) {}
private static translate(oldRoot: string, newRoot: string, path: string) {
if (!oldRoot || !newRoot) return null;
let relative = pathlib.relative(oldRoot, path);
if (relative.startsWith('..') || pathlib.isAbsolute(relative)) return null;
return pathlib.join(newRoot, relative);
}
/** /**
* Maps a path under the real source root to the corresponding path in the virtual source root. * Maps a path under the real source root to the corresponding path in the virtual source root.
*
* Returns `null` for paths already in the virtual source root.
*/ */
public toVirtualPath(path: string) { public toVirtualPath(path: string) {
if (!this.virtualSourceRoot || !this.sourceRoot) return null; let { virtualSourceRoot } = this;
let relative = pathlib.relative(this.sourceRoot, path); if (path.startsWith(virtualSourceRoot)) {
if (relative.startsWith('..') || pathlib.isAbsolute(relative)) return null; // 'qltest' creates a virtual source root inside the real source root.
return pathlib.join(this.virtualSourceRoot, relative); // Make sure such files don't appear to be inside the real source root.
return null;
}
return VirtualSourceRoot.translate(this.sourceRoot, virtualSourceRoot, path);
}
/**
* Maps a path under the virtual source root to the corresponding path in the real source root.
*/
public fromVirtualPath(path: string) {
return VirtualSourceRoot.translate(this.virtualSourceRoot, this.sourceRoot, path);
} }
/** /**

View File

@@ -26,9 +26,11 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -210,6 +212,8 @@ public class AutoBuild {
private volatile boolean seenCode = false; private volatile boolean seenCode = false;
private boolean installDependencies = false; private boolean installDependencies = false;
private int installDependenciesTimeout; private int installDependenciesTimeout;
private final VirtualSourceRoot virtualSourceRoot;
private ExtractorState state;
/** The default timeout when running <code>yarn</code>, in milliseconds. */ /** The default timeout when running <code>yarn</code>, in milliseconds. */
public static final int INSTALL_DEPENDENCIES_DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes public static final int INSTALL_DEPENDENCIES_DEFAULT_TIMEOUT = 10 * 60 * 1000; // 10 minutes
@@ -227,9 +231,15 @@ public class AutoBuild {
Env.systemEnv() Env.systemEnv()
.getInt( .getInt(
"LGTM_INDEX_TYPESCRIPT_INSTALL_DEPS_TIMEOUT", INSTALL_DEPENDENCIES_DEFAULT_TIMEOUT); "LGTM_INDEX_TYPESCRIPT_INSTALL_DEPS_TIMEOUT", INSTALL_DEPENDENCIES_DEFAULT_TIMEOUT);
this.virtualSourceRoot = makeVirtualSourceRoot();
setupFileTypes(); setupFileTypes();
setupXmlMode(); setupXmlMode();
setupMatchers(); setupMatchers();
this.state = new ExtractorState();
}
protected VirtualSourceRoot makeVirtualSourceRoot() {
return new VirtualSourceRoot(LGTM_SRC, toRealPath(Paths.get(EnvironmentVariables.getScratchDir())));
} }
private String getEnvVar(String envVarName) { private String getEnvVar(String envVarName) {
@@ -530,7 +540,7 @@ public class AutoBuild {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException { throws IOException {
if (".js".equals(FileUtil.extension(file.toString()))) extract(extractor, file, null); if (".js".equals(FileUtil.extension(file.toString()))) extract(extractor, file, true);
return super.visitFile(file, attrs); return super.visitFile(file, attrs);
} }
}; };
@@ -568,19 +578,37 @@ public class AutoBuild {
} }
}; };
public class FileExtractors {
FileExtractor defaultExtractor;
Map<String, FileExtractor> customExtractors = new LinkedHashMap<>();
FileExtractors(FileExtractor defaultExtractor) {
this.defaultExtractor = defaultExtractor;
}
public FileExtractor forFile(Path f) {
return customExtractors.getOrDefault(FileUtil.extension(f), defaultExtractor);
}
public FileType fileType(Path f) {
return forFile(f).getFileType(f.toFile());
}
}
/** Extract all supported candidate files that pass the filters. */ /** Extract all supported candidate files that pass the filters. */
private void extractSource() throws IOException { private void extractSource() throws IOException {
// default extractor // default extractor
FileExtractor defaultExtractor = FileExtractor defaultExtractor =
new FileExtractor(mkExtractorConfig(), outputConfig, trapCache); new FileExtractor(mkExtractorConfig(), outputConfig, trapCache);
FileExtractors extractors = new FileExtractors(defaultExtractor);
// custom extractor for explicitly specified file types // custom extractor for explicitly specified file types
Map<String, FileExtractor> customExtractors = new LinkedHashMap<>();
for (Map.Entry<String, FileType> spec : fileTypes.entrySet()) { for (Map.Entry<String, FileType> spec : fileTypes.entrySet()) {
String extension = spec.getKey(); String extension = spec.getKey();
String fileType = spec.getValue().name(); String fileType = spec.getValue().name();
ExtractorConfig extractorConfig = mkExtractorConfig().withFileType(fileType); ExtractorConfig extractorConfig = mkExtractorConfig().withFileType(fileType);
customExtractors.put(extension, new FileExtractor(extractorConfig, outputConfig, trapCache)); extractors.customExtractors.put(extension, new FileExtractor(extractorConfig, outputConfig, trapCache));
} }
Set<Path> filesToExtract = new LinkedHashSet<>(); Set<Path> filesToExtract = new LinkedHashSet<>();
@@ -599,29 +627,44 @@ public class AutoBuild {
if (!tsconfigFiles.isEmpty()) { if (!tsconfigFiles.isEmpty()) {
dependencyInstallationResult = this.preparePackagesAndDependencies(filesToExtract); dependencyInstallationResult = this.preparePackagesAndDependencies(filesToExtract);
} }
Set<Path> extractedFiles = new LinkedHashSet<>();
// Extract HTML files as they may contain TypeScript
CompletableFuture<?> htmlFuture = extractFiles(
filesToExtract, extractedFiles, extractors,
f -> extractors.fileType(f) == FileType.HTML);
htmlFuture.join(); // Wait for HTML extraction to be finished.
// extract TypeScript projects and files // extract TypeScript projects and files
Set<Path> extractedFiles = extractTypeScript(filesToExtract, extractedFiles,
extractTypeScript( extractors, tsconfigFiles, dependencyInstallationResult);
defaultExtractor, filesToExtract, tsconfigFiles, dependencyInstallationResult);
boolean hasTypeScriptFiles = extractedFiles.size() > 0; boolean hasTypeScriptFiles = extractedFiles.size() > 0;
// extract remaining files // extract remaining files
extractFiles(
filesToExtract, extractedFiles, extractors,
f -> !(hasTypeScriptFiles && isFileDerivedFromTypeScriptFile(f, extractedFiles)));
}
private CompletableFuture<?> extractFiles(
Set<Path> filesToExtract,
Set<Path> extractedFiles,
FileExtractors extractors,
Predicate<Path> shouldExtract) {
List<CompletableFuture<?>> futures = new ArrayList<>();
for (Path f : filesToExtract) { for (Path f : filesToExtract) {
if (extractedFiles.contains(f)) if (extractedFiles.contains(f))
continue; continue;
if (hasTypeScriptFiles && isFileDerivedFromTypeScriptFile(f, extractedFiles)) { if (!shouldExtract.test(f)) {
continue; continue;
} }
extractedFiles.add(f); extractedFiles.add(f);
FileExtractor extractor = defaultExtractor; futures.add(extract(extractors.forFile(f), f, true));
if (!fileTypes.isEmpty()) {
String extension = FileUtil.extension(f);
if (customExtractors.containsKey(extension)) extractor = customExtractors.get(extension);
}
extract(extractor, f, null);
} }
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
} }
/** /**
@@ -733,7 +776,6 @@ public class AutoBuild {
*/ */
protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path> filesToExtract) { protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path> filesToExtract) {
final Path sourceRoot = LGTM_SRC; final Path sourceRoot = LGTM_SRC;
final Path virtualSourceRoot = toRealPath(Paths.get(EnvironmentVariables.getScratchDir()));
// Read all package.json files and index them by name. // Read all package.json files and index them by name.
Map<Path, JsonObject> packageJsonFiles = new LinkedHashMap<>(); Map<Path, JsonObject> packageJsonFiles = new LinkedHashMap<>();
@@ -820,8 +862,7 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
// Write the new package.json files to disk // Write the new package.json files to disk
for (Path file : packageJsonFiles.keySet()) { for (Path file : packageJsonFiles.keySet()) {
Path relativePath = sourceRoot.relativize(file); Path virtualFile = virtualSourceRoot.toVirtualFile(file);
Path virtualFile = virtualSourceRoot.resolve(relativePath);
try { try {
Files.createDirectories(virtualFile.getParent()); Files.createDirectories(virtualFile.getParent());
@@ -836,7 +877,7 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
// Install dependencies // Install dependencies
if (this.installDependencies && verifyYarnInstallation()) { if (this.installDependencies && verifyYarnInstallation()) {
for (Path file : packageJsonFiles.keySet()) { for (Path file : packageJsonFiles.keySet()) {
Path virtualFile = virtualSourceRoot.resolve(sourceRoot.relativize(file)); Path virtualFile = virtualSourceRoot.toVirtualFile(file);
System.out.println("Installing dependencies from " + virtualFile); System.out.println("Installing dependencies from " + virtualFile);
ProcessBuilder pb = ProcessBuilder pb =
new ProcessBuilder( new ProcessBuilder(
@@ -862,7 +903,7 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
} }
} }
return new DependencyInstallationResult(sourceRoot, virtualSourceRoot, packageMainFile, packagesInRepo); return new DependencyInstallationResult(packageMainFile, packagesInRepo);
} }
/** /**
@@ -933,21 +974,20 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
ExtractorConfig config = new ExtractorConfig(true); ExtractorConfig config = new ExtractorConfig(true);
config = config.withSourceType(getSourceType()); config = config.withSourceType(getSourceType());
config = config.withTypeScriptMode(typeScriptMode); config = config.withTypeScriptMode(typeScriptMode);
config = config.withVirtualSourceRoot(virtualSourceRoot);
if (defaultEncoding != null) config = config.withDefaultEncoding(defaultEncoding); if (defaultEncoding != null) config = config.withDefaultEncoding(defaultEncoding);
return config; return config;
} }
private Set<Path> extractTypeScript( private Set<Path> extractTypeScript(
FileExtractor extractor,
Set<Path> files, Set<Path> files,
Set<Path> extractedFiles,
FileExtractors extractors,
List<Path> tsconfig, List<Path> tsconfig,
DependencyInstallationResult deps) { DependencyInstallationResult deps) {
Set<Path> extractedFiles = new LinkedHashSet<>();
if (hasTypeScriptFiles(files) || !tsconfig.isEmpty()) { if (hasTypeScriptFiles(files) || !tsconfig.isEmpty()) {
ExtractorState extractorState = new ExtractorState(); TypeScriptParser tsParser = state.getTypeScriptParser();
TypeScriptParser tsParser = extractorState.getTypeScriptParser(); verifyTypeScriptInstallation(state);
verifyTypeScriptInstallation(extractorState);
// Collect all files included in a tsconfig.json inclusion pattern. // Collect all files included in a tsconfig.json inclusion pattern.
// If a given file is referenced by multiple tsconfig files, we prefer to extract it using // If a given file is referenced by multiple tsconfig files, we prefer to extract it using
@@ -955,7 +995,7 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
Set<File> explicitlyIncludedFiles = new LinkedHashSet<>(); Set<File> explicitlyIncludedFiles = new LinkedHashSet<>();
if (tsconfig.size() > 1) { // No prioritization needed if there's only one tsconfig. if (tsconfig.size() > 1) { // No prioritization needed if there's only one tsconfig.
for (Path projectPath : tsconfig) { for (Path projectPath : tsconfig) {
explicitlyIncludedFiles.addAll(tsParser.getOwnFiles(projectPath.toFile(), deps)); explicitlyIncludedFiles.addAll(tsParser.getOwnFiles(projectPath.toFile(), deps, virtualSourceRoot));
} }
} }
@@ -963,16 +1003,19 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
for (Path projectPath : tsconfig) { for (Path projectPath : tsconfig) {
File projectFile = projectPath.toFile(); File projectFile = projectPath.toFile();
long start = logBeginProcess("Opening project " + projectFile); long start = logBeginProcess("Opening project " + projectFile);
ParsedProject project = tsParser.openProject(projectFile, deps); ParsedProject project = tsParser.openProject(projectFile, deps, virtualSourceRoot);
logEndProcess(start, "Done opening project " + projectFile); logEndProcess(start, "Done opening project " + projectFile);
// Extract all files belonging to this project which are also matched // Extract all files belonging to this project which are also matched
// by our include/exclude filters. // by our include/exclude filters.
List<Path> typeScriptFiles = new ArrayList<Path>(); List<Path> typeScriptFiles = new ArrayList<Path>();
for (File sourceFile : project.getAllFiles()) { for (File sourceFile : project.getAllFiles()) {
Path sourcePath = sourceFile.toPath(); Path sourcePath = sourceFile.toPath();
if (!files.contains(normalizePath(sourcePath))) continue; Path normalizedFile = normalizePath(sourcePath);
if (!files.contains(normalizedFile) && !state.getSnippets().containsKey(normalizedFile)) {
continue;
}
if (!project.getOwnFiles().contains(sourceFile) && explicitlyIncludedFiles.contains(sourceFile)) continue; if (!project.getOwnFiles().contains(sourceFile) && explicitlyIncludedFiles.contains(sourceFile)) continue;
if (!FileType.TYPESCRIPT.getExtensions().contains(FileUtil.extension(sourcePath))) { if (extractors.fileType(sourcePath) != FileType.TYPESCRIPT) {
// For the time being, skip non-TypeScript files, even if the TypeScript // For the time being, skip non-TypeScript files, even if the TypeScript
// compiler can parse them for us. // compiler can parse them for us.
continue; continue;
@@ -982,7 +1025,7 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
} }
} }
typeScriptFiles.sort(PATH_ORDERING); typeScriptFiles.sort(PATH_ORDERING);
extractTypeScriptFiles(typeScriptFiles, extractedFiles, extractor, extractorState); extractTypeScriptFiles(typeScriptFiles, extractedFiles, extractors);
tsParser.closeProject(projectFile); tsParser.closeProject(projectFile);
} }
@@ -996,12 +1039,12 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
List<Path> remainingTypeScriptFiles = new ArrayList<>(); List<Path> remainingTypeScriptFiles = new ArrayList<>();
for (Path f : files) { for (Path f : files) {
if (!extractedFiles.contains(f) if (!extractedFiles.contains(f)
&& FileType.forFileExtension(f.toFile()) == FileType.TYPESCRIPT) { && extractors.fileType(f) == FileType.TYPESCRIPT) {
remainingTypeScriptFiles.add(f); remainingTypeScriptFiles.add(f);
} }
} }
if (!remainingTypeScriptFiles.isEmpty()) { if (!remainingTypeScriptFiles.isEmpty()) {
extractTypeScriptFiles(remainingTypeScriptFiles, extractedFiles, extractor, extractorState); extractTypeScriptFiles(remainingTypeScriptFiles, extractedFiles, extractors);
} }
// The TypeScript compiler instance is no longer needed. // The TypeScript compiler instance is no longer needed.
@@ -1087,16 +1130,15 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
public void extractTypeScriptFiles( public void extractTypeScriptFiles(
List<Path> files, List<Path> files,
Set<Path> extractedFiles, Set<Path> extractedFiles,
FileExtractor extractor, FileExtractors extractors) {
ExtractorState extractorState) {
List<File> list = files List<File> list = files
.stream() .stream()
.sorted(PATH_ORDERING) .sorted(PATH_ORDERING)
.map(p -> p.toFile()).collect(Collectors.toList()); .map(p -> p.toFile()).collect(Collectors.toList());
extractorState.getTypeScriptParser().prepareFiles(list); state.getTypeScriptParser().prepareFiles(list);
for (Path path : files) { for (Path path : files) {
extractedFiles.add(path); extractedFiles.add(path);
extract(extractor, path, extractorState); extract(extractors.forFile(path), path, false);
} }
} }
@@ -1139,10 +1181,13 @@ protected DependencyInstallationResult preparePackagesAndDependencies(Set<Path>
* <p>If the state is {@code null}, the extraction job will be submitted to the {@link * <p>If the state is {@code null}, the extraction job will be submitted to the {@link
* #threadPool}, otherwise extraction will happen on the main thread. * #threadPool}, otherwise extraction will happen on the main thread.
*/ */
protected void extract(FileExtractor extractor, Path file, ExtractorState state) { protected CompletableFuture<?> extract(FileExtractor extractor, Path file, boolean concurrent) {
if (state == null && threadPool != null) if (concurrent && threadPool != null) {
threadPool.submit(() -> doExtract(extractor, file, state)); return CompletableFuture.runAsync(() -> doExtract(extractor, file, state), threadPool);
else doExtract(extractor, file, state); } else {
doExtract(extractor, file, state);
return CompletableFuture.completedFuture(null);
}
} }
private void doExtract(FileExtractor extractor, Path file, ExtractorState state) { private void doExtract(FileExtractor extractor, Path file, ExtractorState state) {

View File

@@ -6,46 +6,19 @@ import java.util.Map;
/** Contains the results of installing dependencies. */ /** Contains the results of installing dependencies. */
public class DependencyInstallationResult { public class DependencyInstallationResult {
private Path sourceRoot;
private Path virtualSourceRoot;
private Map<String, Path> packageEntryPoints; private Map<String, Path> packageEntryPoints;
private Map<String, Path> packageJsonFiles; private Map<String, Path> packageJsonFiles;
public static final DependencyInstallationResult empty = public static final DependencyInstallationResult empty =
new DependencyInstallationResult(null, null, Collections.emptyMap(), Collections.emptyMap()); new DependencyInstallationResult(Collections.emptyMap(), Collections.emptyMap());
public DependencyInstallationResult( public DependencyInstallationResult(
Path sourceRoot,
Path virtualSourceRoot,
Map<String, Path> packageEntryPoints, Map<String, Path> packageEntryPoints,
Map<String, Path> packageJsonFiles) { Map<String, Path> packageJsonFiles) {
this.sourceRoot = sourceRoot;
this.virtualSourceRoot = virtualSourceRoot;
this.packageEntryPoints = packageEntryPoints; this.packageEntryPoints = packageEntryPoints;
this.packageJsonFiles = packageJsonFiles; this.packageJsonFiles = packageJsonFiles;
} }
/**
* Returns the source root mirrored by {@link #getVirtualSourceRoot()} or <code>null</code>
* if no virtual source root exists.
* <p>
* When invoked from the AutoBuilder, this corresponds to the source root. When invoked
* from ODASA, there is no notion of source root, so this is always <code>null</code> in that context.
*/
public Path getSourceRoot() {
return sourceRoot;
}
/**
* Returns the virtual source root or <code>null</code> if no virtual source root exists.
* <p>
* The virtual source root is a directory hierarchy that mirrors the real source
* root, where dependencies are installed.
*/
public Path getVirtualSourceRoot() {
return virtualSourceRoot;
}
/** /**
* Returns the mapping from package names to the TypeScript file that should * Returns the mapping from package names to the TypeScript file that should
* act as its main entry point. * act as its main entry point.

View File

@@ -1,8 +1,5 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import com.semmle.js.parser.JcornWrapper;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.UserError;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@@ -12,6 +9,10 @@ import java.util.Collections;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.Set; import java.util.Set;
import com.semmle.js.parser.JcornWrapper;
import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.UserError;
/** /**
* Configuration options that affect the behaviour of the extractor. * Configuration options that affect the behaviour of the extractor.
* *
@@ -236,6 +237,8 @@ public class ExtractorConfig {
/** The default character encoding to use for parsing source files. */ /** The default character encoding to use for parsing source files. */
private String defaultEncoding; private String defaultEncoding;
private VirtualSourceRoot virtualSourceRoot;
public ExtractorConfig(boolean experimental) { public ExtractorConfig(boolean experimental) {
this.ecmaVersion = experimental ? ECMAVersion.ECMA2020 : ECMAVersion.ECMA2019; this.ecmaVersion = experimental ? ECMAVersion.ECMA2020 : ECMAVersion.ECMA2019;
this.platform = Platform.AUTO; this.platform = Platform.AUTO;
@@ -252,6 +255,7 @@ public class ExtractorConfig {
this.typescriptMode = TypeScriptMode.NONE; this.typescriptMode = TypeScriptMode.NONE;
this.e4x = experimental; this.e4x = experimental;
this.defaultEncoding = StandardCharsets.UTF_8.name(); this.defaultEncoding = StandardCharsets.UTF_8.name();
this.virtualSourceRoot = VirtualSourceRoot.none;
} }
public ExtractorConfig(ExtractorConfig that) { public ExtractorConfig(ExtractorConfig that) {
@@ -272,6 +276,7 @@ public class ExtractorConfig {
this.typescriptMode = that.typescriptMode; this.typescriptMode = that.typescriptMode;
this.typescriptRam = that.typescriptRam; this.typescriptRam = that.typescriptRam;
this.defaultEncoding = that.defaultEncoding; this.defaultEncoding = that.defaultEncoding;
this.virtualSourceRoot = that.virtualSourceRoot;
} }
public ECMAVersion getEcmaVersion() { public ECMAVersion getEcmaVersion() {
@@ -452,6 +457,16 @@ public class ExtractorConfig {
return res; return res;
} }
public VirtualSourceRoot getVirtualSourceRoot() {
return virtualSourceRoot;
}
public ExtractorConfig withVirtualSourceRoot(VirtualSourceRoot virtualSourceRoot) {
ExtractorConfig res = new ExtractorConfig(this);
res.virtualSourceRoot = virtualSourceRoot;
return res;
}
@Override @Override
public String toString() { public String toString() {
return "ExtractorConfig [ecmaVersion=" return "ExtractorConfig [ecmaVersion="
@@ -486,6 +501,8 @@ public class ExtractorConfig {
+ typescriptMode + typescriptMode
+ ", defaultEncoding=" + ", defaultEncoding="
+ defaultEncoding + defaultEncoding
+ ", virtualSourceRoot="
+ virtualSourceRoot
+ "]"; + "]";
} }
} }

View File

@@ -1,5 +1,8 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
import com.semmle.js.parser.TypeScriptParser; import com.semmle.js.parser.TypeScriptParser;
/** /**
@@ -17,16 +20,28 @@ import com.semmle.js.parser.TypeScriptParser;
*/ */
public class ExtractorState { public class ExtractorState {
private TypeScriptParser typeScriptParser = new TypeScriptParser(); private TypeScriptParser typeScriptParser = new TypeScriptParser();
private final ConcurrentHashMap<Path, FileSnippet> snippets = new ConcurrentHashMap<>();
public TypeScriptParser getTypeScriptParser() { public TypeScriptParser getTypeScriptParser() {
return typeScriptParser; return typeScriptParser;
} }
/**
* Returns the mapping that denotes where a snippet file originated from.
*
* <p>The map is thread-safe and may be mutated by the caller.
*/
public ConcurrentHashMap<Path, FileSnippet> getSnippets() {
return snippets;
}
/** /**
* Makes this semantically equivalent to a fresh state, but may internally retain shared resources * Makes this semantically equivalent to a fresh state, but may internally retain shared resources
* that are expensive to reacquire. * that are expensive to reacquire.
*/ */
public void reset() { public void reset() {
typeScriptParser.reset(); typeScriptParser.reset();
snippets.clear();
} }
} }

View File

@@ -1,5 +1,17 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
import com.semmle.js.extractor.ExtractionMetrics.ExtractionPhase; import com.semmle.js.extractor.ExtractionMetrics.ExtractionPhase;
import com.semmle.js.extractor.trapcache.CachingTrapWriter; import com.semmle.js.extractor.trapcache.CachingTrapWriter;
import com.semmle.js.extractor.trapcache.ITrapCache; import com.semmle.js.extractor.trapcache.ITrapCache;
@@ -10,16 +22,6 @@ import com.semmle.util.files.FileUtil;
import com.semmle.util.io.WholeIO; import com.semmle.util.io.WholeIO;
import com.semmle.util.trap.TrapWriter; import com.semmle.util.trap.TrapWriter;
import com.semmle.util.trap.TrapWriter.Label; import com.semmle.util.trap.TrapWriter.Label;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
/** /**
* The file extractor extracts a single file and handles source archive population and TRAP caching; * The file extractor extracts a single file and handles source archive population and TRAP caching;
@@ -47,7 +49,7 @@ public class FileExtractor {
HTML(".htm", ".html", ".xhtm", ".xhtml", ".vue") { HTML(".htm", ".html", ".xhtm", ".xhtml", ".vue") {
@Override @Override
public IExtractor mkExtractor(ExtractorConfig config, ExtractorState state) { public IExtractor mkExtractor(ExtractorConfig config, ExtractorState state) {
return new HTMLExtractor(config); return new HTMLExtractor(config, state);
} }
@Override @Override
@@ -293,7 +295,7 @@ public class FileExtractor {
@Override @Override
public IExtractor mkExtractor(ExtractorConfig config, ExtractorState state) { public IExtractor mkExtractor(ExtractorConfig config, ExtractorState state) {
return new TypeScriptExtractor(config, state.getTypeScriptParser()); return new TypeScriptExtractor(config, state);
} }
@Override @Override
@@ -398,6 +400,10 @@ public class FileExtractor {
/** @return the number of lines of code extracted, or {@code null} if the file was cached */ /** @return the number of lines of code extracted, or {@code null} if the file was cached */
public Integer extract(File f, ExtractorState state) throws IOException { public Integer extract(File f, ExtractorState state) throws IOException {
FileSnippet snippet = state.getSnippets().get(f.toPath());
if (snippet != null) {
return this.extractSnippet(f.toPath(), snippet, state);
}
// populate source archive // populate source archive
String source = new WholeIO(config.getDefaultEncoding()).strictread(f); String source = new WholeIO(config.getDefaultEncoding()).strictread(f);
@@ -414,6 +420,25 @@ public class FileExtractor {
return extractContents(f, fileLabel, source, locationManager, state); return extractContents(f, fileLabel, source, locationManager, state);
} }
/**
* Extract the contents of a file that is a snippet from another file.
*
* <p>A trap file will be derived from the snippet file, but its file label, source locations, and
* source archive entry are based on the original file.
*/
private Integer extractSnippet(Path file, FileSnippet origin, ExtractorState state) throws IOException {
TrapWriter trapwriter = outputConfig.getTrapWriterFactory().mkTrapWriter(file.toFile());
File originalFile = origin.getOriginalFile().toFile();
Label fileLabel = trapwriter.populateFile(originalFile);
LocationManager locationManager = new LocationManager(originalFile, trapwriter, fileLabel);
locationManager.setStart(origin.getLine(), origin.getColumn());
String source = new WholeIO(config.getDefaultEncoding()).strictread(file);
return extractContents(file.toFile(), fileLabel, source, locationManager, state);
}
/** /**
* Extract the contents of a file, potentially making use of cached information. * Extract the contents of a file, potentially making use of cached information.
* *
@@ -436,20 +461,20 @@ public class FileExtractor {
* obviously, no caching is done in that scenario. * obviously, no caching is done in that scenario.
*/ */
private Integer extractContents( private Integer extractContents(
File f, Label fileLabel, String source, LocationManager locationManager, ExtractorState state) File extractedFile, Label fileLabel, String source, LocationManager locationManager, ExtractorState state)
throws IOException { throws IOException {
ExtractionMetrics metrics = new ExtractionMetrics(); ExtractionMetrics metrics = new ExtractionMetrics();
metrics.startPhase(ExtractionPhase.FileExtractor_extractContents); metrics.startPhase(ExtractionPhase.FileExtractor_extractContents);
metrics.setLength(source.length()); metrics.setLength(source.length());
metrics.setFileLabel(fileLabel); metrics.setFileLabel(fileLabel);
TrapWriter trapwriter = locationManager.getTrapWriter(); TrapWriter trapwriter = locationManager.getTrapWriter();
FileType fileType = getFileType(f); FileType fileType = getFileType(extractedFile);
File cacheFile = null, // the cache file for this extraction File cacheFile = null, // the cache file for this extraction
resultFile = null; // the final result TRAP file for this extraction resultFile = null; // the final result TRAP file for this extraction
if (bumpIdCounter(trapwriter)) { if (bumpIdCounter(trapwriter)) {
resultFile = outputConfig.getTrapWriterFactory().getTrapFileFor(f); resultFile = outputConfig.getTrapWriterFactory().getTrapFileFor(extractedFile);
} }
// check whether we can perform caching // check whether we can perform caching
if (resultFile != null && fileType.isTrapCachingAllowed()) { if (resultFile != null && fileType.isTrapCachingAllowed()) {
@@ -475,7 +500,7 @@ public class FileExtractor {
trapwriter = new CachingTrapWriter(cacheFile, resultFile); trapwriter = new CachingTrapWriter(cacheFile, resultFile);
bumpIdCounter(trapwriter); bumpIdCounter(trapwriter);
// re-initialise the location manager, since it keeps a reference to the TRAP writer // re-initialise the location manager, since it keeps a reference to the TRAP writer
locationManager = new LocationManager(f, trapwriter, locationManager.getFileLabel()); locationManager = new LocationManager(extractedFile, trapwriter, locationManager.getFileLabel());
} }
// now do the extraction itself // now do the extraction itself
@@ -484,9 +509,9 @@ public class FileExtractor {
IExtractor extractor = fileType.mkExtractor(config, state); IExtractor extractor = fileType.mkExtractor(config, state);
TextualExtractor textualExtractor = TextualExtractor textualExtractor =
new TextualExtractor( new TextualExtractor(
trapwriter, locationManager, source, config.getExtractLines(), metrics); trapwriter, locationManager, source, config.getExtractLines(), metrics, extractedFile);
LoCInfo loc = extractor.extract(textualExtractor); LoCInfo loc = extractor.extract(textualExtractor);
int numLines = textualExtractor.getNumLines(); int numLines = textualExtractor.isSnippet() ? 0 : textualExtractor.getNumLines();
int linesOfCode = loc.getLinesOfCode(), linesOfComments = loc.getLinesOfComments(); int linesOfCode = loc.getLinesOfCode(), linesOfComments = loc.getLinesOfComments();
trapwriter.addTuple("numlines", fileLabel, numLines, linesOfCode, linesOfComments); trapwriter.addTuple("numlines", fileLabel, numLines, linesOfCode, linesOfComments);
trapwriter.addTuple("filetype", fileLabel, fileType.toString()); trapwriter.addTuple("filetype", fileLabel, fileType.toString());

View File

@@ -0,0 +1,44 @@
package com.semmle.js.extractor;
import java.nio.file.Path;
import com.semmle.js.extractor.ExtractorConfig.SourceType;
/**
* Denotes where a code snippet originated from within a file.
*/
public class FileSnippet {
private Path originalFile;
private int line;
private int column;
private int topLevelKind;
private SourceType sourceType;
public FileSnippet(Path originalFile, int line, int column, int topLevelKind, SourceType sourceType) {
this.originalFile = originalFile;
this.line = line;
this.column = column;
this.topLevelKind = topLevelKind;
this.sourceType = sourceType;
}
public Path getOriginalFile() {
return originalFile;
}
public int getLine() {
return line;
}
public int getColumn() {
return column;
}
public int getTopLevelKind() {
return topLevelKind;
}
public SourceType getSourceType() {
return sourceType;
}
}

View File

@@ -1,12 +1,17 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import java.io.File;
import java.nio.file.Path;
import java.util.regex.Pattern;
import com.semmle.js.extractor.ExtractorConfig.Platform; import com.semmle.js.extractor.ExtractorConfig.Platform;
import com.semmle.js.extractor.ExtractorConfig.SourceType; import com.semmle.js.extractor.ExtractorConfig.SourceType;
import com.semmle.js.parser.ParseError; import com.semmle.js.parser.ParseError;
import com.semmle.util.data.StringUtil; import com.semmle.util.data.StringUtil;
import com.semmle.util.io.WholeIO;
import com.semmle.util.trap.TrapWriter; import com.semmle.util.trap.TrapWriter;
import com.semmle.util.trap.TrapWriter.Label; import com.semmle.util.trap.TrapWriter.Label;
import java.util.regex.Pattern;
import net.htmlparser.jericho.Attribute; import net.htmlparser.jericho.Attribute;
import net.htmlparser.jericho.Attributes; import net.htmlparser.jericho.Attributes;
import net.htmlparser.jericho.CharacterReference; import net.htmlparser.jericho.CharacterReference;
@@ -26,9 +31,11 @@ public class HTMLExtractor implements IExtractor {
Pattern.CASE_INSENSITIVE); Pattern.CASE_INSENSITIVE);
private final ExtractorConfig config; private final ExtractorConfig config;
private final ExtractorState state;
public HTMLExtractor(ExtractorConfig config) { public HTMLExtractor(ExtractorConfig config, ExtractorState state) {
this.config = config.withPlatform(Platform.WEB); this.config = config.withPlatform(Platform.WEB);
this.state = state;
} }
@Override @Override
@@ -49,7 +56,7 @@ public class HTMLExtractor implements IExtractor {
for (Element elt : src.getAllElements()) { for (Element elt : src.getAllElements()) {
LoCInfo snippetLoC = null; LoCInfo snippetLoC = null;
if (elt.getName().equals(HTMLElementName.SCRIPT)) { if (elt.getName().equals(HTMLElementName.SCRIPT)) {
SourceType sourceType = getScriptSourceType(elt); SourceType sourceType = getScriptSourceType(elt, textualExtractor.getExtractedFile());
if (sourceType != null) { if (sourceType != null) {
// Jericho sometimes misparses empty elements, which will show up as start tags // Jericho sometimes misparses empty elements, which will show up as start tags
// ending in "/"; we manually exclude these cases to avoid spurious syntax errors // ending in "/"; we manually exclude these cases to avoid spurious syntax errors
@@ -57,6 +64,7 @@ public class HTMLExtractor implements IExtractor {
Segment content = elt.getContent(); Segment content = elt.getContent();
String source = content.toString(); String source = content.toString();
boolean isTypeScript = isTypeScriptTag(elt);
/* /*
* Script blocks in XHTML files may wrap (parts of) their code inside CDATA sections. * Script blocks in XHTML files may wrap (parts of) their code inside CDATA sections.
@@ -79,7 +87,8 @@ public class HTMLExtractor implements IExtractor {
textualExtractor, textualExtractor,
source, source,
contentStart.getRow(), contentStart.getRow(),
contentStart.getColumn()); contentStart.getColumn(),
isTypeScript);
} }
} }
} else { } else {
@@ -101,7 +110,8 @@ public class HTMLExtractor implements IExtractor {
textualExtractor, textualExtractor,
source, source,
valueStart.getRow(), valueStart.getRow(),
valueStart.getColumn()); valueStart.getColumn(),
false /* isTypeScript */);
} else if (source.startsWith("javascript:")) { } else if (source.startsWith("javascript:")) {
source = source.substring(11); source = source.substring(11);
snippetLoC = snippetLoC =
@@ -112,7 +122,8 @@ public class HTMLExtractor implements IExtractor {
textualExtractor, textualExtractor,
source, source,
valueStart.getRow(), valueStart.getRow(),
valueStart.getColumn() + 11); valueStart.getColumn() + 11,
false /* isTypeScript */);
} }
} }
} }
@@ -139,16 +150,23 @@ public class HTMLExtractor implements IExtractor {
* Deduce the {@link SourceType} with which the given <code>script</code> element should be * Deduce the {@link SourceType} with which the given <code>script</code> element should be
* extracted, returning <code>null</code> if it cannot be determined. * extracted, returning <code>null</code> if it cannot be determined.
*/ */
private SourceType getScriptSourceType(Element script) { private SourceType getScriptSourceType(Element script, File file) {
String scriptType = getAttributeValueLC(script, "type"); String scriptType = getAttributeValueLC(script, "type");
String scriptLanguage = getAttributeValueLC(script, "language"); String scriptLanguage = getScriptLanguage(script);
SourceType fallbackSourceType = config.getSourceType();
if (file.getName().endsWith(".vue")) {
fallbackSourceType = SourceType.MODULE;
}
if (isTypeScriptTag(script)) return fallbackSourceType;
// if `type` and `language` are both either missing, contain the // if `type` and `language` are both either missing, contain the
// string "javascript", or if `type` is the string "text/jsx", this is a plain script // string "javascript", or if `type` is the string "text/jsx", this is a plain script
if ((scriptType == null || scriptType.contains("javascript") || "text/jsx".equals(scriptType)) if ((scriptType == null || scriptType.contains("javascript") || "text/jsx".equals(scriptType))
&& (scriptLanguage == null || scriptLanguage.contains("javascript"))) && (scriptLanguage == null || scriptLanguage.contains("javascript")))
// use default source type // use default source type
return config.getSourceType(); return fallbackSourceType;
// if `type` is "text/babel", the source type depends on the `data-plugins` attribute // if `type` is "text/babel", the source type depends on the `data-plugins` attribute
if ("text/babel".equals(scriptType)) { if ("text/babel".equals(scriptType)) {
@@ -156,7 +174,7 @@ public class HTMLExtractor implements IExtractor {
if (plugins != null && plugins.contains("transform-es2015-modules-umd")) { if (plugins != null && plugins.contains("transform-es2015-modules-umd")) {
return SourceType.MODULE; return SourceType.MODULE;
} }
return config.getSourceType(); return fallbackSourceType;
} }
// if `type` is "module", extract as module // if `type` is "module", extract as module
@@ -165,6 +183,23 @@ public class HTMLExtractor implements IExtractor {
return null; return null;
} }
private String getScriptLanguage(Element script) {
String scriptLanguage = getAttributeValueLC(script, "language");
if (scriptLanguage == null) { // Vue templates use 'lang' instead of 'language'.
scriptLanguage = getAttributeValueLC(script, "lang");
}
return scriptLanguage;
}
private boolean isTypeScriptTag(Element script) {
String language = getScriptLanguage(script);
if ("ts".equals(language) || "typescript".equals(language)) return true;
String type = getAttributeValueLC(script, "type");
if (type != null && type.contains("typescript")) return true;
return false;
}
/** /**
* Get the value of attribute <code>attr</code> of element <code>elt</code> in lower case; if the * Get the value of attribute <code>attr</code> of element <code>elt</code> in lower case; if the
* attribute has no value, <code>null</code> is returned. * attribute has no value, <code>null</code> is returned.
@@ -181,7 +216,27 @@ public class HTMLExtractor implements IExtractor {
TextualExtractor textualExtractor, TextualExtractor textualExtractor,
String source, String source,
int line, int line,
int column) { int column,
boolean isTypeScript) {
if (isTypeScript) {
Path file = textualExtractor.getExtractedFile().toPath();
FileSnippet snippet = new FileSnippet(file, line, column, toplevelKind, config.getSourceType());
VirtualSourceRoot vroot = config.getVirtualSourceRoot();
// Vue files are special in that they can be imported as modules, and may only contain one <script> tag.
// For .vue files we omit the usual snippet decoration to ensure the TypeScript compiler can find it.
Path virtualFile =
file.getFileName().toString().endsWith(".vue")
? vroot.toVirtualFile(file.resolveSibling(file.getFileName() + ".ts"))
: vroot.getVirtualFileForSnippet(snippet, ".ts");
if (virtualFile != null) {
virtualFile = virtualFile.toAbsolutePath().normalize();
synchronized(vroot.getLock()) {
new WholeIO().strictwrite(virtualFile, source);
}
state.getSnippets().put(virtualFile, snippet);
}
return null; // LoC info is accounted for later
}
TrapWriter trapwriter = textualExtractor.getTrapwriter(); TrapWriter trapwriter = textualExtractor.getTrapwriter();
LocationManager locationManager = textualExtractor.getLocationManager(); LocationManager locationManager = textualExtractor.getLocationManager();
LocationManager scriptLocationManager = LocationManager scriptLocationManager =
@@ -196,7 +251,8 @@ public class HTMLExtractor implements IExtractor {
scriptLocationManager, scriptLocationManager,
source, source,
config.getExtractLines(), config.getExtractLines(),
textualExtractor.getMetrics()); textualExtractor.getMetrics(),
textualExtractor.getExtractedFile());
return extractor.extract(tx, source, toplevelKind, scopeManager).snd(); return extractor.extract(tx, source, toplevelKind, scopeManager).snd();
} catch (ParseError e) { } catch (ParseError e) {
e.setPosition(scriptLocationManager.translatePosition(e.getPosition())); e.setPosition(scriptLocationManager.translatePosition(e.getPosition()));

View File

@@ -2,6 +2,8 @@ package com.semmle.js.extractor;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
@@ -31,6 +33,8 @@ import com.semmle.util.io.WholeIO;
import com.semmle.util.language.LegacyLanguage; import com.semmle.util.language.LegacyLanguage;
import com.semmle.util.process.ArgsParser; import com.semmle.util.process.ArgsParser;
import com.semmle.util.process.ArgsParser.FileMode; import com.semmle.util.process.ArgsParser.FileMode;
import com.semmle.util.process.Env;
import com.semmle.util.process.Env.Var;
import com.semmle.util.trap.TrapWriter; import com.semmle.util.trap.TrapWriter;
/** The main entry point of the JavaScript extractor. */ /** The main entry point of the JavaScript extractor. */
@@ -134,12 +138,6 @@ public class Main {
return; return;
} }
TypeScriptParser tsParser = extractorState.getTypeScriptParser();
tsParser.setTypescriptRam(extractorConfig.getTypeScriptRam());
if (containsTypeScriptFiles()) {
tsParser.verifyInstallation(!ap.has(P_QUIET));
}
// Sort files for determinism // Sort files for determinism
projectFiles = projectFiles.stream() projectFiles = projectFiles.stream()
.sorted(AutoBuild.FILE_ORDERING) .sorted(AutoBuild.FILE_ORDERING)
@@ -149,16 +147,30 @@ public class Main {
.sorted(AutoBuild.FILE_ORDERING) .sorted(AutoBuild.FILE_ORDERING)
.collect(Collectors.toCollection(() -> new LinkedHashSet<>())); .collect(Collectors.toCollection(() -> new LinkedHashSet<>()));
// Extract HTML files first, as they may contain embedded TypeScript code
for (File file : files) {
if (FileType.forFile(file, extractorConfig) == FileType.HTML) {
ensureFileIsExtracted(file, ap);
}
}
TypeScriptParser tsParser = extractorState.getTypeScriptParser();
tsParser.setTypescriptRam(extractorConfig.getTypeScriptRam());
if (containsTypeScriptFiles()) {
tsParser.verifyInstallation(!ap.has(P_QUIET));
}
for (File projectFile : projectFiles) { for (File projectFile : projectFiles) {
long start = verboseLogStartTimer(ap, "Opening project " + projectFile); long start = verboseLogStartTimer(ap, "Opening project " + projectFile);
ParsedProject project = tsParser.openProject(projectFile, DependencyInstallationResult.empty); ParsedProject project = tsParser.openProject(projectFile, DependencyInstallationResult.empty, extractorConfig.getVirtualSourceRoot());
verboseLogEndTimer(ap, start); verboseLogEndTimer(ap, start);
// Extract all files belonging to this project which are also matched // Extract all files belonging to this project which are also matched
// by our include/exclude filters. // by our include/exclude filters.
List<File> filesToExtract = new ArrayList<>(); List<File> filesToExtract = new ArrayList<>();
for (File sourceFile : project.getOwnFiles()) { for (File sourceFile : project.getOwnFiles()) {
if (files.contains(normalizeFile(sourceFile)) File normalizedFile = normalizeFile(sourceFile);
if ((files.contains(normalizedFile) || extractorState.getSnippets().containsKey(normalizedFile.toPath()))
&& !extractedFiles.contains(sourceFile.getAbsoluteFile()) && !extractedFiles.contains(sourceFile.getAbsoluteFile())
&& FileType.TYPESCRIPT.getExtensions().contains(FileUtil.extension(sourceFile))) { && FileType.TYPESCRIPT.getExtensions().contains(FileUtil.extension(sourceFile))) {
filesToExtract.add(sourceFile); filesToExtract.add(sourceFile);
@@ -287,10 +299,14 @@ public class Main {
} }
public void collectFiles(ArgsParser ap) { public void collectFiles(ArgsParser ap) {
for (File f : ap.getOneOrMoreFiles("files", FileMode.FILE_OR_DIRECTORY_MUST_EXIST)) for (File f : getFilesArg(ap))
collectFiles(f, true); collectFiles(f, true);
} }
private List<File> getFilesArg(ArgsParser ap) {
return ap.getOneOrMoreFiles("files", FileMode.FILE_OR_DIRECTORY_MUST_EXIST);
}
public void setupMatchers(ArgsParser ap) { public void setupMatchers(ArgsParser ap) {
Set<String> includes = new LinkedHashSet<>(); Set<String> includes = new LinkedHashSet<>();
@@ -444,6 +460,21 @@ public class Main {
if (ap.has(P_TYPESCRIPT)) return TypeScriptMode.BASIC; if (ap.has(P_TYPESCRIPT)) return TypeScriptMode.BASIC;
return TypeScriptMode.NONE; return TypeScriptMode.NONE;
} }
private Path inferSourceRoot(ArgsParser ap) {
List<File> files = getFilesArg(ap);
Path sourceRoot = files.iterator().next().toPath().toAbsolutePath().getParent();
for (File file : files) {
Path path = file.toPath().toAbsolutePath().getParent();
for (int i = 0; i < sourceRoot.getNameCount(); ++i) {
if (!(i < path.getNameCount() && path.getName(i).equals(sourceRoot.getName(i)))) {
sourceRoot = sourceRoot.subpath(0, i);
break;
}
}
}
return sourceRoot;
}
private ExtractorConfig parseJSOptions(ArgsParser ap) { private ExtractorConfig parseJSOptions(ArgsParser ap) {
ExtractorConfig cfg = ExtractorConfig cfg =
@@ -466,6 +497,17 @@ public class Main {
? UnitParser.parseOpt(ap.getString(P_TYPESCRIPT_RAM), UnitParser.MEGABYTES) ? UnitParser.parseOpt(ap.getString(P_TYPESCRIPT_RAM), UnitParser.MEGABYTES)
: 0); : 0);
if (ap.has(P_DEFAULT_ENCODING)) cfg = cfg.withDefaultEncoding(ap.getString(P_DEFAULT_ENCODING)); if (ap.has(P_DEFAULT_ENCODING)) cfg = cfg.withDefaultEncoding(ap.getString(P_DEFAULT_ENCODING));
// Make a usable virtual source root mapping.
// The concept of source root and scratch directory do not exist in the legacy extractor,
// so we construct these based on what we have.
String odasaDbDir = Env.systemEnv().getNonEmpty(Var.ODASA_DB);
VirtualSourceRoot virtualSourceRoot =
odasaDbDir == null
? VirtualSourceRoot.none
: new VirtualSourceRoot(inferSourceRoot(ap), Paths.get(odasaDbDir, "working"));
cfg = cfg.withVirtualSourceRoot(virtualSourceRoot);
return cfg; return cfg;
} }

View File

@@ -1,11 +1,13 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.semmle.js.ast.Position; import com.semmle.js.ast.Position;
import com.semmle.js.ast.SourceElement; import com.semmle.js.ast.SourceElement;
import com.semmle.util.trap.TrapWriter; import com.semmle.util.trap.TrapWriter;
import com.semmle.util.trap.TrapWriter.Label; import com.semmle.util.trap.TrapWriter.Label;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* Extractor for populating purely textual information about a file, namely its lines and their line * Extractor for populating purely textual information about a file, namely its lines and their line
@@ -21,19 +23,40 @@ public class TextualExtractor {
private final Label fileLabel; private final Label fileLabel;
private final boolean extractLines; private final boolean extractLines;
private final ExtractionMetrics metrics; private final ExtractionMetrics metrics;
private final File extractedFile;
public TextualExtractor( public TextualExtractor(
TrapWriter trapwriter, TrapWriter trapwriter,
LocationManager locationManager, LocationManager locationManager,
String source, String source,
boolean extractLines, boolean extractLines,
ExtractionMetrics metrics) { ExtractionMetrics metrics,
File extractedFile) {
this.trapwriter = trapwriter; this.trapwriter = trapwriter;
this.locationManager = locationManager; this.locationManager = locationManager;
this.source = source; this.source = source;
this.fileLabel = locationManager.getFileLabel(); this.fileLabel = locationManager.getFileLabel();
this.extractLines = extractLines; this.extractLines = extractLines;
this.metrics = metrics; this.metrics = metrics;
this.extractedFile = extractedFile;
}
/**
* Returns the file whose contents should be extracted, and is contained
* in {@link #source}.
*
* <p>This may differ from the source file of the location manager, which refers
* to the original file that this was derived from.
*/
public File getExtractedFile() {
return extractedFile;
}
/**
* Returns true if the extracted file and the source location files are different.
*/
public boolean isSnippet() {
return !extractedFile.equals(locationManager.getSourceFile());
} }
public TrapWriter getTrapwriter() { public TrapWriter getTrapwriter() {

View File

@@ -1,32 +1,34 @@
package com.semmle.js.extractor; package com.semmle.js.extractor;
import java.io.File;
import com.semmle.js.extractor.ExtractorConfig.ECMAVersion; import com.semmle.js.extractor.ExtractorConfig.ECMAVersion;
import com.semmle.js.extractor.ExtractorConfig.SourceType; import com.semmle.js.extractor.ExtractorConfig.SourceType;
import com.semmle.js.parser.JSParser.Result; import com.semmle.js.parser.JSParser.Result;
import com.semmle.js.parser.ParseError; import com.semmle.js.parser.ParseError;
import com.semmle.js.parser.TypeScriptParser;
import java.io.File;
public class TypeScriptExtractor implements IExtractor { public class TypeScriptExtractor implements IExtractor {
private final JSExtractor jsExtractor; private final JSExtractor jsExtractor;
private final TypeScriptParser parser; private final ExtractorState state;
public TypeScriptExtractor(ExtractorConfig config, TypeScriptParser parser) { public TypeScriptExtractor(ExtractorConfig config, ExtractorState state) {
this.jsExtractor = new JSExtractor(config); this.jsExtractor = new JSExtractor(config);
this.parser = parser; this.state = state;
} }
@Override @Override
public LoCInfo extract(TextualExtractor textualExtractor) { public LoCInfo extract(TextualExtractor textualExtractor) {
LocationManager locationManager = textualExtractor.getLocationManager(); LocationManager locationManager = textualExtractor.getLocationManager();
String source = textualExtractor.getSource(); String source = textualExtractor.getSource();
File sourceFile = locationManager.getSourceFile(); File sourceFile = textualExtractor.getExtractedFile();
Result res = parser.parse(sourceFile, source, textualExtractor.getMetrics()); Result res = state.getTypeScriptParser().parse(sourceFile, source, textualExtractor.getMetrics());
ScopeManager scopeManager = ScopeManager scopeManager =
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017); new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017);
try { try {
SourceType sourceType = jsExtractor.establishSourceType(source, false); FileSnippet snippet = state.getSnippets().get(sourceFile.toPath());
return jsExtractor.extract(textualExtractor, source, 0, scopeManager, sourceType, res).snd(); SourceType sourceType = snippet != null ? snippet.getSourceType() : jsExtractor.establishSourceType(source, false);
int toplevelKind = snippet != null ? snippet.getTopLevelKind() : 0;
return jsExtractor.extract(textualExtractor, source, toplevelKind, scopeManager, sourceType, res).snd();
} catch (ParseError e) { } catch (ParseError e) {
e.setPosition(locationManager.translatePosition(e.getPosition())); e.setPosition(locationManager.translatePosition(e.getPosition()));
throw e.asUserError(); throw e.asUserError();

View File

@@ -0,0 +1,80 @@
package com.semmle.js.extractor;
import java.nio.file.Path;
public class VirtualSourceRoot {
private Path sourceRoot;
private Path virtualSourceRoot;
private Object lock = new Object();
public static final VirtualSourceRoot none = new VirtualSourceRoot(null, null);
public VirtualSourceRoot(Path sourceRoot, Path virtualSourceRoot) {
this.sourceRoot = sourceRoot;
this.virtualSourceRoot = virtualSourceRoot;
}
/**
* Returns the source root mirrored by {@link #getVirtualSourceRoot()} or <code>null</code> if no
* virtual source root exists.
*
* <p>When invoked from the AutoBuilder, this corresponds to the source root. When invoked from
* ODASA, there is no notion of source root, so this is always <code>null</code> in that context.
*/
public Path getSourceRoot() {
return sourceRoot;
}
/**
* Returns the virtual source root or <code>null</code> if no virtual source root exists.
*
* <p>The virtual source root is a directory hierarchy that mirrors the real source root, where
* dependencies are installed.
*/
public Path getVirtualSourceRoot() {
return virtualSourceRoot;
}
private static Path translate(Path oldRoot, Path newRoot, Path file) {
if (oldRoot == null || newRoot == null) return null;
Path relative = oldRoot.relativize(file);
if (relative.startsWith("..") || relative.isAbsolute()) return null;
return newRoot.resolve(relative);
}
public Path toVirtualFile(Path file) {
if (file.startsWith(virtualSourceRoot)) {
// 'qltest' creates a virtual source root inside the real source root.
// Make sure such files don't appear to be inside the real source root.
return null;
}
return translate(sourceRoot, virtualSourceRoot, file);
}
public Path fromVirtualFile(Path file) {
return translate(virtualSourceRoot, sourceRoot, file);
}
public Path getVirtualFileForSnippet(FileSnippet snippet, String extension) {
String basename =
snippet.getOriginalFile().getFileName()
+ ".snippet."
+ snippet.getLine()
+ "."
+ snippet.getColumn()
+ extension;
return toVirtualFile(snippet.getOriginalFile().resolveSibling(basename));
}
@Override
public String toString() {
return "[sourceRoot=" + sourceRoot + ", virtualSourceRoot=" + virtualSourceRoot + "]";
}
/**
* Gets the lock to use when writing to the virtual source root in a multi-threaded context.
*/
public Object getLock() {
return lock;
}
}

View File

@@ -15,6 +15,7 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
@@ -27,6 +28,7 @@ import com.semmle.js.extractor.DependencyInstallationResult;
import com.semmle.js.extractor.ExtractorState; import com.semmle.js.extractor.ExtractorState;
import com.semmle.js.extractor.FileExtractor; import com.semmle.js.extractor.FileExtractor;
import com.semmle.js.extractor.FileExtractor.FileType; import com.semmle.js.extractor.FileExtractor.FileType;
import com.semmle.js.extractor.VirtualSourceRoot;
import com.semmle.util.data.StringUtil; import com.semmle.util.data.StringUtil;
import com.semmle.util.exception.UserError; import com.semmle.util.exception.UserError;
import com.semmle.util.files.FileUtil; import com.semmle.util.files.FileUtil;
@@ -109,11 +111,12 @@ public class AutoBuildTests {
Set<String> actual = new LinkedHashSet<>(); Set<String> actual = new LinkedHashSet<>();
new AutoBuild() { new AutoBuild() {
@Override @Override
protected void extract(FileExtractor extractor, Path file, ExtractorState state) { protected CompletableFuture<?> extract(FileExtractor extractor, Path file, boolean concurrent) {
String extracted = file.toString(); String extracted = file.toString();
if (extractor.getConfig().hasFileType()) if (extractor.getConfig().hasFileType())
extracted += ":" + extractor.getFileType(file.toFile()); extracted += ":" + extractor.getFileType(file.toFile());
actual.add(extracted); actual.add(extracted);
return CompletableFuture.completedFuture(null);
} }
@Override @Override
@@ -123,8 +126,7 @@ public class AutoBuildTests {
public void extractTypeScriptFiles( public void extractTypeScriptFiles(
java.util.List<Path> files, java.util.List<Path> files,
java.util.Set<Path> extractedFiles, java.util.Set<Path> extractedFiles,
FileExtractor extractor, FileExtractors extractors) {
ExtractorState extractorState) {
for (Path f : files) { for (Path f : files) {
actual.add(f.toString()); actual.add(f.toString());
} }
@@ -136,6 +138,11 @@ public class AutoBuildTests {
return DependencyInstallationResult.empty; return DependencyInstallationResult.empty;
} }
@Override
protected VirtualSourceRoot makeVirtualSourceRoot() {
return VirtualSourceRoot.none; // not used in these tests
}
@Override @Override
protected void extractXml() throws IOException { protected void extractXml() throws IOException {
Files.walkFileTree( Files.walkFileTree(

View File

@@ -31,6 +31,7 @@ import com.google.gson.JsonPrimitive;
import com.semmle.js.extractor.DependencyInstallationResult; import com.semmle.js.extractor.DependencyInstallationResult;
import com.semmle.js.extractor.EnvironmentVariables; import com.semmle.js.extractor.EnvironmentVariables;
import com.semmle.js.extractor.ExtractionMetrics; import com.semmle.js.extractor.ExtractionMetrics;
import com.semmle.js.extractor.VirtualSourceRoot;
import com.semmle.js.parser.JSParser.Result; import com.semmle.js.parser.JSParser.Result;
import com.semmle.ts.extractor.TypeTable; import com.semmle.ts.extractor.TypeTable;
import com.semmle.util.data.StringUtil; import com.semmle.util.data.StringUtil;
@@ -501,8 +502,8 @@ public class TypeScriptParser {
/** /**
* Returns the set of files included by the inclusion pattern in the given tsconfig.json file. * Returns the set of files included by the inclusion pattern in the given tsconfig.json file.
*/ */
public Set<File> getOwnFiles(File tsConfigFile, DependencyInstallationResult deps) { public Set<File> getOwnFiles(File tsConfigFile, DependencyInstallationResult deps, VirtualSourceRoot vroot) {
JsonObject request = makeLoadCommand("get-own-files", tsConfigFile, deps); JsonObject request = makeLoadCommand("get-own-files", tsConfigFile, deps, vroot);
JsonObject response = talkToParserWrapper(request); JsonObject response = talkToParserWrapper(request);
try { try {
checkResponseType(response, "file-list"); checkResponseType(response, "file-list");
@@ -521,8 +522,8 @@ public class TypeScriptParser {
* *
* <p>Only one project should be opened at once. * <p>Only one project should be opened at once.
*/ */
public ParsedProject openProject(File tsConfigFile, DependencyInstallationResult deps) { public ParsedProject openProject(File tsConfigFile, DependencyInstallationResult deps, VirtualSourceRoot vroot) {
JsonObject request = makeLoadCommand("open-project", tsConfigFile, deps); JsonObject request = makeLoadCommand("open-project", tsConfigFile, deps, vroot);
JsonObject response = talkToParserWrapper(request); JsonObject response = talkToParserWrapper(request);
try { try {
checkResponseType(response, "project-opened"); checkResponseType(response, "project-opened");
@@ -536,18 +537,18 @@ public class TypeScriptParser {
} }
} }
private JsonObject makeLoadCommand(String command, File tsConfigFile, DependencyInstallationResult deps) { private JsonObject makeLoadCommand(String command, File tsConfigFile, DependencyInstallationResult deps, VirtualSourceRoot vroot) {
JsonObject request = new JsonObject(); JsonObject request = new JsonObject();
request.add("command", new JsonPrimitive(command)); request.add("command", new JsonPrimitive(command));
request.add("tsConfig", new JsonPrimitive(tsConfigFile.getPath())); request.add("tsConfig", new JsonPrimitive(tsConfigFile.getPath()));
request.add("packageEntryPoints", mapToArray(deps.getPackageEntryPoints())); request.add("packageEntryPoints", mapToArray(deps.getPackageEntryPoints()));
request.add("packageJsonFiles", mapToArray(deps.getPackageJsonFiles())); request.add("packageJsonFiles", mapToArray(deps.getPackageJsonFiles()));
request.add("sourceRoot", deps.getSourceRoot() == null request.add("sourceRoot", vroot.getSourceRoot() == null
? JsonNull.INSTANCE ? JsonNull.INSTANCE
: new JsonPrimitive(deps.getSourceRoot().toString())); : new JsonPrimitive(vroot.getSourceRoot().toString()));
request.add("virtualSourceRoot", deps.getVirtualSourceRoot() == null request.add("virtualSourceRoot", vroot.getVirtualSourceRoot() == null
? JsonNull.INSTANCE ? JsonNull.INSTANCE
: new JsonPrimitive(deps.getVirtualSourceRoot().toString())); : new JsonPrimitive(vroot.getVirtualSourceRoot().toString()));
return request; return request;
} }

View File

@@ -206,13 +206,13 @@ class File extends Container, @file {
override string getAbsolutePath() { files(this, result, _, _, _) } override string getAbsolutePath() { files(this, result, _, _, _) }
/** Gets the number of lines in this file. */ /** Gets the number of lines in this file. */
int getNumberOfLines() { numlines(this, result, _, _) } int getNumberOfLines() { result = sum(int loc | numlines(this, loc, _, _) | loc) }
/** Gets the number of lines containing code in this file. */ /** Gets the number of lines containing code in this file. */
int getNumberOfLinesOfCode() { numlines(this, _, result, _) } int getNumberOfLinesOfCode() { result = sum(int loc | numlines(this, _, loc, _) | loc) }
/** Gets the number of lines containing comments in this file. */ /** Gets the number of lines containing comments in this file. */
int getNumberOfLinesOfComments() { numlines(this, _, _, result) } int getNumberOfLinesOfComments() { result = sum(int loc | numlines(this, _, _, loc) | loc) }
/** Gets a toplevel piece of JavaScript code in this file. */ /** Gets a toplevel piece of JavaScript code in this file. */
TopLevel getATopLevel() { result.getFile() = this } TopLevel getATopLevel() { result.getFile() = this }

View File

@@ -0,0 +1,26 @@
classDeclaration
| test.vue:3:18:5:3 | class M ... er;\\n } |
exprType
| htmlfile.html:4:22:4:24 | foo | () => void |
| htmlfile.html:4:22:4:24 | foo | () => void |
| htmlfile.html:4:33:4:41 | "./other" | any |
| htmlfile.html:5:17:5:22 | result | number[] |
| htmlfile.html:5:26:5:28 | foo | () => void |
| htmlfile.html:5:26:5:30 | foo() | void |
| htmlfile.html:5:26:5:42 | foo() as number[] | number[] |
| other.ts:1:8:1:16 | Component | typeof default in library-tests/TypeScript/EmbeddedInScript/test.vue |
| other.ts:1:23:1:34 | "./test.vue" | any |
| other.ts:3:1:3:15 | new Component() | MyComponent |
| other.ts:3:5:3:13 | Component | typeof default in library-tests/TypeScript/EmbeddedInScript/test.vue |
| other.ts:5:17:5:19 | foo | () => void |
| test.vue:2:15:2:19 | other | typeof library-tests/TypeScript/EmbeddedInScript/other.ts |
| test.vue:2:26:2:34 | "./other" | any |
| test.vue:3:24:3:34 | MyComponent | MyComponent |
| test.vue:4:7:4:7 | x | number |
symbols
| other.ts:1:1:6:0 | <toplevel> | library-tests/TypeScript/EmbeddedInScript/other.ts |
| test.vue:2:3:6:0 | <toplevel> | library-tests/TypeScript/EmbeddedInScript/test.vue |
importTarget
| htmlfile.html:4:13:4:42 | import ... other"; | other.ts:1:1:6:0 | <toplevel> |
| other.ts:1:1:1:35 | import ... t.vue"; | test.vue:2:3:6:0 | <toplevel> |
| test.vue:2:3:2:35 | import ... other"; | other.ts:1:1:6:0 | <toplevel> |

View File

@@ -0,0 +1,9 @@
import javascript
query ClassDefinition classDeclaration() { any() }
query Type exprType(Expr e) { result = e.getType() }
query predicate symbols(Module mod, CanonicalName name) { ast_node_symbol(mod, name) }
query predicate importTarget(Import imprt, Module mod) { imprt.getImportedModule() = mod }

View File

@@ -0,0 +1,8 @@
<html>
<body>
<script type="module" language="typescript">
import { foo } from "./other";
let result = foo() as number[];
</script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
import Component from "./test.vue";
new Component();
export function foo() {};

View File

@@ -0,0 +1,6 @@
<script lang='ts'>
import * as other from "./other";
export default class MyComponent {
x!: number;
}
</script>

View File

@@ -0,0 +1,3 @@
{
"include": ["."]
}