@@ -16,3 +16,227 @@ To debug the parser script:
1616
17172 . 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