Skip to content

Commit dd0ae7e

Browse files
paldepindCopilot
andcommitted
JS: Document the native TypeScript parser and runtime requirements
Describe the pinned compiler, build and packaging workflow, AST projection, shared-API lifecycle, compatibility coverage, and performance comparisons. Raise the documented minimum Node.js version to 16.20.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9f12f7cb-26aa-405b-a1f0-9dcc239778cb
1 parent 626426c commit dd0ae7e

2 files changed

Lines changed: 225 additions & 1 deletion

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/extractor/lib/typescript/README.md‎

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,227 @@ 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-beta` 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+
The wrapper always uses the bundled TypeScript dependency.
50+
`SEMMLE_TYPESCRIPT_HOME` is no longer supported and is ignored.
51+
52+
Distribution packaging
53+
----------------------
54+
55+
The Bazel build produces two archives: `:typescript` (`javascript.zip`) contains
56+
the compiled ESM wrapper and JavaScript API; `:native` (`native.zip`) contains the
57+
matching native packages for the target CodeQL distribution:
58+
59+
| Distribution | Native packages |
60+
| ------------- | ----------------------------------------------------------------------------- |
61+
| `linux64` | `@typescript/typescript-linux-x64` |
62+
| `linux-arm64` | `@typescript/typescript-linux-arm64` |
63+
| `osx64` | `@typescript/typescript-darwin-arm64` and `@typescript/typescript-darwin-x64` |
64+
| `win64` | `@typescript/typescript-win32-x64` |
65+
66+
The build installs the selected packages explicitly with `--force --no-save`,
67+
using the pinned versions and integrities in the lockfile. Merely passing
68+
`--force` to `npm ci` still filters optional dependencies by the build host.
69+
`package-runtime.mjs` verifies the native package versions and marks executables
70+
as `0755` before ZIP creation. The common CodeQL archive contains the wrapper;
71+
the architecture archive installs native packages into
72+
`tools/typescript-parser-wrapper/node_modules/@typescript`. Both archives must
73+
be installed, with executable permissions preserved. No separate TypeScript
74+
installation is needed.
75+
76+
Run the installed-package smoke test with:
77+
78+
```sh
79+
bazel test //javascript/extractor/lib/typescript:package-test
80+
```
81+
82+
It checks all distribution selections, mismatched runtime rejection, common/arch
83+
ZIP separation and executable metadata, then installs the merged archives with
84+
the production `ripunzip` and parses TS, TSX, and malformed source through the
85+
installed JSON protocol. It runs on the host platform and uses Bazel's Node.js
86+
runtime. The Java extractor test suite also installs both ZIPs and restores
87+
executable permissions using their central-directory metadata.
88+
89+
All four distribution runtime ZIPs have been cross-assembled and inspected.
90+
Installed parsing has been verified with Node.js 18.15.0 on macOS arm64 and on
91+
macOS x64 through Rosetta. CI integration tests have also passed on Linux x64,
92+
Linux arm64, and Windows x64, including Windows 2022.
93+
94+
Native lifecycle
95+
----------------
96+
97+
A single native API/process is created lazily and reused across files and
98+
prepare-files batches. Each file still owns a separate program: AST projection
99+
and JSON serialization finish before the program is disposed, including on
100+
failure. Program disposal releases its snapshot and reference-counted source-file
101+
caches without restarting the compiler. Reset closes the API, and the next parse
102+
creates a new one. Quit, EOF, failure, and completion of single-file mode also
103+
close the API.
104+
105+
Native stderr is inherited separately from the JSON stdout protocol. Syntax
106+
diagnostics remain successful AST responses; native, I/O, and protocol failures
107+
are reported on stderr and terminate the wrapper with a nonzero exit status.
108+
Reset, quit, EOF, and failure invalidate queued prefetch work; quit also closes
109+
stdin without requiring the sender to close its pipe.
110+
111+
AST projection and regression tests
112+
-----------------------------------
113+
114+
The wrapper targets the native `typescript/sync` and `typescript/ast` entry
115+
points, using the same pinned dependency in the production ESM build.
116+
`npm test` builds the production wrapper with the pinned compiler and runs
117+
`test/wrapper.test.mjs` and `test/lifecycle.test.mjs` against its compiled output,
118+
copied with its ESM package boundary into temporary installations. No separate
119+
probe installation or legacy compiler is needed for these tests.
120+
121+
AST nodes are explicitly projected into plain objects before their native
122+
program is disposed. Syntax-kind and node-flag metadata are sent unchanged.
123+
Projection checks only the whitelisted properties applicable to each syntax kind,
124+
including optional properties, and falls back to the full whitelist for unknown
125+
kinds. `src/ast_extractor.ts` loads the private
126+
`dist/api/node/protocol.generated.js` metadata once from the locally installed,
127+
pinned TypeScript dependency.
128+
Updating TypeScript may require adapting this internal path or metadata format.
129+
Scalar getters and kind-specific booleans are handled separately.
130+
The Java converter consumes native fields directly, including `postfixToken`,
131+
`defaultType`, and import attributes, and recognizes the native `EndOfFile`
132+
spelling. Namespace classification and variable declaration kinds are derived
133+
in Java without synthetic flags or properties.
134+
135+
The tests cover AST projection, metadata, UTF-16 source positions, syntax
136+
diagnostics, TSX, repeated parses, native process reuse and disposal, the child memory
137+
budget, reset/quit/EOF, cancelled prefetch, the ignored legacy override, and fatal native
138+
failures. Run them with Node.js 16.20.0 to verify the minimum supported runtime.
139+
140+
Compatibility and regression coverage
141+
------------------------------------
142+
143+
Native interface `extends` and class `implements` elements are type references.
144+
The Java converter handles these using its existing type conversion, preserving
145+
generic arguments and qualified names without rewriting their node kinds.
146+
147+
The native parser still produces ASTs for legacy `assert { ... }` import
148+
assertions, but adds diagnostic 2880. The wrapper excludes only that diagnostic
149+
to preserve previously accepted syntax; genuine syntax diagnostics are unchanged.
150+
Import-attribute names and values are explicitly serialized. Their absence
151+
from the old wrapper's JSON was a bug: the old Java converter expected `key`/`value`,
152+
but the old filter exposed neither correctly. Java now reads the native `name`/`value`
153+
fields. The corresponding TRAP expectation was regenerated; existing QL
154+
expectations did not need changes.
155+
156+
Rescanning preserves token text and offsets while retaining the original
157+
pre-rescan token kinds on the wire. In particular, regex and template-tail tokens
158+
keep the classifications expected by existing TRAP tests.
159+
160+
Source-phase imports (`import source wasm from "./module.wasm"`) are accepted.
161+
Their declaration, imported path, and variable bindings use the existing import
162+
representation; the schema does not yet distinguish the source phase. The new
163+
`TypeScript/SourceImports` QL test covers these bindings.
164+
165+
Validation on October 19, 2026 passed the pinned-compiler build/type check,
166+
existing TSLint rules, parser compatibility and lifecycle tests,
167+
500 Java tests, 68 TypeScript QL tests, the syntax-error query test, both extraction
168+
diagnostic integration tests, and the installed-package smoke test. The
169+
internal-error integration test deliberately terminates the native child rather
170+
than relying on the old parser's stack overflow.
171+
172+
All 19 production-output regression tests also pass on Node.js 16.20.0, the
173+
minimum supported runtime.
174+
175+
For QL tests, install the newly built extractor and explicitly select it; otherwise
176+
the CLI may silently exercise an older installed extractor:
177+
178+
```sh
179+
# From the repository root, with the Java extractor build dependencies available:
180+
bazel run //javascript:install
181+
codeql resolve languages --search-path=javascript/extractor-pack:. --format=json
182+
codeql test run -j4 --search-path=javascript/extractor-pack:. \
183+
javascript/ql/test/library-tests/TypeScript \
184+
javascript/ql/test/query-tests/LanguageFeatures/SyntaxError
185+
```
186+
187+
Performance comparison
188+
----------------------
189+
190+
The current shared-API implementation was compared with refreshed `main` at
191+
`fb0ba8c8a8a` on October 21, 2026. Both ran on macOS arm64 with Node.js 18.15.0;
192+
`main` was built and run with TypeScript 5.9.2, and this branch used the pinned
193+
7.1 native compiler. These are medians of five fresh-process runs after one
194+
warm-up per variant and workload, alternating variant order:
195+
196+
| Workload | Main time | Branch time | Main combined RSS | Branch combined RSS |
197+
| --- | --- | --- | --- | --- |
198+
| 104 TypeScript fixtures (33,779 bytes) | 0.081 s | 0.120 s | 105.5 MiB | 110.4 MiB |
199+
| Five parses of TypeScript's 588,128-byte declaration file | 0.886 s | 0.992 s | 263.7 MiB | 247.2 MiB |
200+
201+
The branch takes 47.3% more time on the fixtures and 11.9% more on the declaration
202+
workload. Sampled combined RSS is 4.7% higher and 6.3% lower, respectively.
203+
Both variants parse identical inputs. Timing starts after the metadata handshake
204+
and includes prepare-files, AST extraction, serialization, and protocol responses,
205+
including the branch's first native-process startup. It excludes Java conversion
206+
and database import. RSS is the maximum sampled sum of Node and its native
207+
descendants, sampled at a nominal 5 ms interval; short-lived peaks may be missed.
208+
Current `main` includes parser cleanup absent from the older baseline below.
209+
210+
### Historical and API-lifetime comparisons
211+
212+
The initial implementation created a native process per file. On macOS arm64
213+
with Node.js 18.15.0, a comparison against the pre-migration wrapper from
214+
`1971c368e1d` compiled with TypeScript 5.9.2 gave these medians over three runs:
215+
216+
| Workload | 5.9.2 time | 7.1 time | 5.9.2 combined RSS | 7.1 combined RSS |
217+
| --------------------------------------------------------- | ---------- | -------- | ------------------ | ---------------- |
218+
| 103 TypeScript fixtures (33,589 bytes) | 0.237 s | 1.349 s | 124.1 MiB | 113.9 MiB |
219+
| Five parses of TypeScript's 588,128-byte declaration file | 0.991 s | 1.116 s | 269.3 MiB | 249.1 MiB |
220+
221+
Timing covers prepare-files, extraction, serialization, and protocol responses
222+
after the metadata handshake, not Java conversion or database import. Combined
223+
RSS is the maximum sampled sum of Node and its native descendants, sampled every
224+
5 ms; short-lived peaks may be missed. The per-file native process costs about
225+
5.7x on tiny files and 1.13x on the declaration workload, while measured memory is
226+
slightly lower.
227+
228+
A follow-up comparison on October 21, 2026, using the same platform and Node.js
229+
version, measured the per-file and shared-API lifetimes directly (medians of three
230+
runs):
231+
232+
| Workload | Per-file API time | Shared API time | Per-file combined RSS | Shared combined RSS |
233+
| --- | --- | --- | --- | --- |
234+
| 104 TypeScript fixtures (33,779 bytes) | 1.017 s | 0.118 s | 115.8 MiB | 109.4 MiB |
235+
| Five parses of the same declaration file | 1.044 s | 0.985 s | 250.2 MiB | 260.2 MiB |
236+
237+
AST responses were byte-identical between lifetimes. A longer shared-API run on
238+
Node.js 26.7.0 processed 50,000 distinct generated TypeScript files: native RSS
239+
was 27.9 MiB after 1,000 files and 30.4 MiB after 50,000, while combined RSS
240+
stabilized around 176 MiB. Another run completed 500 parses of the declaration
241+
file with peak native RSS of 56.5 MiB. These workloads showed no unbounded
242+
per-file retention, so the wrapper reuses the API without a fixed batch limit.

0 commit comments

Comments
 (0)