Skip to content

Commit 8af740c

Browse files
paldepindCopilot
andcommitted
JavaScript: migrate the TypeScript parser to native 7.1
Preserve the Java-facing extraction protocol while adopting the pinned native TypeScript API and platform-specific runtimes. Add compatibility, lifecycle, packaging, and syntax regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 86598cdc-5945-4d8e-9ea4-eeb4eb4356ba Copilot-Session: 2e49a953-2280-4a74-839a-abdfa6fb61f0
1 parent 1971c36 commit 8af740c

26 files changed

Lines changed: 2560 additions & 1529 deletions

File tree

‎docs/codeql/codeql-overview/system-requirements.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ For extraction of compiled languages on Windows:
3030

3131
For TypeScript extraction on all platforms:
3232

33-
- Node.js 14 or higher must be installed and available on the ``PATH`` as ``node``.
33+
- Node.js 16.20.0 or higher must be installed and available on the ``PATH`` as ``node``.
3434

3535
For Python extraction:
3636

‎javascript/BUILD.bazel‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,9 @@ codeql_pack(
3333
"//javascript/extractor:tools-extractor",
3434
"//javascript/resources",
3535
],
36-
zips = {"//javascript/extractor/lib/typescript": "tools"},
36+
arch_overrides = ["tools/typescript-parser-wrapper/node_modules/@typescript"],
37+
zips = {
38+
"//javascript/extractor/lib/typescript": "tools",
39+
"//javascript/extractor/lib/typescript:native": "tools/typescript-parser-wrapper/node_modules/@typescript",
40+
},
3741
)

‎javascript/extractor/lib/typescript/BUILD.bazel‎

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
# Builds a zip file of the compiled typscript-parser-wrapper and its dependencies.
1+
load("@rules_python//python:defs.bzl", "py_test")
2+
load("//misc/bazel:os.bzl", "codeql_platform_select")
3+
load("//misc/bazel:pkg.bzl", "codeql_pack", "codeql_pack_group")
4+
5+
# Keep the JavaScript API common, but ship native executables in the arch package.
26
genrule(
3-
name = "typescript",
7+
name = "build",
48
srcs = [
59
"tsconfig.json",
610
"package.json",
711
"package-lock.json",
12+
"package-runtime.mjs",
813
] + glob(["src/**"]),
9-
outs = ["javascript.zip"],
14+
outs = [
15+
"javascript.zip",
16+
"native.zip",
17+
],
1018
cmd = "\n".join([
1119
# the original working directory is not preserved anywhere, but needs to be accessible, as
1220
# all paths are relative to this
@@ -25,20 +33,35 @@ genrule(
2533
"rm -rf $(RULEDIR)/inputs",
2634
"cp -L -R $$(dirname $(execpath package.json)) $(RULEDIR)/inputs",
2735
"cd $(RULEDIR)/inputs",
28-
"$$NPM install",
36+
"$$NPM ci",
2937
"$$NPM run build",
3038
"rm -rf node_modules",
31-
# Install again with only runtime deps
32-
"$$NPM install --prod",
39+
"DISTRIBUTION=",
40+
]) + codeql_platform_select(
41+
linux64 = "linux64",
42+
linux_arm64 = "linux-arm64",
43+
osx64 = "osx64",
44+
win64 = "win64",
45+
) + "\n" + "\n".join([
46+
"$$NPM ci --omit=dev --omit=optional --ignore-scripts",
47+
# Optional dependencies are host-filtered even with --force. Install the
48+
# selected packages explicitly, retaining the lockfile's versions/integrities.
49+
"$$NPM install --omit=dev --ignore-scripts --no-save --force $$(node package-runtime.mjs --packages $$DISTRIBUTION)",
50+
"node package-runtime.mjs $$DISTRIBUTION",
3351
"mv node_modules build/",
52+
# Node needs the ESM package boundary in the installed wrapper too.
53+
"cp package.json build/",
3454
"mkdir -p typescript-parser-wrapper",
3555
"mv build/* typescript-parser-wrapper",
36-
"OUT=$$BAZEL_ROOT/$@",
56+
"OUT=$$BAZEL_ROOT/$(execpath javascript.zip)",
57+
"NATIVE_OUT=$$BAZEL_ROOT/$(execpath native.zip)",
3758
"case $$OSTYPE in",
38-
" cygwin|msys|win32) OUT=$$(cygpath -w $$OUT);;",
59+
" cygwin|msys|win32) OUT=$$(cygpath -w $$OUT); NATIVE_OUT=$$(cygpath -w $$NATIVE_OUT);;",
3960
"esac",
4061
"",
4162
"$$BAZEL_ROOT/$(execpath @bazel_tools//tools/zip:zipper) cC $$OUT $$(find typescript-parser-wrapper -name '*' -print)",
63+
"cd native",
64+
"$$BAZEL_ROOT/$(execpath @bazel_tools//tools/zip:zipper) cC $$NATIVE_OUT $$(find typescript-* -print)",
4265
"rm -rf $$TEMP",
4366
]),
4467
tools = [
@@ -48,3 +71,59 @@ genrule(
4871
],
4972
visibility = ["//visibility:public"],
5073
)
74+
75+
filegroup(
76+
name = "typescript",
77+
srcs = ["javascript.zip"],
78+
visibility = ["//visibility:public"],
79+
)
80+
81+
filegroup(
82+
name = "native",
83+
srcs = ["native.zip"],
84+
visibility = ["//visibility:public"],
85+
)
86+
87+
codeql_pack(
88+
name = "package-test-pack",
89+
srcs = [],
90+
arch_overrides = ["tools/typescript-parser-wrapper/node_modules/@typescript"],
91+
install_dest = None,
92+
pack_prefix = "",
93+
zips = {
94+
":native": "tools/typescript-parser-wrapper/node_modules/@typescript",
95+
":typescript": "tools",
96+
},
97+
)
98+
99+
codeql_pack_group(
100+
name = "package-test-distribution",
101+
srcs = [":package-test-pack"],
102+
skip_installer = True,
103+
)
104+
105+
py_test(
106+
name = "package-test",
107+
size = "small",
108+
srcs = ["test/package_test.py"],
109+
data = [
110+
"package-runtime.mjs",
111+
":native",
112+
":package-test-distribution-arch-zip",
113+
":package-test-distribution-common-zip",
114+
":typescript",
115+
"//misc/ripunzip",
116+
"@nodejs//:node_bin",
117+
],
118+
env = {
119+
"NODE_BIN": "$(rlocationpath @nodejs//:node_bin)",
120+
"PACKAGE_RUNTIME": "$(rlocationpath package-runtime.mjs)",
121+
"RIPUNZIP": "$(rlocationpath //misc/ripunzip)",
122+
"TS_ARCH_ZIP": "$(rlocationpath :package-test-distribution-arch-zip)",
123+
"TS_COMMON_ZIP": "$(rlocationpath :package-test-distribution-common-zip)",
124+
"TS_NATIVE_ZIP": "$(rlocationpath :native)",
125+
"TS_WRAPPER_ZIP": "$(rlocationpath :typescript)",
126+
},
127+
main = "test/package_test.py",
128+
deps = ["@rules_python//python/runfiles"],
129+
)

‎javascript/extractor/lib/typescript/README.md‎

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,195 @@ To debug the parser script:
1616

1717
2. Open VSCode and choose "Debug: Attach to Node process" from the command palette, and
1818
choose the process that looks like the parser-wrapper.
19+
20+
Building and running
21+
--------------------
22+
23+
The wrapper uses the exact `typescript@7.1.0-dev.20261015.1` package for both
24+
compilation/type checking and the native runtime. Its integrity and those of all
25+
platform-specific optional dependencies are recorded in `package-lock.json`.
26+
27+
```sh
28+
npm ci
29+
npm run build
30+
npm test
31+
node build/main.js --version
32+
```
33+
34+
Node.js 16.20.0 is the minimum supported runtime, matching the pinned TypeScript
35+
package's engine requirement. The existing Bazel toolchain uses Node.js 18.15.0
36+
and does not need an upgrade. Build scripts invoke TypeScript's
37+
`lib/tsc.js` launcher explicitly because these Node.js versions cannot load its
38+
extensionless ESM `bin/tsc` entry point. The installed wrapper must include its
39+
`package.json` to retain the ESM package boundary.
40+
41+
The existing TSLint rules require the legacy JavaScript compiler API, not the
42+
native TypeScript 7 API. If compatible TSLint tooling is not already installed,
43+
run it separately without replacing the wrapper's build/runtime dependency:
44+
45+
```sh
46+
npm exec --yes --package=tslint@6.1.3 --package=typescript@5.9.2 -- tslint --project .
47+
```
48+
49+
`SEMMLE_TYPESCRIPT_HOME` still overrides the runtime dependency. It must point to
50+
the installed TypeScript package directory (containing `package.json`), with the
51+
native `typescript/sync` and `typescript/ast` entry points and matching executable.
52+
A legacy 5.x module or a directory from which the bundled package merely happens
53+
to be resolvable is not a valid override.
54+
55+
Distribution packaging
56+
----------------------
57+
58+
The Bazel build produces two archives: `:typescript` (`javascript.zip`) contains
59+
the compiled ESM wrapper and JavaScript API; `:native` (`native.zip`) contains the
60+
matching native packages for the target CodeQL distribution:
61+
62+
| Distribution | Native packages |
63+
| --- | --- |
64+
| `linux64` | `@typescript/typescript-linux-x64` |
65+
| `linux-arm64` | `@typescript/typescript-linux-arm64` |
66+
| `osx64` | `@typescript/typescript-darwin-arm64` and `@typescript/typescript-darwin-x64` |
67+
| `win64` | `@typescript/typescript-win32-x64` |
68+
69+
The build installs the selected packages explicitly with `--force --no-save`,
70+
using the pinned versions and integrities in the lockfile. Merely passing
71+
`--force` to `npm ci` still filters optional dependencies by the build host.
72+
`package-runtime.mjs` verifies the native package versions and marks executables
73+
as `0755` before ZIP creation. The common CodeQL archive contains the wrapper;
74+
the architecture archive installs native packages into
75+
`tools/typescript-parser-wrapper/node_modules/@typescript`. Both archives must
76+
be installed, with executable permissions preserved. No separate TypeScript
77+
installation is needed.
78+
79+
Run the installed-package smoke test with:
80+
81+
```sh
82+
bazel test //javascript/extractor/lib/typescript:package-test
83+
```
84+
85+
It checks all distribution selections, mismatched runtime rejection, common/arch
86+
ZIP separation and executable metadata, then installs the merged archives with
87+
the production `ripunzip` and parses TS, TSX, and malformed source through the
88+
installed JSON protocol. It runs on the host platform and uses Bazel's Node.js
89+
runtime. The Java extractor test suite also installs both ZIPs and restores
90+
executable permissions using their central-directory metadata.
91+
92+
All four distribution runtime ZIPs have been cross-assembled and inspected.
93+
Installed parsing has been verified with Node.js 18.15.0 on macOS arm64 and on
94+
macOS x64 through Rosetta. Linux x64, Linux arm64, and Windows x64 still require
95+
execution on matching hosts; cross-assembling their ZIPs is not execution coverage.
96+
97+
Native lifecycle and memory
98+
---------------------------
99+
100+
Each file owns a fresh native API/process. AST projection and JSON serialization
101+
finish before the program is disposed and the API is closed, including on
102+
failure. No native process or compiler cache is retained between files, during
103+
stdout backpressure, or across reset/quit. This deliberately favors bounded
104+
resource lifetime over reusing a compiler process; the measured cost is described
105+
below.
106+
107+
`SEMMLE_TYPESCRIPT_MEMORY_THRESHOLD` remains a positive megabyte budget, defaulting
108+
to 1000 when the Java extractor does not supply it. At native-process creation,
109+
the wrapper subtracts Node's current RSS (not just its V8 heap) from that budget
110+
and passes the remaining bytes, with a minimum of one, as the child's
111+
`GOMEMLIMIT`. The parent's original `GOMEMLIMIT` is restored after spawning.
112+
This is a Go garbage-collector soft limit, not a hard combined RSS ceiling:
113+
Node allocations during extraction, native non-Go allocations, and oversized
114+
individual files can exceed the budget. Closing the native process after every
115+
file prevents native caches and heap capacity from accumulating across files.
116+
Node RSS exceeding the budget produces a warning on stderr.
117+
118+
Native stderr is inherited separately from the JSON stdout protocol. Syntax
119+
diagnostics remain successful AST responses; native, I/O, and protocol failures
120+
are reported on stderr and terminate the wrapper with a nonzero exit status.
121+
Reset, quit, EOF, and failure invalidate queued prefetch work; quit also closes
122+
stdin without requiring the sender to close its pipe.
123+
124+
AST projection and regression tests
125+
-----------------------------------
126+
127+
The wrapper targets the native `typescript/sync` and `typescript/ast` entry
128+
points, using the same pinned dependency in the production ESM build.
129+
`npm test` builds the production wrapper with the pinned compiler and runs
130+
`test/wrapper.test.mjs` and `test/lifecycle.test.mjs` against its compiled output,
131+
copied with its ESM package boundary into temporary installations. No separate
132+
probe installation or legacy compiler is needed for these tests.
133+
134+
AST nodes are explicitly projected into plain objects before their native
135+
program is disposed. The projection preserves the Java converter's property
136+
names, including optional member tokens, type-parameter defaults, import
137+
attributes, and namespace flags. EOF metadata retains the `EndOfFileToken`
138+
spelling; the native `TypeOfExpression`, `TypeOfKeyword`, `InstanceOfKeyword`,
139+
and `KeyOfKeyword` names already match the Java converter.
140+
141+
The tests cover AST projection, metadata, UTF-16 source positions, syntax
142+
diagnostics, TSX, repeated parses, native process disposal, the child memory
143+
budget, reset/quit/EOF, cancelled prefetch, runtime overrides, and fatal native
144+
failures. Run them with Node.js 16.20.0 to verify the minimum supported runtime.
145+
146+
Compatibility and regression coverage
147+
------------------------------------
148+
149+
Native interface `extends` and class `implements` elements are type references.
150+
The wrapper projects them into the expression-shaped heritage nodes consumed by
151+
the Java converter, preserving generic arguments and qualified names.
152+
153+
The native parser still produces ASTs for legacy `assert { ... }` import
154+
assertions, but adds diagnostic 2880. The wrapper excludes only that diagnostic
155+
to preserve previously accepted syntax; genuine syntax diagnostics are unchanged.
156+
Import-attribute keys and values are now explicitly serialized. Their absence
157+
from the old wrapper's JSON was a bug: the Java converter expects `key`/`value`,
158+
but the old filter exposed neither correctly. The corresponding TRAP expectation
159+
was regenerated; existing QL expectations did not need changes.
160+
161+
Rescanning preserves token text and offsets while retaining the original
162+
pre-rescan token kinds on the wire. In particular, regex and template-tail tokens
163+
keep the classifications expected by existing TRAP tests.
164+
165+
Source-phase imports (`import source wasm from "./module.wasm"`) are accepted.
166+
Their declaration, imported path, and variable bindings use the existing import
167+
representation; the schema does not yet distinguish the source phase. The new
168+
`TypeScript/SourceImports` QL test covers these bindings.
169+
170+
Validation on October 19, 2026 passed the pinned-compiler build/type check,
171+
existing TSLint rules, parser compatibility and lifecycle tests,
172+
500 Java tests, 68 TypeScript QL tests, the syntax-error query test, both extraction
173+
diagnostic integration tests, and the installed-package smoke test. The
174+
internal-error integration test deliberately terminates the native child rather
175+
than relying on the old parser's stack overflow.
176+
177+
All 19 production-output regression tests also pass on Node.js 16.20.0, the
178+
minimum supported runtime.
179+
180+
For QL tests, install the newly built extractor and explicitly select it; otherwise
181+
the CLI may silently exercise an older installed extractor:
182+
183+
```sh
184+
# From the repository root, with the Java extractor build dependencies available:
185+
bazel run //javascript:install
186+
codeql resolve languages --search-path=javascript/extractor-pack:. --format=json
187+
codeql test run -j4 --search-path=javascript/extractor-pack:. \
188+
javascript/ql/test/library-tests/TypeScript \
189+
javascript/ql/test/query-tests/LanguageFeatures/SyntaxError
190+
```
191+
192+
Performance comparison
193+
----------------------
194+
195+
On macOS arm64 with Node.js 18.15.0, a comparison against the pre-migration
196+
wrapper from `1971c368e1d` compiled with TypeScript 5.9.2 gave these medians over three
197+
runs:
198+
199+
| Workload | 5.9.2 time | 7.1 time | 5.9.2 combined RSS | 7.1 combined RSS |
200+
| --- | --- | --- | --- | --- |
201+
| 103 TypeScript fixtures (33,589 bytes) | 0.237 s | 1.349 s | 124.1 MiB | 113.9 MiB |
202+
| Five parses of TypeScript's 588,128-byte declaration file | 0.991 s | 1.116 s | 269.3 MiB | 249.1 MiB |
203+
204+
Timing covers prepare-files, extraction, serialization, and protocol responses
205+
after the metadata handshake, not Java conversion or database import. Combined
206+
RSS is the maximum sampled sum of Node and its native descendants, sampled every
207+
5 ms; short-lived peaks may be missed. The per-file native process costs about
208+
5.7x on tiny files and 1.13x on the declaration workload, while measured memory is
209+
slightly lower. The bounded lifecycle is retained deliberately; this migration
210+
does not claim a speedup.

0 commit comments

Comments
 (0)