diff --git a/javascript/extractor/lib/typescript/src/type_table.ts b/javascript/extractor/lib/typescript/src/type_table.ts index 0f3b2df1685..9843e2f3988 100644 --- a/javascript/extractor/lib/typescript/src/type_table.ts +++ b/javascript/extractor/lib/typescript/src/type_table.ts @@ -251,8 +251,8 @@ export class TypeTable { * A symbol string is a `;`-separated string consisting of: * - a tag string, `root`, `member`, or `other`, * - an empty string or a `file:pos` string to distinguish this from symbols with other lexical roots, - * - the unqualified name of the symbol, - * - for non-root symbols, the ID of the parent symbol. + * - the ID of the parent symbol, or an empty string if this is a root symbol, + * - the unqualified name of the symbol. * * Symbol strings serve the same dual purpose as type strings (see `typeIds`). */ @@ -667,11 +667,11 @@ export class TypeTable { private getSymbolString(symbol: AugmentedSymbol): string { let parent = symbol.parent; if (parent == null || parent.escapedName === ts.InternalSymbolName.Global) { - return "root;" + this.getSymbolDeclarationString(symbol) + ";" + symbol.name; + return "root;" + this.getSymbolDeclarationString(symbol) + ";;" + symbol.name; } else if (parent.exports != null && parent.exports.get(symbol.escapedName) === symbol) { - return "member;;" + symbol.name + ";" + this.getSymbolId(parent); + return "member;;" + this.getSymbolId(parent) + ";" + symbol.name; } else { - return "other;" + this.getSymbolDeclarationString(symbol) + ";" + symbol.name + ";" + this.getSymbolId(parent); + return "other;" + this.getSymbolDeclarationString(symbol) + ";" + this.getSymbolId(parent) + ";" + symbol.name; } } diff --git a/javascript/extractor/src/com/semmle/ts/extractor/TypeExtractor.java b/javascript/extractor/src/com/semmle/ts/extractor/TypeExtractor.java index b626aab52d0..8d1aae35681 100644 --- a/javascript/extractor/src/com/semmle/ts/extractor/TypeExtractor.java +++ b/javascript/extractor/src/com/semmle/ts/extractor/TypeExtractor.java @@ -4,7 +4,10 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.semmle.util.trap.TrapWriter; import com.semmle.util.trap.TrapWriter.Label; + +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** @@ -97,7 +100,7 @@ public class TypeExtractor { private void extractType(int id) { Label lbl = trapWriter.globalID("type;" + id); String contents = table.getTypeString(id); - String[] parts = contents.split(";"); + String[] parts = split(contents); int kind = tagToKind.get(parts[0]); trapWriter.addTuple("types", lbl, kind, table.getTypeToStringValue(id)); int firstChild = 1; @@ -160,14 +163,15 @@ public class TypeExtractor { } private void extractSymbol(int index) { - // Format is: kind;decl;name[;parent] - String[] parts = table.getSymbolString(index).split(";"); + // Format is: kind;decl;parent;name + String[] parts = split(table.getSymbolString(index), 4); int kind = symbolKind.get(parts[0]); - String name = parts[2]; + String name = parts[3]; Label label = trapWriter.globalID("symbol;" + index); trapWriter.addTuple("symbols", label, kind, name); - if (parts.length == 4) { - Label parentLabel = trapWriter.globalID("symbol;" + parts[3]); + String parentStr = parts[2]; + if (parentStr.length() > 0) { + Label parentLabel = trapWriter.globalID("symbol;" + parentStr); trapWriter.addTuple("symbol_parent", label, parentLabel); } } @@ -185,7 +189,7 @@ public class TypeExtractor { private void extractSignature(int index) { // Format is: // kind;numTypeParams;requiredParams;returnType(;paramName;paramType)* - String[] parts = table.getSignatureString(index).split(";"); + String[] parts = split(table.getSignatureString(index)); Label label = trapWriter.globalID("signature;" + index); int kind = Integer.parseInt(parts[0]); int numberOfTypeParameters = Integer.parseInt(parts[1]); @@ -269,4 +273,35 @@ public class TypeExtractor { trapWriter.globalID("type;" + typeId)); } } + + /** Like {@link #split(String)} without a limit. */ + private static String[] split(String input) { + return split(input, -1); + } + + /** + * Splits the input around the semicolon (;) character, preserving all empty + * substrings. + * + *

At most limit substrings will be extracted. If the limit is reached, the last + * substring will extend to the end of the string, possibly itself containing semicolons. + * + *

Note that the {@link String#split(String)} method does not preserve empty substrings at the + * end of the string in case the string ends with a semicolon. + */ + private static String[] split(String input, int limit) { + List result = new ArrayList(); + int lastPos = 0; + for (int i = 0; i < input.length(); ++i) { + if (input.charAt(i) == ';') { + result.add(input.substring(lastPos, i)); + lastPos = i + 1; + if (result.size() == limit - 1) break; + } + } + result.add(input.substring(lastPos)); + return result.toArray(EMPTY_STRING_ARRAY); + } + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; } diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.expected b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.expected new file mode 100644 index 00000000000..eb1c70712c6 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.expected @@ -0,0 +1,11 @@ +| in unknown scope | +| Array in global scope | +| Intl in global scope | +| Intl.CollatorOptions in global scope | +| Intl.NumberFormatOptions in global scope | +| MK in unknown scope | +| Mapped in test.ts | +| RegExp in global scope | +| RegExpMatchArray in global scope | +| fn in test.ts | +| test.ts | diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ql b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ql new file mode 100644 index 00000000000..3212e219ccd --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ql @@ -0,0 +1,4 @@ +import javascript + +from CanonicalName name +select name diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ts new file mode 100644 index 00000000000..3082f40600f --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/test.ts @@ -0,0 +1,9 @@ +type Mapped = { +     [mk in MK]: string + }; + +export function fn(ev: Mapped) { +    const props: Mapped = { +        ...ev +    }; +} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/tsconfig.json b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/tsconfig.json new file mode 100644 index 00000000000..d144c8ddb02 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/EmptyName/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["."] +} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.expected b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.expected new file mode 100644 index 00000000000..1e3d1d88a1a --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.expected @@ -0,0 +1,4 @@ +| Bar.Foo in global scope | Bar in global scope | +| Intl.CollatorOptions in global scope | Intl in global scope | +| Intl.NumberFormatOptions in global scope | Intl in global scope | +| fn in test.ts | test.ts | diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ql b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ql new file mode 100644 index 00000000000..b722f0aa2d6 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ql @@ -0,0 +1,4 @@ +import javascript + +from CanonicalName typename +select typename, typename.getParent() diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ts new file mode 100644 index 00000000000..1726e5abafb --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/test.ts @@ -0,0 +1,9 @@ +type Mapped = { +     [mk in MK]: string + }; + +export function fn(ev: Mapped) { +    const props: Mapped = { +        ...ev +    }; +} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tsconfig.json b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tsconfig.json new file mode 100644 index 00000000000..d144c8ddb02 --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tsconfig.json @@ -0,0 +1,3 @@ +{ + "include": ["."] +} diff --git a/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tst.ts b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tst.ts new file mode 100644 index 00000000000..659ab91153f --- /dev/null +++ b/javascript/ql/test/library-tests/TypeScript/RegressionTests/SemicolonInName/tst.ts @@ -0,0 +1,13 @@ +namespace Bar { + + export interface Foo { + [";x"]: number; + w: number; + } + + let x : Foo; + +} + +let y: Bar.Foo; +let z: typeof Bar[";"];