TS: Support type-only import/export

This commit is contained in:
Asger Feldthaus
2020-02-07 16:25:49 +00:00
parent 0351f0b775
commit 8d58aad0f2
11 changed files with 138 additions and 11 deletions

View File

@@ -15,13 +15,21 @@ public class ExportNamedDeclaration extends ExportDeclaration {
private final Statement declaration;
private final List<ExportSpecifier> specifiers;
private final Literal source;
private final boolean hasTypeKeyword;
public ExportNamedDeclaration(
SourceLocation loc, Statement declaration, List<ExportSpecifier> specifiers, Literal source) {
this(loc, declaration, specifiers, source, false);
}
public ExportNamedDeclaration(
SourceLocation loc, Statement declaration, List<ExportSpecifier> specifiers, Literal source,
boolean hasTypeKeyword) {
super("ExportNamedDeclaration", loc);
this.declaration = declaration;
this.specifiers = specifiers;
this.source = source;
this.hasTypeKeyword = hasTypeKeyword;
}
public Statement getDeclaration() {
@@ -48,4 +56,9 @@ public class ExportNamedDeclaration extends ExportDeclaration {
public <C, R> R accept(Visitor<C, R> v, C c) {
return v.visit(this, c);
}
/** Returns true if this is an <code>export type</code> declaration. */
public boolean hasTypeKeyword() {
return hasTypeKeyword;
}
}

View File

@@ -1,8 +1,9 @@
package com.semmle.js.ast;
import com.semmle.ts.ast.INodeWithSymbol;
import java.util.List;
import com.semmle.ts.ast.INodeWithSymbol;
/**
* An import declaration, which can be of one of the following forms:
*
@@ -24,10 +25,17 @@ public class ImportDeclaration extends Statement implements INodeWithSymbol {
private int symbol = -1;
private boolean hasTypeKeyword;
public ImportDeclaration(SourceLocation loc, List<ImportSpecifier> specifiers, Literal source) {
this(loc, specifiers, source, false);
}
public ImportDeclaration(SourceLocation loc, List<ImportSpecifier> specifiers, Literal source, boolean hasTypeKeyword) {
super("ImportDeclaration", loc);
this.specifiers = specifiers;
this.source = source;
this.hasTypeKeyword = hasTypeKeyword;
}
public Literal getSource() {
@@ -52,4 +60,9 @@ public class ImportDeclaration extends Statement implements INodeWithSymbol {
public void setSymbol(int symbol) {
this.symbol = symbol;
}
/** Returns true if this is an <code>import type</code> declaration. */
public boolean hasTypeKeyword() {
return hasTypeKeyword;
}
}

View File

@@ -250,6 +250,22 @@ public class ASTExtractor {
/** An identifier that declares a variable and a namespace. */
varAndNamespaceDecl,
/**
* An identifier that occurs in a type-only import.
*
* These may declare a type and/or a namespace, but for compatibility with our AST,
* must be emitted as a VarDecl (with no variable binding).
*/
typeOnlyImport,
/**
* An identifier that occurs in a type-only export.
*
* These may refer to a type and/or a namespace, but for compatibility with our AST,
* must be emitted as an ExportVarAccess (with no variable binding).
*/
typeOnlyExport,
/** An identifier that declares a variable, type, and namepsace. */
varAndTypeAndNamespaceDecl,
@@ -278,7 +294,8 @@ public class ASTExtractor {
* True if this occurs as part of a type annotation, i.e. it is {@link #typeBind} or {@link
* #typeDecl}, {@link #typeLabel}, {@link #varInTypeBind}, or {@link #namespaceBind}.
*
* <p>Does not hold for {@link #varAndTypeDecl}.
* <p>Does not hold for {@link #varAndTypeDecl} or {@link #typeOnlyImportExport} as these
* do not occur in type annotations.
*/
public boolean isInsideType() {
return this == typeBind
@@ -488,6 +505,14 @@ public class ASTExtractor {
addVariableBinding("decl", key, name);
addNamespaceBinding("namespacedecl", key, name);
break;
case typeOnlyImport:
addTypeBinding("typedecl", key, name);
addNamespaceBinding("namespacedecl", key, name);
break;
case typeOnlyExport:
addTypeBinding("typebind", key, name);
addNamespaceBinding("namespacebind", key, name);
break;
case varAndTypeAndNamespaceDecl:
addVariableBinding("decl", key, name);
addTypeBinding("typedecl", key, name);
@@ -1538,7 +1563,14 @@ public class ASTExtractor {
Label lbl = super.visit(nd, c);
visit(nd.getDeclaration(), lbl, -1);
visit(nd.getSource(), lbl, -2);
visitAll(nd.getSpecifiers(), lbl, nd.hasSource() ? IdContext.label : IdContext.export, 0);
IdContext childContext =
nd.hasSource() ? IdContext.label :
nd.hasTypeKeyword() ? IdContext.typeOnlyExport :
IdContext.export;
visitAll(nd.getSpecifiers(), lbl, childContext, 0);
if (nd.hasTypeKeyword()) {
trapwriter.addTuple("hasTypeKeyword", lbl);
}
return lbl;
}
@@ -1554,8 +1586,12 @@ public class ASTExtractor {
public Label visit(ImportDeclaration nd, Context c) {
Label lbl = super.visit(nd, c);
visit(nd.getSource(), lbl, -1);
visitAll(nd.getSpecifiers(), lbl);
IdContext childContext = nd.hasTypeKeyword() ? IdContext.typeOnlyImport : IdContext.varAndTypeAndNamespaceDecl;
visitAll(nd.getSpecifiers(), lbl, childContext, 0);
emitNodeSymbol(nd, lbl);
if (nd.hasTypeKeyword()) {
trapwriter.addTuple("hasTypeKeyword", lbl);
}
return lbl;
}
@@ -1563,7 +1599,7 @@ public class ASTExtractor {
public Label visit(ImportSpecifier nd, Context c) {
Label lbl = super.visit(nd, c);
visit(nd.getImported(), lbl, 0, IdContext.label);
visit(nd.getLocal(), lbl, 1, IdContext.varAndTypeAndNamespaceDecl);
visit(nd.getLocal(), lbl, 1, c.idcontext);
return lbl;
}

View File

@@ -1,5 +1,9 @@
package com.semmle.js.extractor;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.semmle.jcorn.TokenType;
import com.semmle.jcorn.jsx.JSXParser;
import com.semmle.js.ast.AssignmentExpression;
@@ -29,9 +33,6 @@ import com.semmle.ts.ast.DecoratorList;
import com.semmle.ts.ast.ExpressionWithTypeArguments;
import com.semmle.ts.ast.TypeAssertion;
import com.semmle.util.exception.CatastrophicError;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.Map;
/** Map from SpiderMonkey expression types to the numeric kinds used in the DB scheme. */
public class ExprKinds {
@@ -154,6 +155,8 @@ public class ExprKinds {
idKinds.put(IdContext.namespaceDecl, 78);
idKinds.put(IdContext.varAndNamespaceDecl, 78);
idKinds.put(IdContext.varAndTypeAndNamespaceDecl, 78);
idKinds.put(IdContext.typeOnlyImport, 78);
idKinds.put(IdContext.typeOnlyExport, 103);
idKinds.put(IdContext.varBind, 79);
idKinds.put(IdContext.export, 103);
idKinds.put(IdContext.exportBase, 103);

View File

@@ -1179,11 +1179,12 @@ public class TypeScriptASTConverter {
private Node convertExportDeclaration(JsonObject node, SourceLocation loc) throws ParseError {
Literal source = tryConvertChild(node, "moduleSpecifier", Literal.class);
if (hasChild(node, "exportClause")) {
boolean hasTypeKeyword = node.get("isTypeOnly").getAsBoolean();
List<ExportSpecifier> specifiers =
hasKind(node.get("exportClause"), "NamespaceExport")
? Collections.singletonList(convertChild(node, "exportClause"))
: convertChildren(node.get("exportClause").getAsJsonObject(), "elements");
return new ExportNamedDeclaration(loc, null, specifiers, source);
return new ExportNamedDeclaration(loc, null, specifiers, source, hasTypeKeyword);
} else {
return new ExportAllDeclaration(loc, source);
}
@@ -1368,6 +1369,7 @@ public class TypeScriptASTConverter {
private Node convertImportDeclaration(JsonObject node, SourceLocation loc) throws ParseError {
Literal src = tryConvertChild(node, "moduleSpecifier", Literal.class);
List<ImportSpecifier> specifiers = new ArrayList<>();
boolean hasTypeKeyword = false;
if (hasChild(node, "importClause")) {
JsonObject importClause = node.get("importClause").getAsJsonObject();
if (hasChild(importClause, "name")) {
@@ -1381,8 +1383,9 @@ public class TypeScriptASTConverter {
specifiers.addAll(convertChildren(namedBindings, "elements"));
}
}
hasTypeKeyword = importClause.get("isTypeOnly").getAsBoolean();
}
ImportDeclaration importDecl = new ImportDeclaration(loc, specifiers, src);
ImportDeclaration importDecl = new ImportDeclaration(loc, specifiers, src, hasTypeKeyword);
attachSymbolInformation(importDecl, node);
return importDecl;
}

View File

@@ -125,7 +125,7 @@ class ASTNode extends @ast_node, Locatable {
* Holds if this is part of an ambient declaration or type annotation in a TypeScript file.
*
* A declaration is ambient if it occurs under a `declare` modifier or is
* an interface declaration, type alias, or type annotation.
* an interface declaration, type alias, type annotation, or type-only import/export declaration.
*
* The TypeScript compiler emits no code for ambient declarations, but they
* can affect name resolution and type checking at compile-time.

View File

@@ -76,6 +76,16 @@ class ImportDeclaration extends Stmt, Import, @importdeclaration {
// `import { createServer } from 'http'`
result = DataFlow::destructuredModuleImportNode(this)
}
/** Holds if this is declared with the `type` keyword, so it only imports types. */
predicate isTypeOnly() {
hasTypeKeyword(this)
}
override predicate isAmbient() {
Stmt.super.isAmbient() or
isTypeOnly()
}
}
/** A literal path expression appearing in an `import` declaration. */
@@ -256,6 +266,16 @@ abstract class ExportDeclaration extends Stmt, @exportdeclaration {
* to module `a` or possibly to some other module from which `a` re-exports.
*/
abstract DataFlow::Node getSourceNode(string name);
/** Holds if is declared with the `type` keyword, so only types are exported. */
predicate isTypeOnly() {
hasTypeKeyword(this)
}
override predicate isAmbient() {
Stmt.super.isAmbient() or
isTypeOnly()
}
}
/**
@@ -413,6 +433,18 @@ class ExportNamedDeclaration extends ExportDeclaration, @exportnameddeclaration
}
}
/**
* An export declaration with the `type` modifier.
*/
private class TypeOnlyExportDeclaration extends ExportNamedDeclaration {
TypeOnlyExportDeclaration() { isTypeOnly() }
override predicate exportsAs(LexicalName v, string name) {
super.exportsAs(v, name) and
not v instanceof Variable
}
}
/**
* An export specifier in an export declaration.
*

View File

@@ -385,6 +385,8 @@ case @expr.kind of
@exportspecifier = @namedexportspecifier | @exportdefaultspecifier | @exportnamespacespecifier;
@import_or_export_declaration = @importdeclaration | @exportdeclaration;
@typeassertion = @astypeassertion | @prefixtypeassertion;
@classdefinition = @classdeclstmt | @classexpr;
@@ -518,6 +520,7 @@ hasPublicKeyword (int id: @property ref);
hasPrivateKeyword (int id: @property ref);
hasProtectedKeyword (int id: @property ref);
hasReadonlyKeyword (int id: @property ref);
hasTypeKeyword (int id: @import_or_export_declaration ref);
isOptionalMember (int id: @property ref);
hasDefiniteAssignmentAssertion (int id: @field_or_vardeclarator ref);
isOptionalParameterDeclaration (unique int parameter: @pattern ref);

View File

@@ -0,0 +1,6 @@
getAVarReference
| Foo | tst.ts:5:5:5:7 | Foo |
getAnExportAccess
| Foo | tst.ts:3:15:3:17 | Foo |
getATypeDecl
| Foo | tst.ts:1:15:1:17 | Foo |

View File

@@ -0,0 +1,13 @@
import javascript
query VarRef getAVarReference(Variable v) {
result = v.getAReference()
}
query VarRef getAnExportAccess(LocalTypeName t) {
result = t.getAnExportAccess()
}
query TypeDecl getATypeDecl(LocalTypeName t) {
result = t.getADeclaration()
}

View File

@@ -0,0 +1,5 @@
import type { Foo } from "foo";
export type { Foo };
var Foo = 45;