@@ -5,6 +5,7 @@ import { describe, expect, it } from 'vitest';
55import {
66 classifyNativeDrops ,
77 formatDropExtensionSummary ,
8+ LANGUAGE_REGISTRY ,
89 NATIVE_SUPPORTED_EXTENSIONS ,
910} from '../../src/domain/parser.js' ;
1011
@@ -203,3 +204,84 @@ describe('NATIVE_SUPPORTED_EXTENSIONS drift guard', () => {
203204 ) . toEqual ( [ ] ) ;
204205 } ) ;
205206} ) ;
207+
208+ /**
209+ * Parity gate for `LANGUAGE_REGISTRY` ↔ `NATIVE_SUPPORTED_EXTENSIONS`.
210+ *
211+ * Acceptance criterion from #1071 (tracked in #1121): a CI gate prevents
212+ * future drift between the JS `LANGUAGE_REGISTRY` and the Rust extractor
213+ * coverage. The existing drift guard above covers
214+ * `NATIVE_SUPPORTED_EXTENSIONS ↔ parser_registry.rs`, but the link from
215+ * `LANGUAGE_REGISTRY` (the source of truth for languages we support at all)
216+ * to `NATIVE_SUPPORTED_EXTENSIONS` (the hand-maintained mirror of the Rust
217+ * enum) had no test — silently adding a WASM-only language would degrade the
218+ * native engine without flagging the regression.
219+ *
220+ * This test closes that gap. Every extension declared in `LANGUAGE_REGISTRY`
221+ * must either:
222+ * 1. Be present in `NATIVE_SUPPORTED_EXTENSIONS` (i.e. a Rust extractor
223+ * exists), or
224+ * 2. Appear in `WASM_ONLY_ALLOWLIST` below, with a comment explaining why
225+ * the language is intentionally WASM-only.
226+ *
227+ * Adding an extension to the allowlist is a deliberate choice: prefer porting
228+ * the extractor to Rust. The allowlist exists so a contributor can land a
229+ * WASM-only grammar (e.g. while a Rust port is in flight) without bypassing
230+ * the gate entirely, but every entry should have a tracking issue.
231+ */
232+ describe ( 'LANGUAGE_REGISTRY ↔ NATIVE_SUPPORTED_EXTENSIONS parity' , ( ) => {
233+ // Extensions intentionally left WASM-only. Currently empty: every language
234+ // in `LANGUAGE_REGISTRY` has a corresponding Rust extractor. If you must
235+ // add an entry, include a comment with the language id and the issue
236+ // tracking the Rust port.
237+ const WASM_ONLY_ALLOWLIST : ReadonlySet < string > = new Set < string > ( ) ;
238+
239+ it ( 'every LANGUAGE_REGISTRY extension has a Rust extractor or is on the allowlist' , ( ) => {
240+ const registryExts = new Set < string > ( ) ;
241+ for ( const entry of LANGUAGE_REGISTRY ) {
242+ for ( const ext of entry . extensions ) {
243+ registryExts . add ( ext . toLowerCase ( ) ) ;
244+ }
245+ }
246+ const missingFromNative = [ ...registryExts ]
247+ . filter ( ( ext ) => ! NATIVE_SUPPORTED_EXTENSIONS . has ( ext ) )
248+ . filter ( ( ext ) => ! WASM_ONLY_ALLOWLIST . has ( ext ) )
249+ . sort ( ) ;
250+ expect (
251+ missingFromNative ,
252+ `LANGUAGE_REGISTRY extensions without a Rust extractor (and not on WASM_ONLY_ALLOWLIST): ${ missingFromNative . join (
253+ ', ' ,
254+ ) } . Either port the extractor to Rust and add the extension to NATIVE_SUPPORTED_EXTENSIONS, or add it to WASM_ONLY_ALLOWLIST with a justification.`,
255+ ) . toEqual ( [ ] ) ;
256+ } ) ;
257+
258+ it ( 'WASM_ONLY_ALLOWLIST does not list extensions that already have a Rust extractor' , ( ) => {
259+ // Catches stale allowlist entries: once a language is ported to Rust the
260+ // allowlist line should be deleted, not left behind as dead config.
261+ const stale = [ ...WASM_ONLY_ALLOWLIST ]
262+ . map ( ( ext ) => ext . toLowerCase ( ) )
263+ . filter ( ( ext ) => NATIVE_SUPPORTED_EXTENSIONS . has ( ext ) ) ;
264+ expect (
265+ stale ,
266+ `WASM_ONLY_ALLOWLIST entries that already have a Rust extractor — remove them: ${ stale . join ( ', ' ) } ` ,
267+ ) . toEqual ( [ ] ) ;
268+ } ) ;
269+
270+ it ( 'WASM_ONLY_ALLOWLIST only references extensions that LANGUAGE_REGISTRY declares' , ( ) => {
271+ // Catches typos and dead entries: an allowlist line for an extension no
272+ // longer in the registry is silently useless.
273+ const registryExts = new Set < string > ( ) ;
274+ for ( const entry of LANGUAGE_REGISTRY ) {
275+ for ( const ext of entry . extensions ) {
276+ registryExts . add ( ext . toLowerCase ( ) ) ;
277+ }
278+ }
279+ const orphans = [ ...WASM_ONLY_ALLOWLIST ]
280+ . map ( ( ext ) => ext . toLowerCase ( ) )
281+ . filter ( ( ext ) => ! registryExts . has ( ext ) ) ;
282+ expect (
283+ orphans ,
284+ `WASM_ONLY_ALLOWLIST entries not declared in LANGUAGE_REGISTRY — likely a typo: ${ orphans . join ( ', ' ) } ` ,
285+ ) . toEqual ( [ ] ) ;
286+ } ) ;
287+ } ) ;
0 commit comments