rbtr-lang-javascript 2026.9.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. rbtr_lang_javascript/__init__.py +1 -0
  2. rbtr_lang_javascript/javascript.scm +4 -0
  3. rbtr_lang_javascript/plugin.py +190 -0
  4. rbtr_lang_javascript/py.typed +0 -0
  5. rbtr_lang_javascript/shared.scm +29 -0
  6. rbtr_lang_javascript/tests/__init__.py +0 -0
  7. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_edges_match_snapshot[javascript].json +4 -0
  8. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_edges_match_snapshot[tsx].json +3 -0
  9. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_edges_match_snapshot[typescript].json +6 -0
  10. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_extraction_matches_snapshot[javascript].json +420 -0
  11. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_extraction_matches_snapshot[tsx].json +211 -0
  12. rbtr_lang_javascript/tests/__snapshots__/test_samples/test_extraction_matches_snapshot[typescript].json +610 -0
  13. rbtr_lang_javascript/tests/cases_docstrings.py +225 -0
  14. rbtr_lang_javascript/tests/cases_extraction.py +429 -0
  15. rbtr_lang_javascript/tests/samples/javascript/config.js +2 -0
  16. rbtr_lang_javascript/tests/samples/javascript/javascript.js +46 -0
  17. rbtr_lang_javascript/tests/samples/javascript/styles.css +3 -0
  18. rbtr_lang_javascript/tests/samples/tsx/labels.ts +2 -0
  19. rbtr_lang_javascript/tests/samples/tsx/tsx.tsx +33 -0
  20. rbtr_lang_javascript/tests/samples/typescript/config.ts +2 -0
  21. rbtr_lang_javascript/tests/samples/typescript/types.ts +2 -0
  22. rbtr_lang_javascript/tests/samples/typescript/typescript.ts +68 -0
  23. rbtr_lang_javascript/tests/test_docstrings.py +51 -0
  24. rbtr_lang_javascript/tests/test_extraction.py +80 -0
  25. rbtr_lang_javascript/tests/test_samples.py +95 -0
  26. rbtr_lang_javascript/typescript.scm +36 -0
  27. rbtr_lang_javascript/variables.scm +54 -0
  28. rbtr_lang_javascript-2026.9.0.dev0.dist-info/METADATA +74 -0
  29. rbtr_lang_javascript-2026.9.0.dev0.dist-info/RECORD +32 -0
  30. rbtr_lang_javascript-2026.9.0.dev0.dist-info/WHEEL +4 -0
  31. rbtr_lang_javascript-2026.9.0.dev0.dist-info/entry_points.txt +5 -0
  32. rbtr_lang_javascript-2026.9.0.dev0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,225 @@
1
+ """JavaScript / TypeScript docstring-extraction test cases.
2
+
3
+ Each `@case` returns `(lang, source, symbol_name, snippet)` consumed by
4
+ `test_docstrings.py`; tags drive the documented/undocumented assertion.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pytest_cases import case
10
+
11
+ type DocstringCase = tuple[str, str, str, str]
12
+
13
+
14
+ @case(tags=["documented", "canonical", "exterior_doc"])
15
+ def case_js_jsdoc_on_function() -> DocstringCase:
16
+ """Canonical JSDoc above a function declaration."""
17
+ src = """\
18
+ /** Return a friendly greeting. */
19
+ function greet() {}
20
+ """
21
+ return "javascript", src, "greet", "Return a friendly greeting"
22
+
23
+
24
+ @case(tags=["documented", "canonical", "exterior_doc"])
25
+ def case_js_jsdoc_on_class() -> DocstringCase:
26
+ """Canonical JSDoc above a class declaration."""
27
+ src = """\
28
+ /** A widget. */
29
+ class Widget {}
30
+ """
31
+ return "javascript", src, "Widget", "A widget"
32
+
33
+
34
+ @case(tags=["documented", "canonical", "exterior_doc"])
35
+ def case_js_jsdoc_on_arrow_function() -> DocstringCase:
36
+ """Arrow-function assignment — the `@function` capture
37
+ lands on the `lexical_declaration`, so JSDoc attaches via
38
+ the walk on that node's prev_named_sibling.
39
+ """
40
+ src = """\
41
+ /** Increment. */
42
+ const inc = (x) => x + 1;
43
+ """
44
+ return "javascript", src, "inc", "Increment"
45
+
46
+
47
+ @case(tags=["documented", "edge_case", "exterior_doc"])
48
+ def case_js_multiline_jsdoc() -> DocstringCase:
49
+ """Multi-line JSDoc with leading `*` gutter."""
50
+ src = """\
51
+ /**
52
+ * Compute a checksum over *data*.
53
+ *
54
+ * The algorithm is CRC32 — explained below.
55
+ */
56
+ function checksum(data) { return 0; }
57
+ """
58
+ return "javascript", src, "checksum", "The algorithm is CRC32"
59
+
60
+
61
+ @case(tags=["documented", "edge_case", "exterior_doc"])
62
+ def case_js_banner_comment() -> DocstringCase:
63
+ """`/*! ... */` banner comments are common in bundled UMD
64
+ libs; the grammar lands them as `comment` nodes and we
65
+ attach them — the benchmark will say whether that hurts.
66
+ """
67
+ src = """\
68
+ /*! (c) 2024 Acme. */
69
+ function publicApi() {}
70
+ """
71
+ return "javascript", src, "publicApi", "(c) 2024 Acme"
72
+
73
+
74
+ @case(tags=["documented", "unconventional", "exterior_doc"])
75
+ def case_js_line_comment_run() -> DocstringCase:
76
+ """`//` comment runs used as docs — common in TS-first
77
+ code where JSDoc is syntactically less convenient.
78
+ """
79
+ src = """\
80
+ // First description line.
81
+ // Second description line.
82
+ function documented() {}
83
+ """
84
+ return "javascript", src, "documented", "First description line"
85
+
86
+
87
+ @case(tags=["undocumented", "no_docs"])
88
+ def case_js_function_without_doc() -> DocstringCase:
89
+ """Plain function, no comments."""
90
+ src = """\
91
+ function bare() {}
92
+ """
93
+ return "javascript", src, "bare", "PHANTOM_DOC_TEXT_SHOULD_NEVER_APPEAR"
94
+
95
+
96
+ @case(tags=["undocumented", "boundary_not_attached"])
97
+ def case_js_jsdoc_detached_by_blank_line() -> DocstringCase:
98
+ """A blank line between the JSDoc block and the function
99
+ breaks attachment.
100
+ """
101
+ src = """\
102
+ /** Stale JSDoc — not attached. */
103
+
104
+ function stale() {}
105
+ """
106
+ return "javascript", src, "stale", "Stale JSDoc"
107
+
108
+
109
+ @case(tags=["undocumented", "invalid"])
110
+ def case_js_jsdoc_above_import_does_not_attach_to_class() -> DocstringCase:
111
+ """JSDoc above an `import` stays on the import line. A
112
+ class several statements later must *not* inherit it.
113
+ Imports are excluded from attachment by design (see
114
+ `treesitter.extract_symbols`).
115
+ """
116
+ src = """\
117
+ /** Nonsense JSDoc above import. */
118
+ import { x } from './x';
119
+
120
+ class Real {}
121
+ """
122
+ return "javascript", src, "Real", "Nonsense JSDoc"
123
+
124
+
125
+ @case(tags=["documented", "canonical", "exterior_doc"])
126
+ def case_ts_jsdoc_on_function() -> DocstringCase:
127
+ """Canonical JSDoc above a typed function declaration."""
128
+ src = """\
129
+ /** Return the length of *s*. */
130
+ function len(s: string): number { return s.length; }
131
+ """
132
+ return "typescript", src, "len", "Return the length"
133
+
134
+
135
+ @case(tags=["documented", "canonical", "exterior_doc"])
136
+ def case_ts_jsdoc_on_class() -> DocstringCase:
137
+ """JSDoc above a TypeScript class — grammar uses
138
+ `type_identifier` for the class name.
139
+ """
140
+ src = """\
141
+ /** A widget. */
142
+ class Widget {}
143
+ """
144
+ return "typescript", src, "Widget", "A widget"
145
+
146
+
147
+ @case(tags=["documented", "canonical", "exterior_doc"])
148
+ def case_ts_jsdoc_on_arrow_function() -> DocstringCase:
149
+ """Arrow-function assignment with a type annotation."""
150
+ src = """\
151
+ /** Increment. */
152
+ const inc: (x: number) => number = (x) => x + 1;
153
+ """
154
+ return "typescript", src, "inc", "Increment"
155
+
156
+
157
+ @case(tags=["documented", "edge_case", "exterior_doc"])
158
+ def case_ts_jsdoc_on_generic_function() -> DocstringCase:
159
+ """Generic type parameters between name and arguments."""
160
+ src = """\
161
+ /** Identity. */
162
+ function identity<T>(x: T): T { return x; }
163
+ """
164
+ return "typescript", src, "identity", "Identity"
165
+
166
+
167
+ @case(tags=["documented", "edge_case", "exterior_doc"])
168
+ def case_ts_multiline_jsdoc_with_tags() -> DocstringCase:
169
+ """Multi-line JSDoc with `@param` / `@returns` tags."""
170
+ src = """\
171
+ /**
172
+ * Compute a hash.
173
+ *
174
+ * @param data bytes to hash
175
+ * @returns hex digest
176
+ */
177
+ function hash(data: Uint8Array): string { return ''; }
178
+ """
179
+ return "typescript", src, "hash", "@returns hex digest"
180
+
181
+
182
+ @case(tags=["documented", "unconventional", "exterior_doc"])
183
+ def case_ts_line_comment_run() -> DocstringCase:
184
+ """`//` comment runs used as docs, common in TS-heavy
185
+ codebases that avoid JSDoc because types are already in
186
+ the signature.
187
+ """
188
+ src = """\
189
+ // Describe the value.
190
+ // Useful in calling code.
191
+ function describe(x: number): string { return String(x); }
192
+ """
193
+ return "typescript", src, "describe", "Describe the value"
194
+
195
+
196
+ @case(tags=["undocumented", "no_docs"])
197
+ def case_ts_function_without_doc() -> DocstringCase:
198
+ """Plain TS function."""
199
+ src = """\
200
+ function bare(x: number): number { return x; }
201
+ """
202
+ return "typescript", src, "bare", "PHANTOM_DOC_TEXT_SHOULD_NEVER_APPEAR"
203
+
204
+
205
+ @case(tags=["undocumented", "boundary_not_attached"])
206
+ def case_ts_jsdoc_detached_by_blank_line() -> DocstringCase:
207
+ """Blank line breaks attachment for TS too."""
208
+ src = """\
209
+ /** Stale JSDoc. */
210
+
211
+ function stale(): void {}
212
+ """
213
+ return "typescript", src, "stale", "Stale JSDoc"
214
+
215
+
216
+ @case(tags=["undocumented", "invalid"])
217
+ def case_ts_jsdoc_above_import() -> DocstringCase:
218
+ """JSDoc above `import` does not attach to a later class."""
219
+ src = """\
220
+ /** Nonsense JSDoc. */
221
+ import { x } from './x';
222
+
223
+ class Real {}
224
+ """
225
+ return "typescript", src, "Real", "Nonsense JSDoc"
@@ -0,0 +1,429 @@
1
+ """JavaScript / TypeScript / TSX extraction test cases.
2
+
3
+ Each `@case` returns test data consumed by `test_extraction.py` via
4
+ `pytest-cases`. See the plugin docstring for the source→chunk mapping.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import pytest
10
+ from pytest_cases import case
11
+
12
+ type SymbolCase = tuple[str, str, list[tuple[str, str, str]]]
13
+ type ImportCase = tuple[str, str, dict[str, str]]
14
+ type MultiImportCase = tuple[str, str, int, list[dict[str, str]]]
15
+ type MixedCase = tuple[str, str, set[str], list[tuple[str, str]]]
16
+
17
+ _xfail_nested = pytest.mark.xfail(
18
+ reason="nested/chained destructuring unsupported — no query-only recursion",
19
+ strict=True,
20
+ )
21
+
22
+
23
+ @case(tags=["symbol"])
24
+ def case_js_function_declaration() -> SymbolCase:
25
+ """function greet() {}."""
26
+ return "javascript", "function greet() {}\n", [("function", "greet", "")]
27
+
28
+
29
+ @case(tags=["symbol"])
30
+ def case_js_arrow_function() -> SymbolCase:
31
+ """const add = (a, b) => a + b."""
32
+ return "javascript", "const add = (a, b) => a + b;\n", [("function", "add", "")]
33
+
34
+
35
+ @case(tags=["symbol"])
36
+ def case_js_arrow_function_block() -> SymbolCase:
37
+ """Arrow function with block body."""
38
+ src = """\
39
+ const fetch = () => {
40
+ return data;
41
+ };
42
+ """
43
+ return "javascript", src, [("function", "fetch", "")]
44
+
45
+
46
+ @case(tags=["symbol"])
47
+ def case_js_multiple_functions() -> SymbolCase:
48
+ """Multiple function forms."""
49
+ src = """\
50
+ function a() {}
51
+ function b() {}
52
+ const c = () => {};
53
+ """
54
+ return (
55
+ "javascript",
56
+ src,
57
+ [
58
+ ("function", "a", ""),
59
+ ("function", "b", ""),
60
+ ("function", "c", ""),
61
+ ],
62
+ )
63
+
64
+
65
+ @case(tags=["symbol"])
66
+ def case_js_class() -> SymbolCase:
67
+ """class User {}."""
68
+ return "javascript", "class User {}\n", [("class", "User", "")]
69
+
70
+
71
+ @case(tags=["symbol"])
72
+ def case_js_class_extends() -> SymbolCase:
73
+ """class Admin extends User {}."""
74
+ return "javascript", "class Admin extends User {}\n", [("class", "Admin", "")]
75
+
76
+
77
+ @case(tags=["symbol"])
78
+ def case_js_nested_function() -> SymbolCase:
79
+ """A function nested in a function is addressed by the outer function."""
80
+ src = """\
81
+ function outer() {
82
+ function inner() {
83
+ return 1;
84
+ }
85
+ return inner;
86
+ }
87
+ """
88
+ return "javascript", src, [("function", "inner", "outer")]
89
+
90
+
91
+ @case(tags=["import"])
92
+ def case_js_import_named_single() -> ImportCase:
93
+ """import { foo } from './models'."""
94
+ return (
95
+ "javascript",
96
+ "import { foo } from './models';\n",
97
+ {"module": "models", "names": "foo", "dots": "1"},
98
+ )
99
+
100
+
101
+ @case(tags=["import"])
102
+ def case_js_import_named_multiple() -> ImportCase:
103
+ """import { foo, bar } from './models'."""
104
+ return (
105
+ "javascript",
106
+ "import { foo, bar } from './models';\n",
107
+ {"module": "models", "names": "foo,bar", "dots": "1"},
108
+ )
109
+
110
+
111
+ @case(tags=["import"])
112
+ def case_js_import_named_parent() -> ImportCase:
113
+ """import from parent directory."""
114
+ return (
115
+ "javascript",
116
+ "import { Config } from '../config';\n",
117
+ {"module": "config", "names": "Config", "dots": "2"},
118
+ )
119
+
120
+
121
+ @case(tags=["import"])
122
+ def case_js_import_named_grandparent() -> ImportCase:
123
+ """import from grandparent directory."""
124
+ return (
125
+ "javascript",
126
+ "import { util } from '../../shared/util';\n",
127
+ {"module": "shared/util", "names": "util", "dots": "3"},
128
+ )
129
+
130
+
131
+ @case(tags=["import"])
132
+ def case_js_import_default() -> ImportCase:
133
+ """import React from 'react'."""
134
+ return "javascript", "import React from 'react';\n", {"module": "react", "names": "React"}
135
+
136
+
137
+ @case(tags=["import"])
138
+ def case_js_import_default_relative() -> ImportCase:
139
+ """import App from './App'."""
140
+ return (
141
+ "javascript",
142
+ "import App from './App';\n",
143
+ {"module": "App", "names": "App", "dots": "1"},
144
+ )
145
+
146
+
147
+ @case(tags=["import"])
148
+ def case_js_import_namespace() -> ImportCase:
149
+ """import * as utils from '../utils'."""
150
+ return (
151
+ "javascript",
152
+ "import * as utils from '../utils';\n",
153
+ {"module": "utils", "names": "utils", "dots": "2"},
154
+ )
155
+
156
+
157
+ @case(tags=["import"])
158
+ def case_js_import_side_effect() -> ImportCase:
159
+ """import './styles.css' — side-effect only."""
160
+ return "javascript", "import './styles.css';\n", {"module": "styles", "dots": "1"}
161
+
162
+
163
+ @case(tags=["import"])
164
+ def case_js_import_side_effect_no_ext() -> ImportCase:
165
+ """import './polyfills' — no extension."""
166
+ return "javascript", "import './polyfills';\n", {"module": "polyfills", "dots": "1"}
167
+
168
+
169
+ @case(tags=["import"])
170
+ def case_js_import_package() -> ImportCase:
171
+ """import express from 'express' — absolute."""
172
+ return (
173
+ "javascript",
174
+ "import express from 'express';\n",
175
+ {"module": "express", "names": "express"},
176
+ )
177
+
178
+
179
+ @case(tags=["import"])
180
+ def case_js_import_scoped_package() -> ImportCase:
181
+ """import from scoped npm package."""
182
+ return (
183
+ "javascript",
184
+ "import { render } from '@testing-library/react';\n",
185
+ {"module": "@testing-library/react", "names": "render"},
186
+ )
187
+
188
+
189
+ @case(tags=["multi_import"])
190
+ def case_js_multiple_imports() -> MultiImportCase:
191
+ """Two import statements."""
192
+ src = """\
193
+ import React from 'react';
194
+ import { useState } from 'react';
195
+ """
196
+ return (
197
+ "javascript",
198
+ src,
199
+ 2,
200
+ [
201
+ {"module": "react", "names": "React"},
202
+ {"module": "react", "names": "useState"},
203
+ ],
204
+ )
205
+
206
+
207
+ @case(tags=["mixed"])
208
+ def case_js_full_module() -> MixedCase:
209
+ """Realistic JS module with JSDoc on every symbol.
210
+
211
+ Adding JSDoc here exercises the production-plugin default
212
+ (leading-comment attachment on) against the conventional
213
+ JS documentation style. The expected-kinds tuple is
214
+ unchanged — this confirms documentation does not disturb
215
+ symbol extraction. Content assertions for docstring
216
+ presence are covered by `test_docstrings.py`.
217
+ """
218
+ src = """\
219
+ import { Model } from './model';
220
+
221
+ /** Service facade over `Model`. */
222
+ class Service {
223
+ }
224
+
225
+ /** Create a new service instance. */
226
+ function create() {
227
+ }
228
+
229
+ /** Tear it down. */
230
+ const destroy = () => {};
231
+ """
232
+ return "javascript", src, {"import", "class", "function"}, []
233
+
234
+
235
+ @case(tags=["symbol"])
236
+ def case_ts_function() -> SymbolCase:
237
+ """function greet(): void {}."""
238
+ return "typescript", "function greet(): void {}\n", [("function", "greet", "")]
239
+
240
+
241
+ @case(tags=["symbol"])
242
+ def case_ts_arrow_function() -> SymbolCase:
243
+ """Arrow function with type annotations."""
244
+ return (
245
+ "typescript",
246
+ "const add = (a: number, b: number): number => a + b;\n",
247
+ [("function", "add", "")],
248
+ )
249
+
250
+
251
+ @case(tags=["symbol"])
252
+ def case_ts_class() -> SymbolCase:
253
+ """class Service {}."""
254
+ return "typescript", "class Service {}\n", [("class", "Service", "")]
255
+
256
+
257
+ @case(tags=["symbol"])
258
+ def case_ts_class_generics() -> SymbolCase:
259
+ """class Container<T> {}."""
260
+ return "typescript", "class Container<T> {}\n", [("class", "Container", "")]
261
+
262
+
263
+ @case(tags=["symbol"])
264
+ def case_ts_namespace_function() -> SymbolCase:
265
+ """A function in a TS namespace is addressed by the namespace.
266
+
267
+ TS `namespace` is not tracked today (`f` → ""); target "N".
268
+ """
269
+ src = """\
270
+ namespace N {
271
+ export function f(): void {}
272
+ }
273
+ """
274
+ return "typescript", src, [("function", "f", "N")]
275
+
276
+
277
+ @case(tags=["symbol"])
278
+ def case_ts_nested_namespace() -> SymbolCase:
279
+ """Nested TS namespaces compose."""
280
+ src = """\
281
+ namespace A {
282
+ export namespace B {
283
+ export function f(): void {}
284
+ }
285
+ }
286
+ """
287
+ return "typescript", src, [("function", "f", "A::B")]
288
+
289
+
290
+ @case(tags=["import"])
291
+ def case_ts_import_named() -> ImportCase:
292
+ """import { User } from './types'."""
293
+ return (
294
+ "typescript",
295
+ "import { User } from './types';\n",
296
+ {"module": "types", "names": "User", "dots": "1"},
297
+ )
298
+
299
+
300
+ @case(tags=["import"])
301
+ def case_ts_import_type() -> ImportCase:
302
+ """import type { Config } from './config'."""
303
+ return (
304
+ "typescript",
305
+ "import type { Config } from './config';\n",
306
+ {"module": "config", "names": "Config", "dots": "1"},
307
+ )
308
+
309
+
310
+ @case(tags=["import"])
311
+ def case_ts_import_default() -> ImportCase:
312
+ """import Express from 'express'."""
313
+ return (
314
+ "typescript",
315
+ "import Express from 'express';\n",
316
+ {"module": "express", "names": "Express"},
317
+ )
318
+
319
+
320
+ @case(tags=["import"])
321
+ def case_ts_import_namespace() -> ImportCase:
322
+ """import * as path from 'path'."""
323
+ return "typescript", "import * as path from 'path';\n", {"module": "path", "names": "path"}
324
+
325
+
326
+ @case(tags=["import"])
327
+ def case_ts_import_side_effect() -> ImportCase:
328
+ """import './setup'."""
329
+ return "typescript", "import './setup';\n", {"module": "setup", "dots": "1"}
330
+
331
+
332
+ @case(tags=["mixed"])
333
+ def case_ts_full_module() -> MixedCase:
334
+ """Realistic TS module with JSDoc on every symbol.
335
+
336
+ Mirrors the JS variant; expected-kinds tuple unchanged.
337
+ Content assertions for docstring presence are in
338
+ `test_docstrings.py`.
339
+ """
340
+ src = """\
341
+ import { Model } from './model';
342
+
343
+ /** Repository over `Model` records. */
344
+ class Repository {
345
+ }
346
+
347
+ /** Run a query and return the first row. */
348
+ function query(): void {
349
+ }
350
+ """
351
+ return "typescript", src, {"import", "class", "function"}, []
352
+
353
+
354
+ @case(tags=["symbol"])
355
+ def case_js_module_const() -> SymbolCase:
356
+ """Top-level const."""
357
+ return "javascript", "const MAX = 100;\n", [("variable", "MAX", "")]
358
+
359
+
360
+ @case(tags=["symbol"])
361
+ def case_js_exported_const() -> SymbolCase:
362
+ """Exported top-level const."""
363
+ return "javascript", "export const TIMEOUT = 30;\n", [("variable", "TIMEOUT", "")]
364
+
365
+
366
+ @case(tags=["symbol"])
367
+ def case_ts_module_const() -> SymbolCase:
368
+ """Top-level annotated const."""
369
+ return "typescript", "const MAX: number = 100;\n", [("variable", "MAX", "")]
370
+
371
+
372
+ @case(tags=["symbol"])
373
+ def case_js_object_destructure() -> SymbolCase:
374
+ """Object destructuring (shorthand)."""
375
+ return "javascript", "const {a, b} = o;\n", [("variable", "a", ""), ("variable", "b", "")]
376
+
377
+
378
+ @case(tags=["symbol"])
379
+ def case_js_object_renamed() -> SymbolCase:
380
+ """Object destructuring with rename binds the renamed target."""
381
+ return "javascript", "const {a: ra} = o;\n", [("variable", "ra", "")]
382
+
383
+
384
+ @case(tags=["symbol"])
385
+ def case_js_array_destructure() -> SymbolCase:
386
+ """Array destructuring."""
387
+ return "javascript", "const [x, y] = arr;\n", [("variable", "x", ""), ("variable", "y", "")]
388
+
389
+
390
+ @case(tags=["symbol"])
391
+ def case_js_object_rest() -> SymbolCase:
392
+ """Object rest element."""
393
+ return (
394
+ "javascript",
395
+ "const {a, ...rest} = o;\n",
396
+ [("variable", "a", ""), ("variable", "rest", "")],
397
+ )
398
+
399
+
400
+ @case(tags=["symbol"])
401
+ def case_js_exported_destructure() -> SymbolCase:
402
+ """Exported destructuring."""
403
+ return (
404
+ "javascript",
405
+ "export const {a, b} = o;\n",
406
+ [("variable", "a", ""), ("variable", "b", "")],
407
+ )
408
+
409
+
410
+ @case(tags=["symbol"])
411
+ def case_ts_object_destructure() -> SymbolCase:
412
+ """TS object destructuring."""
413
+ return "typescript", "const {a, b} = o;\n", [("variable", "a", ""), ("variable", "b", "")]
414
+
415
+
416
+ @case(tags=["symbol"], marks=_xfail_nested)
417
+ def case_js_nested_array_xfail() -> SymbolCase:
418
+ """Nested array destructuring — only the outer level captured today."""
419
+ return (
420
+ "javascript",
421
+ "const [a, [b, c]] = x;\n",
422
+ [("variable", "a", ""), ("variable", "b", ""), ("variable", "c", "")],
423
+ )
424
+
425
+
426
+ @case(tags=["symbol"], marks=_xfail_nested)
427
+ def case_js_nested_object_xfail() -> SymbolCase:
428
+ """Nested object destructuring — nothing captured today."""
429
+ return "javascript", "const {a: {b}} = x;\n", [("variable", "b", "")]
@@ -0,0 +1,2 @@
1
+ // Locale configuration for the greeter.
2
+ export const LOCALE = "en";
@@ -0,0 +1,46 @@
1
+ // Greeter — format greetings for named recipients.
2
+ //
3
+ // The JavaScript plugin extracts function declarations, generator
4
+ // functions, arrow functions bound to consts, classes, module-level
5
+ // const/let variables (including destructuring), imports, and methods
6
+ // (class members, including get/set accessors; object-literal methods are
7
+ // captured without a scope).
8
+
9
+ import { LOCALE } from "./config.js";
10
+ import format from "formatter";
11
+ import "./styles.css";
12
+
13
+ export const DEFAULT_GREETING = "Hello";
14
+ const { locale, fallback } = LOCALE;
15
+
16
+ /** Format a greeting for a name. */
17
+ export function formatGreeting(name) {
18
+ return `${DEFAULT_GREETING}, ${name} (${locale ?? fallback})`;
19
+ }
20
+
21
+ const cachedDefault = () => DEFAULT_GREETING;
22
+
23
+ /** Stateful greeter holding a prefix. */
24
+ export class Greeter {
25
+ constructor(prefix = DEFAULT_GREETING) {
26
+ this.prefix = prefix;
27
+ }
28
+
29
+ greet(name) {
30
+ return format(`${this.prefix}, ${name}`);
31
+ }
32
+ }
33
+
34
+ /** Yield each recipient name in turn. */
35
+ export function* recipients(names) {
36
+ for (const name of names) {
37
+ yield name;
38
+ }
39
+ }
40
+
41
+ /** Greeting helpers grouped as an object literal. */
42
+ export const helpers = {
43
+ shout(name) {
44
+ return `${name.toUpperCase()}!`;
45
+ },
46
+ };