Skip to content

Commit

Permalink
Merge pull request #9266 from asgerf/js/madman-prep
Browse files Browse the repository at this point in the history
JS: Some fixes to support proper analysis of d.ts files
  • Loading branch information
asgerf authored May 31, 2022
2 parents 7f5dcfa + c188aa8 commit f70f769
Show file tree
Hide file tree
Showing 22 changed files with 150 additions and 62 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ go/tools/win64
go/tools/tokenizer.jar
go/main

# node_modules folders except in the JS test suite
node_modules/
!/javascript/ql/test/**/node_modules/
6 changes: 6 additions & 0 deletions javascript/extractor/lib/typescript/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,12 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
if (file.endsWith(".d.ts")) {
return pathlib.basename(file, ".d.ts");
}
if (file.endsWith(".d.mts") || file.endsWith(".d.cts")) {
// We don't extract d.mts or d.cts files, but their symbol can coincide with that of a d.ts file,
// which means any module bindings we generate for it will ultimately be visible in QL.
let base = pathlib.basename(file);
return base.substring(0, base.length - '.d.mts'.length);
}
let base = pathlib.basename(file);
let dot = base.lastIndexOf('.');
return dot === -1 || dot === 0 ? base : base.substring(0, dot);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ public Label visit(Literal nd, Context c) {
trapwriter.addTuple("literals", valueString, source, key);
Position start = nd.getLoc().getStart();
com.semmle.util.locations.Position startPos = new com.semmle.util.locations.Position(start.getLine(), start.getColumn() + 1 /* Convert from 0-based to 1-based. */, start.getOffset());

if (nd.isRegExp()) {
OffsetTranslation offsets = new OffsetTranslation();
offsets.set(0, 1); // skip the initial '/'
Expand Down Expand Up @@ -622,7 +622,7 @@ private boolean isOctalDigit(char ch) {
/**
* Constant-folds simple string concatenations in `exp` while keeping an offset translation
* that tracks back to the original source.
*/
*/
private Pair<String, OffsetTranslation> getStringConcatResult(Expression exp) {
if (exp instanceof BinaryExpression) {
BinaryExpression be = (BinaryExpression) exp;
Expand All @@ -636,7 +636,7 @@ private Pair<String, OffsetTranslation> getStringConcatResult(Expression exp) {
if (str.length() > 1000) {
return null;
}

int delta = be.getRight().getLoc().getStart().getOffset() - be.getLeft().getLoc().getStart().getOffset();
int offset = left.fst().length();
return Pair.make(str, left.snd().append(right.snd(), offset, delta));
Expand Down Expand Up @@ -747,7 +747,7 @@ public Label visit(MemberExpression nd, Context c) {
visit(nd.getProperty(), key, 1, IdContext.TYPE_LABEL);
} else {
IdContext baseIdContext =
c.idcontext == IdContext.EXPORT ? IdContext.EXPORT_BASE : IdContext.VAR_BIND;
(c.idcontext == IdContext.EXPORT || c.idcontext == IdContext.EXPORT_BASE) ? IdContext.EXPORT_BASE : IdContext.VAR_BIND;
visit(nd.getObject(), key, 0, baseIdContext);
visit(nd.getProperty(), key, 1, nd.isComputed() ? IdContext.VAR_BIND : IdContext.LABEL);
}
Expand Down Expand Up @@ -848,14 +848,14 @@ public Label visit(AssignmentExpression nd, Context c) {
public Label visit(BinaryExpression nd, Context c) {
Label key = super.visit(nd, c);
if (nd.getOperator().equals("in") && nd.getLeft() instanceof Identifier && ((Identifier)nd.getLeft()).getName().startsWith("#")) {
// this happens with Ergonomic brand checks for Private Fields (see https://github.com/tc39/proposal-private-fields-in-in).
// this happens with Ergonomic brand checks for Private Fields (see https://github.com/tc39/proposal-private-fields-in-in).
// it's the only case where private field identifiers are used not as a field.
visit(nd.getLeft(), key, 0, IdContext.LABEL, true);
} else {
visit(nd.getLeft(), key, 0, true);
}
visit(nd.getRight(), key, 1, true);

extractRegxpFromBinop(nd, c);
return key;
}
Expand Down Expand Up @@ -1815,7 +1815,7 @@ public Label visit(ImportSpecifier nd, Context c) {
visit(nd.getLocal(), lbl, 1, nd.hasTypeKeyword() ? IdContext.TYPE_ONLY_IMPORT : c.idcontext);
if (nd.hasTypeKeyword()) {
trapwriter.addTuple("has_type_keyword", lbl);
}
}
return lbl;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public JavaScriptHTMLElementHandler(TextualExtractor textualExtractor) {
this.textualExtractor = textualExtractor;

this.scopeManager =
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), true);
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(),
ScopeManager.FileKind.TEMPLATE);
}

/*
Expand Down Expand Up @@ -425,7 +426,7 @@ private void extractTemplateTags(
extractSnippet(
TopLevelKind.ANGULAR_STYLE_TEMPLATE,
config.withSourceType(SourceType.ANGULAR_STYLE_TEMPLATE),
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2020, true),
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2020, ScopeManager.FileKind.TEMPLATE),
textualExtractor,
m.group(bodyGroup),
m.start(bodyGroup),
Expand Down
36 changes: 26 additions & 10 deletions javascript/extractor/src/com/semmle/js/extractor/ScopeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,43 @@ public Label lookupNamespace(String name) {
}
}

public static enum FileKind {
/** Any file not specific to one of the other file kinds. */
PLAIN,

/** A file potentially containing template tags. */
TEMPLATE,

/** A d.ts file, containing TypeScript ambient declarations. */
TYPESCRIPT_DECLARATION,
}

private final TrapWriter trapWriter;
private Scope curScope;
private final Scope toplevelScope;
private final ECMAVersion ecmaVersion;
private final Set<String> implicitGlobals = new LinkedHashSet<String>();
private Scope implicitVariableScope;
private final boolean isInTemplateScope;
private final FileKind fileKind;

public ScopeManager(TrapWriter trapWriter, ECMAVersion ecmaVersion, boolean isInTemplateScope) {
public ScopeManager(TrapWriter trapWriter, ECMAVersion ecmaVersion, FileKind fileKind) {
this.trapWriter = trapWriter;
this.toplevelScope = enterScope(ScopeKind.GLOBAL, trapWriter.globalID("global_scope"), null);
this.ecmaVersion = ecmaVersion;
this.implicitVariableScope = toplevelScope;
this.isInTemplateScope = isInTemplateScope;
this.implicitVariableScope = toplevelScope;
this.fileKind = fileKind;
}

/**
* Returns true the current scope is potentially in a template file, and may contain
* relevant template tags.
*/
public boolean isInTemplateFile() {
return isInTemplateScope;
return this.fileKind == FileKind.TEMPLATE;
}

public boolean isInTypeScriptDeclarationFile() {
return this.fileKind == FileKind.TYPESCRIPT_DECLARATION;
}

/**
Expand Down Expand Up @@ -221,7 +236,7 @@ public Scope getToplevelScope() {

/**
* Get the label for a given variable in the current scope; if it cannot be found, add it to the
* implicit variable scope (usually the global scope).
* implicit variable scope (usually the global scope).
*/
public Label getVarKey(String name) {
Label lbl = curScope.lookupVariable(name);
Expand Down Expand Up @@ -411,15 +426,15 @@ public Void visit(Identifier nd, Void v) {
// cases where we turn on the 'declKind' flags
@Override
public Void visit(FunctionDeclaration nd, Void v) {
if (nd.hasDeclareKeyword()) return null;
if (nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile()) return null;
// strict mode functions are block-scoped, non-strict mode ones aren't
if (blockscope == isStrict) visit(nd.getId(), DeclKind.var);
return null;
}

@Override
public Void visit(ClassDeclaration nd, Void c) {
if (nd.hasDeclareKeyword()) return null;
if (nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile()) return null;
if (blockscope) visit(nd.getClassDef().getId(), DeclKind.varAndType);
return null;
}
Expand Down Expand Up @@ -468,7 +483,7 @@ public Void visit(EnhancedForStatement nd, Void v) {

@Override
public Void visit(VariableDeclaration nd, Void v) {
if (nd.hasDeclareKeyword()) return null;
if (nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile()) return null;
// in block scoping mode, only process 'let'; in non-block scoping
// mode, only process non-'let'
if (blockscope == nd.isBlockScoped(ecmaVersion)) visit(nd.getDeclarations());
Expand Down Expand Up @@ -503,7 +518,8 @@ public Void visit(ClassBody nd, Void c) {
@Override
public Void visit(NamespaceDeclaration nd, Void c) {
if (blockscope) return null;
boolean hasVariable = nd.isInstantiated() && !nd.hasDeclareKeyword();
boolean isAmbientOutsideDtsFile = nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile();
boolean hasVariable = nd.isInstantiated() && !isAmbientOutsideDtsFile;
visit(nd.getName(), hasVariable ? DeclKind.varAndNamespace : DeclKind.namespace);
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public LoCInfo extract(TextualExtractor textualExtractor) {
}

ScopeManager scopeManager =
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), false);
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), ScopeManager.FileKind.PLAIN);
Label toplevelLabel = null;
LoCInfo loc;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ public LoCInfo extract(TextualExtractor textualExtractor) {
String source = textualExtractor.getSource();
File sourceFile = textualExtractor.getExtractedFile();
Result res = state.getTypeScriptParser().parse(sourceFile, source, textualExtractor.getMetrics());
ScopeManager scopeManager =
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017, false);
ScopeManager.FileKind fileKind = sourceFile.getName().endsWith(".d.ts")
? ScopeManager.FileKind.TYPESCRIPT_DECLARATION
: ScopeManager.FileKind.PLAIN;
ScopeManager scopeManager = new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017, fileKind);
try {
FileSnippet snippet = state.getSnippets().get(sourceFile.toPath());
SourceType sourceType = snippet != null ? snippet.getSourceType() : jsExtractor.establishSourceType(source, false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,28 +97,28 @@ scopenodes(#20001,#20033)
scopenesting(#20033,#20000)
is_module(#20001)
is_es2015_module(#20001)
#20034=*
stmts(#20034,30,#20001,0,"export ... foo();")
hasLocation(#20034,#20003)
stmt_containers(#20034,#20001)
#20034=@"var;{foo};{#20033}"
variables(#20034,"foo",#20033)
#20035=*
stmts(#20035,17,#20034,-1,"declare ... foo();")
#20036=@"loc,{#10000},1,8,1,30"
locations_default(#20036,#10000,1,8,1,30)
hasLocation(#20035,#20036)
stmts(#20035,30,#20001,0,"export ... foo();")
hasLocation(#20035,#20003)
stmt_containers(#20035,#20001)
has_declare_keyword(#20035)
#20037=*
exprs(#20037,78,#20035,-1,"foo")
hasLocation(#20037,#20013)
expr_containers(#20037,#20035)
literals("foo","foo",#20037)
#20038=@"var;{foo};{#20000}"
variables(#20038,"foo",#20000)
decl(#20037,#20038)
#20036=*
stmts(#20036,17,#20035,-1,"declare ... foo();")
#20037=@"loc,{#10000},1,8,1,30"
locations_default(#20037,#10000,1,8,1,30)
hasLocation(#20036,#20037)
stmt_containers(#20036,#20001)
has_declare_keyword(#20036)
#20038=*
exprs(#20038,78,#20036,-1,"foo")
hasLocation(#20038,#20013)
expr_containers(#20038,#20036)
literals("foo","foo",#20038)
decl(#20038,#20034)
#20039=*
scopes(#20039,1)
scopenodes(#20035,#20039)
scopenodes(#20036,#20039)
scopenesting(#20039,#20033)
#20040=@"var;{arguments};{#20039}"
variables(#20040,"arguments",#20039)
Expand All @@ -142,8 +142,8 @@ hasLocation(#20043,#20044)
exit_cfg_node(#20045,#20001)
hasLocation(#20045,#20031)
successor(#20041,#20045)
successor(#20034,#20035)
successor(#20035,#20041)
successor(#20043,#20034)
successor(#20035,#20036)
successor(#20036,#20041)
successor(#20043,#20035)
numlines(#10000,2,2,0)
filetype(#10000,"typescript")
3 changes: 2 additions & 1 deletion javascript/extractor/tests/ts/output/trap/namespaces.ts.trap
Original file line number Diff line number Diff line change
Expand Up @@ -305,11 +305,12 @@ hasLocation(#20096,#20097)
enclosing_stmt(#20096,#20092)
expr_containers(#20096,#20001)
#20098=*
exprs(#20098,79,#20096,0,"M")
exprs(#20098,103,#20096,0,"M")
hasLocation(#20098,#20052)
enclosing_stmt(#20098,#20092)
expr_containers(#20098,#20001)
literals("M","M",#20098)
namespacebind(#20098,#20069)
bind(#20098,#20066)
#20099=*
exprs(#20099,0,#20096,1,"N")
Expand Down
15 changes: 14 additions & 1 deletion javascript/ql/lib/semmle/javascript/Files.qll
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@ class Folder extends Container, @folder {
result.getExtension() = extension
}

/** Like `getFile` except `d.ts` is treated as a single extension. */
private File getFileLongExtension(string stem, string extension) {
not (stem.matches("%.d") and extension = "ts") and
result = this.getFile(stem, extension)
or
extension = "d.ts" and
result = this.getFile(stem + ".d", "ts")
}

/**
* Gets the file in this folder that has the given `stem` and any of the supported JavaScript extensions.
*
Expand All @@ -188,7 +197,11 @@ class Folder extends Container, @folder {
*/
File getJavaScriptFile(string stem) {
result =
min(int p, string ext | p = getFileExtensionPriority(ext) | this.getFile(stem, ext) order by p)
min(int p, string ext |
p = getFileExtensionPriority(ext)
|
this.getFileLongExtension(stem, ext) order by p
)
}

/** Gets a subfolder contained in this folder. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ int getFileExtensionPriority(string ext) {
ext = "json" and result = 8
or
ext = "node" and result = 9
or
ext = "d.ts" and result = 10
}

int prioritiesPerCandidate() { result = 3 * (numberOfExtensions() + 1) }
Expand Down
30 changes: 13 additions & 17 deletions javascript/ql/lib/semmle/javascript/TypeScript.qll
Original file line number Diff line number Diff line change
Expand Up @@ -896,21 +896,28 @@ class ArrayTypeExpr extends @array_typeexpr, TypeExpr {
override string getAPrimaryQlClass() { result = "ArrayTypeExpr" }
}

private class RawUnionOrIntersectionTypeExpr = @union_typeexpr or @intersection_typeexpr;

/**
* A union type, such as `string|number|boolean`.
* A union or intersection type, such as `string|number|boolean` or `A & B`.
*/
class UnionTypeExpr extends @union_typeexpr, TypeExpr {
/** Gets the `n`th type in the union, starting at 0. */
class UnionOrIntersectionTypeExpr extends RawUnionOrIntersectionTypeExpr, TypeExpr {
/** Gets the `n`th type in the union or intersection, starting at 0. */
TypeExpr getElementType(int n) { result = this.getChildTypeExpr(n) }

/** Gets any of the types in the union. */
/** Gets any of the types in the union or intersection. */
TypeExpr getAnElementType() { result = this.getElementType(_) }

/** Gets the number of types in the union. This is always at least two. */
/** Gets the number of types in the union or intersection. This is always at least two. */
int getNumElementType() { result = count(this.getAnElementType()) }

override TypeExpr getAnUnderlyingType() { result = this.getAnElementType().getAnUnderlyingType() }
}

/**
* A union type, such as `string|number|boolean`.
*/
class UnionTypeExpr extends @union_typeexpr, UnionOrIntersectionTypeExpr {
override string getAPrimaryQlClass() { result = "UnionTypeExpr" }
}

Expand All @@ -932,18 +939,7 @@ class IndexedAccessTypeExpr extends @indexed_access_typeexpr, TypeExpr {
*
* In general, there are can more than two operands to an intersection type.
*/
class IntersectionTypeExpr extends @intersection_typeexpr, TypeExpr {
/** Gets the `n`th operand of the intersection type, starting at 0. */
TypeExpr getElementType(int n) { result = this.getChildTypeExpr(n) }

/** Gets any of the operands to the intersection type. */
TypeExpr getAnElementType() { result = this.getElementType(_) }

/** Gets the number of operands to the intersection type. This is always at least two. */
int getNumElementType() { result = count(this.getAnElementType()) }

override TypeExpr getAnUnderlyingType() { result = this.getAnElementType().getAnUnderlyingType() }

class IntersectionTypeExpr extends @intersection_typeexpr, UnionOrIntersectionTypeExpr {
override string getAPrimaryQlClass() { result = "IntersectionTypeExpr" }
}

Expand Down
Loading

0 comments on commit f70f769

Please sign in to comment.