Skip to content

Commit f70f769

Browse files
authored
Merge pull request github#9266 from asgerf/js/madman-prep
JS: Some fixes to support proper analysis of d.ts files
2 parents 7f5dcfa + c188aa8 commit f70f769

File tree

22 files changed

+150
-62
lines changed

22 files changed

+150
-62
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ go/tools/win64
5555
go/tools/tokenizer.jar
5656
go/main
5757

58+
# node_modules folders except in the JS test suite
59+
node_modules/
60+
!/javascript/ql/test/**/node_modules/

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,12 @@ function handleOpenProjectCommand(command: OpenProjectCommand) {
670670
if (file.endsWith(".d.ts")) {
671671
return pathlib.basename(file, ".d.ts");
672672
}
673+
if (file.endsWith(".d.mts") || file.endsWith(".d.cts")) {
674+
// We don't extract d.mts or d.cts files, but their symbol can coincide with that of a d.ts file,
675+
// which means any module bindings we generate for it will ultimately be visible in QL.
676+
let base = pathlib.basename(file);
677+
return base.substring(0, base.length - '.d.mts'.length);
678+
}
673679
let base = pathlib.basename(file);
674680
let dot = base.lastIndexOf('.');
675681
return dot === -1 || dot === 0 ? base : base.substring(0, dot);

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,7 @@ public Label visit(Literal nd, Context c) {
590590
trapwriter.addTuple("literals", valueString, source, key);
591591
Position start = nd.getLoc().getStart();
592592
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());
593-
593+
594594
if (nd.isRegExp()) {
595595
OffsetTranslation offsets = new OffsetTranslation();
596596
offsets.set(0, 1); // skip the initial '/'
@@ -622,7 +622,7 @@ private boolean isOctalDigit(char ch) {
622622
/**
623623
* Constant-folds simple string concatenations in `exp` while keeping an offset translation
624624
* that tracks back to the original source.
625-
*/
625+
*/
626626
private Pair<String, OffsetTranslation> getStringConcatResult(Expression exp) {
627627
if (exp instanceof BinaryExpression) {
628628
BinaryExpression be = (BinaryExpression) exp;
@@ -636,7 +636,7 @@ private Pair<String, OffsetTranslation> getStringConcatResult(Expression exp) {
636636
if (str.length() > 1000) {
637637
return null;
638638
}
639-
639+
640640
int delta = be.getRight().getLoc().getStart().getOffset() - be.getLeft().getLoc().getStart().getOffset();
641641
int offset = left.fst().length();
642642
return Pair.make(str, left.snd().append(right.snd(), offset, delta));
@@ -747,7 +747,7 @@ public Label visit(MemberExpression nd, Context c) {
747747
visit(nd.getProperty(), key, 1, IdContext.TYPE_LABEL);
748748
} else {
749749
IdContext baseIdContext =
750-
c.idcontext == IdContext.EXPORT ? IdContext.EXPORT_BASE : IdContext.VAR_BIND;
750+
(c.idcontext == IdContext.EXPORT || c.idcontext == IdContext.EXPORT_BASE) ? IdContext.EXPORT_BASE : IdContext.VAR_BIND;
751751
visit(nd.getObject(), key, 0, baseIdContext);
752752
visit(nd.getProperty(), key, 1, nd.isComputed() ? IdContext.VAR_BIND : IdContext.LABEL);
753753
}
@@ -848,14 +848,14 @@ public Label visit(AssignmentExpression nd, Context c) {
848848
public Label visit(BinaryExpression nd, Context c) {
849849
Label key = super.visit(nd, c);
850850
if (nd.getOperator().equals("in") && nd.getLeft() instanceof Identifier && ((Identifier)nd.getLeft()).getName().startsWith("#")) {
851-
// this happens with Ergonomic brand checks for Private Fields (see https://github.com/tc39/proposal-private-fields-in-in).
851+
// this happens with Ergonomic brand checks for Private Fields (see https://github.com/tc39/proposal-private-fields-in-in).
852852
// it's the only case where private field identifiers are used not as a field.
853853
visit(nd.getLeft(), key, 0, IdContext.LABEL, true);
854854
} else {
855855
visit(nd.getLeft(), key, 0, true);
856856
}
857857
visit(nd.getRight(), key, 1, true);
858-
858+
859859
extractRegxpFromBinop(nd, c);
860860
return key;
861861
}
@@ -1815,7 +1815,7 @@ public Label visit(ImportSpecifier nd, Context c) {
18151815
visit(nd.getLocal(), lbl, 1, nd.hasTypeKeyword() ? IdContext.TYPE_ONLY_IMPORT : c.idcontext);
18161816
if (nd.hasTypeKeyword()) {
18171817
trapwriter.addTuple("has_type_keyword", lbl);
1818-
}
1818+
}
18191819
return lbl;
18201820
}
18211821

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public JavaScriptHTMLElementHandler(TextualExtractor textualExtractor) {
4040
this.textualExtractor = textualExtractor;
4141

4242
this.scopeManager =
43-
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), true);
43+
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(),
44+
ScopeManager.FileKind.TEMPLATE);
4445
}
4546

4647
/*
@@ -425,7 +426,7 @@ private void extractTemplateTags(
425426
extractSnippet(
426427
TopLevelKind.ANGULAR_STYLE_TEMPLATE,
427428
config.withSourceType(SourceType.ANGULAR_STYLE_TEMPLATE),
428-
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2020, true),
429+
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2020, ScopeManager.FileKind.TEMPLATE),
429430
textualExtractor,
430431
m.group(bodyGroup),
431432
m.start(bodyGroup),

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,43 @@ public Label lookupNamespace(String name) {
9797
}
9898
}
9999

100+
public static enum FileKind {
101+
/** Any file not specific to one of the other file kinds. */
102+
PLAIN,
103+
104+
/** A file potentially containing template tags. */
105+
TEMPLATE,
106+
107+
/** A d.ts file, containing TypeScript ambient declarations. */
108+
TYPESCRIPT_DECLARATION,
109+
}
110+
100111
private final TrapWriter trapWriter;
101112
private Scope curScope;
102113
private final Scope toplevelScope;
103114
private final ECMAVersion ecmaVersion;
104115
private final Set<String> implicitGlobals = new LinkedHashSet<String>();
105116
private Scope implicitVariableScope;
106-
private final boolean isInTemplateScope;
117+
private final FileKind fileKind;
107118

108-
public ScopeManager(TrapWriter trapWriter, ECMAVersion ecmaVersion, boolean isInTemplateScope) {
119+
public ScopeManager(TrapWriter trapWriter, ECMAVersion ecmaVersion, FileKind fileKind) {
109120
this.trapWriter = trapWriter;
110121
this.toplevelScope = enterScope(ScopeKind.GLOBAL, trapWriter.globalID("global_scope"), null);
111122
this.ecmaVersion = ecmaVersion;
112-
this.implicitVariableScope = toplevelScope;
113-
this.isInTemplateScope = isInTemplateScope;
123+
this.implicitVariableScope = toplevelScope;
124+
this.fileKind = fileKind;
114125
}
115126

116127
/**
117128
* Returns true the current scope is potentially in a template file, and may contain
118129
* relevant template tags.
119130
*/
120131
public boolean isInTemplateFile() {
121-
return isInTemplateScope;
132+
return this.fileKind == FileKind.TEMPLATE;
133+
}
134+
135+
public boolean isInTypeScriptDeclarationFile() {
136+
return this.fileKind == FileKind.TYPESCRIPT_DECLARATION;
122137
}
123138

124139
/**
@@ -221,7 +236,7 @@ public Scope getToplevelScope() {
221236

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

420435
@Override
421436
public Void visit(ClassDeclaration nd, Void c) {
422-
if (nd.hasDeclareKeyword()) return null;
437+
if (nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile()) return null;
423438
if (blockscope) visit(nd.getClassDef().getId(), DeclKind.varAndType);
424439
return null;
425440
}
@@ -468,7 +483,7 @@ public Void visit(EnhancedForStatement nd, Void v) {
468483

469484
@Override
470485
public Void visit(VariableDeclaration nd, Void v) {
471-
if (nd.hasDeclareKeyword()) return null;
486+
if (nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile()) return null;
472487
// in block scoping mode, only process 'let'; in non-block scoping
473488
// mode, only process non-'let'
474489
if (blockscope == nd.isBlockScoped(ecmaVersion)) visit(nd.getDeclarations());
@@ -503,7 +518,8 @@ public Void visit(ClassBody nd, Void c) {
503518
@Override
504519
public Void visit(NamespaceDeclaration nd, Void c) {
505520
if (blockscope) return null;
506-
boolean hasVariable = nd.isInstantiated() && !nd.hasDeclareKeyword();
521+
boolean isAmbientOutsideDtsFile = nd.hasDeclareKeyword() && !isInTypeScriptDeclarationFile();
522+
boolean hasVariable = nd.isInstantiated() && !isAmbientOutsideDtsFile;
507523
visit(nd.getName(), hasVariable ? DeclKind.varAndNamespace : DeclKind.namespace);
508524
return null;
509525
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public LoCInfo extract(TextualExtractor textualExtractor) {
7777
}
7878

7979
ScopeManager scopeManager =
80-
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), false);
80+
new ScopeManager(textualExtractor.getTrapwriter(), config.getEcmaVersion(), ScopeManager.FileKind.PLAIN);
8181
Label toplevelLabel = null;
8282
LoCInfo loc;
8383
try {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ public LoCInfo extract(TextualExtractor textualExtractor) {
2222
String source = textualExtractor.getSource();
2323
File sourceFile = textualExtractor.getExtractedFile();
2424
Result res = state.getTypeScriptParser().parse(sourceFile, source, textualExtractor.getMetrics());
25-
ScopeManager scopeManager =
26-
new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017, false);
25+
ScopeManager.FileKind fileKind = sourceFile.getName().endsWith(".d.ts")
26+
? ScopeManager.FileKind.TYPESCRIPT_DECLARATION
27+
: ScopeManager.FileKind.PLAIN;
28+
ScopeManager scopeManager = new ScopeManager(textualExtractor.getTrapwriter(), ECMAVersion.ECMA2017, fileKind);
2729
try {
2830
FileSnippet snippet = state.getSnippets().get(sourceFile.toPath());
2931
SourceType sourceType = snippet != null ? snippet.getSourceType() : jsExtractor.establishSourceType(source, false);

javascript/extractor/tests/ts/output/trap/exportasnamespace.d.ts.trap

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,28 @@ scopenodes(#20001,#20033)
9797
scopenesting(#20033,#20000)
9898
is_module(#20001)
9999
is_es2015_module(#20001)
100-
#20034=*
101-
stmts(#20034,30,#20001,0,"export ... foo();")
102-
hasLocation(#20034,#20003)
103-
stmt_containers(#20034,#20001)
100+
#20034=@"var;{foo};{#20033}"
101+
variables(#20034,"foo",#20033)
104102
#20035=*
105-
stmts(#20035,17,#20034,-1,"declare ... foo();")
106-
#20036=@"loc,{#10000},1,8,1,30"
107-
locations_default(#20036,#10000,1,8,1,30)
108-
hasLocation(#20035,#20036)
103+
stmts(#20035,30,#20001,0,"export ... foo();")
104+
hasLocation(#20035,#20003)
109105
stmt_containers(#20035,#20001)
110-
has_declare_keyword(#20035)
111-
#20037=*
112-
exprs(#20037,78,#20035,-1,"foo")
113-
hasLocation(#20037,#20013)
114-
expr_containers(#20037,#20035)
115-
literals("foo","foo",#20037)
116-
#20038=@"var;{foo};{#20000}"
117-
variables(#20038,"foo",#20000)
118-
decl(#20037,#20038)
106+
#20036=*
107+
stmts(#20036,17,#20035,-1,"declare ... foo();")
108+
#20037=@"loc,{#10000},1,8,1,30"
109+
locations_default(#20037,#10000,1,8,1,30)
110+
hasLocation(#20036,#20037)
111+
stmt_containers(#20036,#20001)
112+
has_declare_keyword(#20036)
113+
#20038=*
114+
exprs(#20038,78,#20036,-1,"foo")
115+
hasLocation(#20038,#20013)
116+
expr_containers(#20038,#20036)
117+
literals("foo","foo",#20038)
118+
decl(#20038,#20034)
119119
#20039=*
120120
scopes(#20039,1)
121-
scopenodes(#20035,#20039)
121+
scopenodes(#20036,#20039)
122122
scopenesting(#20039,#20033)
123123
#20040=@"var;{arguments};{#20039}"
124124
variables(#20040,"arguments",#20039)
@@ -142,8 +142,8 @@ hasLocation(#20043,#20044)
142142
exit_cfg_node(#20045,#20001)
143143
hasLocation(#20045,#20031)
144144
successor(#20041,#20045)
145-
successor(#20034,#20035)
146-
successor(#20035,#20041)
147-
successor(#20043,#20034)
145+
successor(#20035,#20036)
146+
successor(#20036,#20041)
147+
successor(#20043,#20035)
148148
numlines(#10000,2,2,0)
149149
filetype(#10000,"typescript")

javascript/extractor/tests/ts/output/trap/namespaces.ts.trap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,12 @@ hasLocation(#20096,#20097)
305305
enclosing_stmt(#20096,#20092)
306306
expr_containers(#20096,#20001)
307307
#20098=*
308-
exprs(#20098,79,#20096,0,"M")
308+
exprs(#20098,103,#20096,0,"M")
309309
hasLocation(#20098,#20052)
310310
enclosing_stmt(#20098,#20092)
311311
expr_containers(#20098,#20001)
312312
literals("M","M",#20098)
313+
namespacebind(#20098,#20069)
313314
bind(#20098,#20066)
314315
#20099=*
315316
exprs(#20099,0,#20096,1,"N")

javascript/ql/lib/semmle/javascript/Files.qll

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ class Folder extends Container, @folder {
175175
result.getExtension() = extension
176176
}
177177

178+
/** Like `getFile` except `d.ts` is treated as a single extension. */
179+
private File getFileLongExtension(string stem, string extension) {
180+
not (stem.matches("%.d") and extension = "ts") and
181+
result = this.getFile(stem, extension)
182+
or
183+
extension = "d.ts" and
184+
result = this.getFile(stem + ".d", "ts")
185+
}
186+
178187
/**
179188
* Gets the file in this folder that has the given `stem` and any of the supported JavaScript extensions.
180189
*
@@ -188,7 +197,11 @@ class Folder extends Container, @folder {
188197
*/
189198
File getJavaScriptFile(string stem) {
190199
result =
191-
min(int p, string ext | p = getFileExtensionPriority(ext) | this.getFile(stem, ext) order by p)
200+
min(int p, string ext |
201+
p = getFileExtensionPriority(ext)
202+
|
203+
this.getFileLongExtension(stem, ext) order by p
204+
)
192205
}
193206

194207
/** Gets a subfolder contained in this folder. */

0 commit comments

Comments
 (0)