diff --git a/change-notes/1.23/extractor-javascript.md b/change-notes/1.23/extractor-javascript.md index 5c61ce675416..557c9ccda24d 100644 --- a/change-notes/1.23/extractor-javascript.md +++ b/change-notes/1.23/extractor-javascript.md @@ -8,3 +8,4 @@ * Recognition of CommonJS modules has improved. As a result, some files that were previously extracted as global scripts are now extracted as modules. * Top-level `await` is now supported. +* A bug was fixed in how the TypeScript extractor handles default-exported anonymous classes. diff --git a/javascript/extractor/src/com/semmle/js/extractor/Main.java b/javascript/extractor/src/com/semmle/js/extractor/Main.java index 466e93639eb1..4b77c9ab22ee 100644 --- a/javascript/extractor/src/com/semmle/js/extractor/Main.java +++ b/javascript/extractor/src/com/semmle/js/extractor/Main.java @@ -1,13 +1,5 @@ package com.semmle.js.extractor; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Pattern; - import com.semmle.js.extractor.ExtractorConfig.HTMLHandling; import com.semmle.js.extractor.ExtractorConfig.Platform; import com.semmle.js.extractor.ExtractorConfig.SourceType; @@ -31,6 +23,13 @@ import com.semmle.util.process.ArgsParser; import com.semmle.util.process.ArgsParser.FileMode; import com.semmle.util.trap.TrapWriter; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; /** The main entry point of the JavaScript extractor. */ public class Main { @@ -38,7 +37,7 @@ public class Main { * A version identifier that should be updated every time the extractor changes in such a way that * it may produce different tuples for the same file under the same {@link ExtractorConfig}. */ - public static final String EXTRACTOR_VERSION = "2019-09-18"; + public static final String EXTRACTOR_VERSION = "2019-10-07"; public static final Pattern NEWLINE = Pattern.compile("\n"); diff --git a/javascript/extractor/src/com/semmle/js/parser/TypeScriptASTConverter.java b/javascript/extractor/src/com/semmle/js/parser/TypeScriptASTConverter.java index de9042b6d366..55a36130efec 100644 --- a/javascript/extractor/src/com/semmle/js/parser/TypeScriptASTConverter.java +++ b/javascript/extractor/src/com/semmle/js/parser/TypeScriptASTConverter.java @@ -942,11 +942,13 @@ private Node convertClass(JsonObject node, String kind, SourceLocation loc) thro SourceLocation bodyLoc = new SourceLocation(loc.getSource(), loc.getStart(), loc.getEnd()); advance(bodyLoc, skip); ClassBody body = new ClassBody(bodyLoc, convertChildren(node, "members")); - if ("ClassExpression".equals(kind)) { + if ("ClassExpression".equals(kind) || id == null) { + // Note that `export default class {}` is represented as a ClassDeclaration + // in TypeScript but we treat this as a ClassExpression. ClassExpression classExpr = new ClassExpression(loc, id, typeParameters, superClass, superInterfaces, body); attachSymbolInformation(classExpr.getClassDef(), node); - return classExpr; + return fixExports(loc, classExpr); } boolean hasDeclareKeyword = hasModifier(node, "DeclareKeyword"); boolean hasAbstractKeyword = hasModifier(node, "AbstractKeyword"); @@ -1225,6 +1227,11 @@ private Node convertForOfStatement(JsonObject node, SourceLocation loc) throws P private Node convertFunctionDeclaration(JsonObject node, SourceLocation loc) throws ParseError { List params = convertParameters(node); Identifier fnId = convertChild(node, "name", "Identifier"); + if (fnId == null) { + // Anonymous function declarations may occur as part of default exported functions. + // We represent these as function expressions. + return fixExports(loc, convertFunctionExpression(node, loc)); + } BlockStatement fnbody = convertChild(node, "body"); boolean generator = hasChild(node, "asteriskToken"); boolean async = hasModifier(node, "AsyncKeyword"); @@ -2305,7 +2312,7 @@ private IJSXName convertJSXName(Expression e) { *

If the declared statement has decorators, the {@code loc} should first be advanced past * these using {@link #advanceUntilAfter}. */ - private Node fixExports(SourceLocation loc, Statement decl) { + private Node fixExports(SourceLocation loc, Node decl) { Matcher m = EXPORT_DECL_START.matcher(loc.getSource()); if (m.find()) { String skipped = m.group(0); @@ -2313,7 +2320,7 @@ private Node fixExports(SourceLocation loc, Statement decl) { advance(loc, skipped); // capture group 1 is `default`, if present if (m.group(1) == null) - return new ExportNamedDeclaration(outerLoc, decl, new ArrayList<>(), null); + return new ExportNamedDeclaration(outerLoc, (Statement) decl, new ArrayList<>(), null); return new ExportDefaultDeclaration(outerLoc, decl); } return decl; diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportClass.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportClass.ts new file mode 100644 index 000000000000..c8de02deadf4 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportClass.ts @@ -0,0 +1,3 @@ +import { Foo } from "somwhere"; + +export default class extends Foo {} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportFunction.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportFunction.ts new file mode 100644 index 000000000000..30c2c5ef190a --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/exportFunction.ts @@ -0,0 +1,3 @@ +import { Foo } from "somwhere"; + +export default function(x=Foo) {} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.expected b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.expected new file mode 100644 index 000000000000..6818c85c6d24 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.expected @@ -0,0 +1,5 @@ +classExprs +| exportClass.ts:3:16:3:35 | class extends Foo {} | +functionExprs +| exportClass.ts:3:34:3:33 | (...arg ... rgs); } | +| exportFunction.ts:3:16:3:33 | function(x=Foo) {} | diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.ql b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.ql new file mode 100644 index 000000000000..c4e321bb4192 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/DefaultExports/test.ql @@ -0,0 +1,5 @@ +import javascript + +query ClassExpr classExprs() { any() } + +query FunctionExpr functionExprs() { any() } diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/ExportedClassExpr/exportFunction.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/ExportedClassExpr/exportFunction.ts new file mode 100644 index 000000000000..1fd441ff0408 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/ExportedClassExpr/exportFunction.ts @@ -0,0 +1,3 @@ +import { Foo } from "./node_modules/somwhere"; + +export default function(x=Foo) {} diff --git a/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultClass.ts b/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultClass.ts new file mode 100644 index 000000000000..6481a749c40c --- /dev/null +++ b/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultClass.ts @@ -0,0 +1,5 @@ +var C1 = global.C1; // OK +var C2 = global.C2; // OK + +class C extends C1 {} +export default class extends C2 {} diff --git a/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultFunction.ts b/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultFunction.ts new file mode 100644 index 000000000000..5c6b48f8796c --- /dev/null +++ b/javascript/ql/test/query-tests/Declarations/DeadStoreOfLocal/exportDefaultFunction.ts @@ -0,0 +1,3 @@ +var C1 = global.C1; // OK + +export default function(x=C1) {}