From 5676891e44806940a660f3135f73ca5ce03d15eb Mon Sep 17 00:00:00 2001 From: Asger Feldthaus Date: Thu, 29 Oct 2020 21:14:27 +0000 Subject: [PATCH] JS: Add TemplateLiteralTypeExpr --- .../src/com/semmle/js/ast/DefaultVisitor.java | 6 ++ .../src/com/semmle/js/ast/NodeCopier.java | 6 ++ .../com/semmle/js/ast/TemplateLiteral.java | 15 +++-- .../src/com/semmle/js/ast/Visitor.java | 3 + .../com/semmle/js/extractor/ASTExtractor.java | 8 +++ .../semmle/js/extractor/TypeExprKinds.java | 13 +++++ .../ts/ast/TemplateLiteralTypeExpr.java | 49 ++++++++++++++++ .../ts/extractor/TypeScriptASTConverter.java | 16 ++++++ .../ql/src/semmle/javascript/TypeScript.qll | 23 ++++++++ .../ql/src/semmlecode.javascript.dbscheme | 1 + .../templateLiteralTypes.ts | 57 +++++++++++++++++++ .../TemplateLiteralTypes/test.expected | 9 +++ .../TypeScript/TemplateLiteralTypes/test.ql | 5 ++ .../Declarations/UnusedVariable/importtype.ts | 6 +- 14 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 javascript/extractor/src/com/semmle/ts/ast/TemplateLiteralTypeExpr.java create mode 100644 javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/templateLiteralTypes.ts create mode 100644 javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.expected create mode 100644 javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.ql diff --git a/javascript/extractor/src/com/semmle/js/ast/DefaultVisitor.java b/javascript/extractor/src/com/semmle/js/ast/DefaultVisitor.java index e7ea02467e2..151936f46dc 100644 --- a/javascript/extractor/src/com/semmle/js/ast/DefaultVisitor.java +++ b/javascript/extractor/src/com/semmle/js/ast/DefaultVisitor.java @@ -43,6 +43,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeAliasDeclaration; import com.semmle.ts.ast.TypeAssertion; @@ -368,6 +369,11 @@ public class DefaultVisitor implements Visitor { return visit((Expression) nd, c); } + @Override + public R visit(TemplateLiteralTypeExpr nd, C c) { + return visit((TypeExpression) nd, c); + } + @Override public R visit(TaggedTemplateExpression nd, C c) { return visit((Expression) nd, c); diff --git a/javascript/extractor/src/com/semmle/js/ast/NodeCopier.java b/javascript/extractor/src/com/semmle/js/ast/NodeCopier.java index 7fa73bd4a31..c381a2d6b10 100644 --- a/javascript/extractor/src/com/semmle/js/ast/NodeCopier.java +++ b/javascript/extractor/src/com/semmle/js/ast/NodeCopier.java @@ -39,6 +39,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeAliasDeclaration; import com.semmle.ts.ast.TypeAssertion; @@ -419,6 +420,11 @@ public class NodeCopier implements Visitor { return new TemplateLiteral(visit(nd.getLoc()), copy(nd.getExpressions()), copy(nd.getQuasis())); } + @Override + public TemplateLiteralTypeExpr visit(TemplateLiteralTypeExpr nd, Void q) { + return new TemplateLiteralTypeExpr(visit(nd.getLoc()), copy(nd.getExpressions()), copy(nd.getQuasis())); + } + @Override public TaggedTemplateExpression visit(TaggedTemplateExpression nd, Void q) { return new TaggedTemplateExpression( diff --git a/javascript/extractor/src/com/semmle/js/ast/TemplateLiteral.java b/javascript/extractor/src/com/semmle/js/ast/TemplateLiteral.java index 7b9333576a4..f91b91722d8 100644 --- a/javascript/extractor/src/com/semmle/js/ast/TemplateLiteral.java +++ b/javascript/extractor/src/com/semmle/js/ast/TemplateLiteral.java @@ -21,20 +21,23 @@ public class TemplateLiteral extends Expression { super("TemplateLiteral", loc); this.expressions = expressions; this.quasis = quasis; - this.children = mergeChildren(expressions, quasis); + this.children = TemplateLiteral.mergeChildren(expressions, quasis); } /* * Merge quasis and expressions into a single array in textual order. * Also filter out the empty constant strings that the parser likes to generate. */ - private List mergeChildren( - List expressions, List quasis) { - List children = new ArrayList(); + @SuppressWarnings("unchecked") + public static List mergeChildren( + List expressions, + List quasis) { + + List children = new ArrayList(); int j = 0, n = quasis.size(); for (int i = 0, m = expressions.size(); i < m; ++i) { - Expression expr = expressions.get(i); + INode expr = expressions.get(i); for (; j < n; ++j) { TemplateElement quasi = quasis.get(j); if (quasi.getLoc().getStart().compareTo(expr.getLoc().getStart()) > 0) break; @@ -48,7 +51,7 @@ public class TemplateLiteral extends Expression { if (!quasi.getRaw().isEmpty()) children.add(quasi); } - return children; + return (List)children; } @Override diff --git a/javascript/extractor/src/com/semmle/js/ast/Visitor.java b/javascript/extractor/src/com/semmle/js/ast/Visitor.java index 618e54193fc..98551d2e324 100644 --- a/javascript/extractor/src/com/semmle/js/ast/Visitor.java +++ b/javascript/extractor/src/com/semmle/js/ast/Visitor.java @@ -39,6 +39,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeAliasDeclaration; import com.semmle.ts.ast.TypeAssertion; @@ -157,6 +158,8 @@ public interface Visitor { public R visit(TemplateLiteral nd, C q); + public R visit(TemplateLiteralTypeExpr nd, C q); + public R visit(TaggedTemplateExpression nd, C q); public R visit(ArrowFunctionExpression nd, C q); diff --git a/javascript/extractor/src/com/semmle/js/extractor/ASTExtractor.java b/javascript/extractor/src/com/semmle/js/extractor/ASTExtractor.java index ba6ef39efd2..62a1465aea5 100644 --- a/javascript/extractor/src/com/semmle/js/extractor/ASTExtractor.java +++ b/javascript/extractor/src/com/semmle/js/extractor/ASTExtractor.java @@ -143,6 +143,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeAliasDeclaration; import com.semmle.ts.ast.TypeAssertion; @@ -1270,6 +1271,13 @@ public class ASTExtractor { return key; } + @Override + public Label visit(TemplateLiteralTypeExpr nd, Context c) { + Label key = super.visit(nd, c); + visitAll(nd.getChildren(), key, IdContext.typeBind, 0); + return key; + } + @Override public Label visit(TemplateElement nd, Context c) { Label key = super.visit(nd, c); diff --git a/javascript/extractor/src/com/semmle/js/extractor/TypeExprKinds.java b/javascript/extractor/src/com/semmle/js/extractor/TypeExprKinds.java index 6c9263a5360..0d2769d9e66 100644 --- a/javascript/extractor/src/com/semmle/js/extractor/TypeExprKinds.java +++ b/javascript/extractor/src/com/semmle/js/extractor/TypeExprKinds.java @@ -6,6 +6,7 @@ import com.semmle.js.ast.INode; import com.semmle.js.ast.Identifier; import com.semmle.js.ast.Literal; import com.semmle.js.ast.MemberExpression; +import com.semmle.js.ast.TemplateElement; import com.semmle.js.extractor.ASTExtractor.IdContext; import com.semmle.ts.ast.ArrayTypeExpr; import com.semmle.ts.ast.ConditionalTypeExpr; @@ -22,6 +23,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeParameter; import com.semmle.ts.ast.TypeofTypeExpr; @@ -67,6 +69,7 @@ public class TypeExprKinds { private static final int restTypeExpr = 34; private static final int bigintLiteralTypeExpr = 35; private static final int readonlyTypeExpr = 36; + private static final int templateLiteralTypeExpr = 37; public static int getTypeExprKind(final INode type, final IdContext idcontext) { Integer kind = @@ -241,6 +244,16 @@ public class TypeExprKinds { public Integer visit(RestTypeExpr nd, Void c) { return restTypeExpr; } + + @Override + public Integer visit(TemplateLiteralTypeExpr nd, Void c) { + return templateLiteralTypeExpr; + } + + @Override + public Integer visit(TemplateElement nd, Void c) { + return stringLiteralTypeExpr; + } }, null); if (kind == null) diff --git a/javascript/extractor/src/com/semmle/ts/ast/TemplateLiteralTypeExpr.java b/javascript/extractor/src/com/semmle/ts/ast/TemplateLiteralTypeExpr.java new file mode 100644 index 00000000000..d2a4aef926c --- /dev/null +++ b/javascript/extractor/src/com/semmle/ts/ast/TemplateLiteralTypeExpr.java @@ -0,0 +1,49 @@ +package com.semmle.ts.ast; + +import java.util.ArrayList; +import java.util.List; + +import com.semmle.js.ast.Expression; +import com.semmle.js.ast.INode; +import com.semmle.js.ast.Node; +import com.semmle.js.ast.SourceLocation; +import com.semmle.js.ast.TemplateElement; +import com.semmle.js.ast.TemplateLiteral; +import com.semmle.js.ast.Visitor; + +/** + * A template literal used in a type, such as in type T = `Hello, ${name}!`. + */ +public class TemplateLiteralTypeExpr extends TypeExpression { + private final List expressions; + private final List quasis; + private final List children; + + public TemplateLiteralTypeExpr( + SourceLocation loc, List expressions, List quasis) { + super("TemplateLiteralTypeExpr", loc); + this.expressions = expressions; + this.quasis = quasis; + this.children = TemplateLiteral.mergeChildren(expressions, quasis); + } + + @Override + public A accept(Visitor v, Q q) { + return v.visit(this, q); + } + + /** The type expressions in this template. */ + public List getExpressions() { + return expressions; + } + + /** The template elements in this template. */ + public List getQuasis() { + return quasis; + } + + /** All type expressions and template elements in this template, in lexical order. */ + public List getChildren() { + return children; + } +} diff --git a/javascript/extractor/src/com/semmle/ts/extractor/TypeScriptASTConverter.java b/javascript/extractor/src/com/semmle/ts/extractor/TypeScriptASTConverter.java index 0950e16b501..aff87a3715d 100644 --- a/javascript/extractor/src/com/semmle/ts/extractor/TypeScriptASTConverter.java +++ b/javascript/extractor/src/com/semmle/ts/extractor/TypeScriptASTConverter.java @@ -145,6 +145,7 @@ import com.semmle.ts.ast.OptionalTypeExpr; import com.semmle.ts.ast.ParenthesizedTypeExpr; import com.semmle.ts.ast.PredicateTypeExpr; import com.semmle.ts.ast.RestTypeExpr; +import com.semmle.ts.ast.TemplateLiteralTypeExpr; import com.semmle.ts.ast.TupleTypeExpr; import com.semmle.ts.ast.TypeAliasDeclaration; import com.semmle.ts.ast.TypeAssertion; @@ -576,6 +577,8 @@ public class TypeScriptASTConverter { case "TemplateMiddle": case "TemplateTail": return convertTemplateElement(node, kind, loc); + case "TemplateLiteralType": + return convertTemplateLiteralType(node, loc); case "ThisKeyword": return convertThisKeyword(loc); case "ThisType": @@ -2152,6 +2155,19 @@ public class TypeScriptASTConverter { return new TemplateLiteral(loc, expressions, quasis); } + private Node convertTemplateLiteralType(JsonObject node, SourceLocation loc) throws ParseError { + List quasis; + List expressions = new ArrayList<>(); + quasis = new ArrayList<>(); + quasis.add(convertChild(node, "head")); + for (JsonElement elt : node.get("templateSpans").getAsJsonArray()) { + JsonObject templateSpan = (JsonObject) elt; + expressions.add(convertChildAsType(templateSpan, "type")); + quasis.add(convertChild(templateSpan, "literal")); + } + return new TemplateLiteralTypeExpr(loc, expressions, quasis); + } + private Node convertTemplateElement(JsonObject node, String kind, SourceLocation loc) { boolean tail = "TemplateTail".equals(kind); if (loc.getSource().startsWith("`") || loc.getSource().startsWith("}")) { diff --git a/javascript/ql/src/semmle/javascript/TypeScript.qll b/javascript/ql/src/semmle/javascript/TypeScript.qll index 99a06d96069..a1b312411ac 100644 --- a/javascript/ql/src/semmle/javascript/TypeScript.qll +++ b/javascript/ql/src/semmle/javascript/TypeScript.qll @@ -1213,6 +1213,29 @@ class InferTypeExpr extends @infer_typeexpr, TypeParameterized, TypeExpr { override string getAPrimaryQlClass() { result = "InferTypeExpr" } } +/** + * A template literal used as a type. + */ +class TemplateLiteralTypeExpr extends @template_literal_typeexpr, TypeExpr { + /** + * Gets the `i`th element of this template literal, which may either + * be a type expression or a constant template element. + */ + ExprOrType getElement(int i) { result = getChild(i) } + + /** + * Gets an element of this template literal. + */ + ExprOrType getAnElement() { result = getElement(_) } + + /** + * Gets the number of elements of this template literal. + */ + int getNumElement() { result = count(getAnElement()) } + + override string getAPrimaryQlClass() { result = "TemplateLiteralTypeExpr" } +} + /** * A scope induced by a conditional type expression whose `extends` type * contains `infer` types. diff --git a/javascript/ql/src/semmlecode.javascript.dbscheme b/javascript/ql/src/semmlecode.javascript.dbscheme index 9a70dfc47d8..c8859f3725d 100644 --- a/javascript/ql/src/semmlecode.javascript.dbscheme +++ b/javascript/ql/src/semmlecode.javascript.dbscheme @@ -584,6 +584,7 @@ case @typeexpr.kind of | 34 = @rest_typeexpr | 35 = @bigint_literal_typeexpr | 36 = @readonly_typeexpr +| 37 = @template_literal_typeexpr ; @typeref = @typeaccess | @type_decl; diff --git a/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/templateLiteralTypes.ts b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/templateLiteralTypes.ts new file mode 100644 index 00000000000..40fcee52616 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/templateLiteralTypes.ts @@ -0,0 +1,57 @@ +// Based on snippets in https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/ + +type World = "world"; +type Greeting = `hello ${World}`; + + +type Color = "red" | "blue"; +type Quantity = "one" | "two"; +type SeussFish = `${Quantity | Color} fish`; + + +type VerticalAlignment = "top" | "middle" | "bottom"; +type HorizontalAlignment = "left" | "center" | "right"; +declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void; + + +type PropEventSource = { + on(eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void; +}; + +declare function makeWatchedObject(obj: T): T & PropEventSource; + +let person = makeWatchedObject({ + firstName: "Homer", + age: 42, + location: "Springfield", +}); + + +// Can no longer be parsed by TypeScript: +// type EnthusiasticGreeting = `${uppercase T}` + +type HELLO = EnthusiasticGreeting<"hello">; + +// Can no longer be parsed by TypeScript: +// type Getters = { +// [K in keyof T as `get${capitalize K}`]: () => T[K] +// }; + +interface Person { + name: string; + age: number; + location: string; +} + +type LazyPerson = Getters; + +type RemoveKindField = { + [K in keyof T as Exclude]: T[K] +}; + +interface Circle { + kind: "circle"; + radius: number; +} + +type KindlessCircle = RemoveKindField; diff --git a/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.expected b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.expected new file mode 100644 index 00000000000..a230e0afd3c --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.expected @@ -0,0 +1,9 @@ +| templateLiteralTypes.ts:4:17:4:32 | `hello ${World}` | 0 | templateLiteralTypes.ts:4:18:4:23 | hello | +| templateLiteralTypes.ts:4:17:4:32 | `hello ${World}` | 1 | templateLiteralTypes.ts:4:26:4:30 | World | +| templateLiteralTypes.ts:9:18:9:43 | `${Quan ... } fish` | 0 | templateLiteralTypes.ts:9:21:9:36 | Quantity \| Color | +| templateLiteralTypes.ts:9:18:9:43 | `${Quan ... } fish` | 1 | templateLiteralTypes.ts:9:38:9:42 | fish | +| templateLiteralTypes.ts:14:38:14:82 | `${Vert ... nment}` | 0 | templateLiteralTypes.ts:14:41:14:57 | VerticalAlignment | +| templateLiteralTypes.ts:14:38:14:82 | `${Vert ... nment}` | 1 | templateLiteralTypes.ts:14:59:14:59 | - | +| templateLiteralTypes.ts:14:38:14:82 | `${Vert ... nment}` | 2 | templateLiteralTypes.ts:14:62:14:80 | HorizontalAlignment | +| templateLiteralTypes.ts:18:47:18:59 | `${K}Changed` | 0 | templateLiteralTypes.ts:18:50:18:50 | K | +| templateLiteralTypes.ts:18:47:18:59 | `${K}Changed` | 1 | templateLiteralTypes.ts:18:52:18:58 | Changed | diff --git a/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.ql b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.ql new file mode 100644 index 00000000000..30e21c3fc18 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/TemplateLiteralTypes/test.ql @@ -0,0 +1,5 @@ +import javascript + +query ExprOrType getElement(TemplateLiteralTypeExpr e, int i) { + result = e.getElement(i) +} diff --git a/javascript/ql/test/query-tests/Declarations/UnusedVariable/importtype.ts b/javascript/ql/test/query-tests/Declarations/UnusedVariable/importtype.ts index 9479962919b..0fa2f18066c 100644 --- a/javascript/ql/test/query-tests/Declarations/UnusedVariable/importtype.ts +++ b/javascript/ql/test/query-tests/Declarations/UnusedVariable/importtype.ts @@ -3,4 +3,8 @@ import SomeInterface from 'somewhere'; class SomeClass implements SomeInterface { } -new SomeClass(); \ No newline at end of file +new SomeClass(); + +import SomethingElse from 'somewhere'; // OK: SomethingElse is used in a type + +type T = `Now for ${SomethingElse}`;