From 966793f0203f5c4605680f67a156ceabdd6b49b2 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 03:04:10 +0900 Subject: [PATCH 1/7] fix: highlight variadic type parameter names as types Co-authored-by: WillBooster (Codex CLI) --- queries/highlights.scm | 1 + test/unit/variadicTypeHighlights.test.ts | 45 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/unit/variadicTypeHighlights.test.ts diff --git a/queries/highlights.scm b/queries/highlights.scm index 9272277..95eb1ab 100644 --- a/queries/highlights.scm +++ b/queries/highlights.scm @@ -63,6 +63,7 @@ (attribute attribute: (identifier) @property) (type (identifier) @type) +(type_parameter (splat_type (identifier) @type)) ; Literals diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts new file mode 100644 index 0000000..fbbbd99 --- /dev/null +++ b/test/unit/variadicTypeHighlights.test.ts @@ -0,0 +1,45 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { expect, test } from 'vitest'; + +test('renders variadic type parameter names as types', () => { + const root = path.resolve(import.meta.dirname, '../..'); + const directory = fs.mkdtempSync(path.join(root, '.tmp/variadic-highlights-')); + const input = path.join(directory, 'types.py'); + try { + fs.writeFileSync( + input, + `class C[*Ts]: pass +def f[**P](): pass +type A[*Us] = tuple[*Us] +Ts = 1 +P = 2 +` + ); + const html = execFileSync( + path.join(root, 'script/tree-sitter'), + ['highlight', '-p', root, '-H', '--layout', 'fragment', '--style', 'minimal', input], + { + cwd: root, + encoding: 'utf8', + timeout: 170_000, + env: { ...process.env, TREE_SITTER_LIBDIR: path.join(root, '.tmp/tree-sitter-lib') }, + } + ); + const highlights = [...html.matchAll(/(Ts|P|Us)<\/span>/g)].map(([, kind, name]) => [ + name, + kind, + ]); + expect(highlights).toEqual([ + ['Ts', 'type'], + ['P', 'type'], + ['Us', 'type'], + ['Us', 'type'], + ['Ts', 'constructor'], + ['P', 'constant'], + ]); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}, 180_000); From 8ee34462baf088f01b077dfe23ca0b805162cd88 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 03:10:28 +0900 Subject: [PATCH 2/7] fix: include unpacked parameter annotations in type highlights Co-authored-by: WillBooster (Codex CLI) --- queries/highlights.scm | 2 +- test/unit/variadicTypeHighlights.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/queries/highlights.scm b/queries/highlights.scm index 95eb1ab..fa9e918 100644 --- a/queries/highlights.scm +++ b/queries/highlights.scm @@ -63,7 +63,7 @@ (attribute attribute: (identifier) @property) (type (identifier) @type) -(type_parameter (splat_type (identifier) @type)) +(splat_type (identifier) @type) ; Literals diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts index fbbbd99..9494916 100644 --- a/test/unit/variadicTypeHighlights.test.ts +++ b/test/unit/variadicTypeHighlights.test.ts @@ -12,6 +12,7 @@ test('renders variadic type parameter names as types', () => { input, `class C[*Ts]: pass def f[**P](): pass +def g[*Vs](*args: *Vs): pass type A[*Us] = tuple[*Us] Ts = 1 P = 2 @@ -27,13 +28,15 @@ P = 2 env: { ...process.env, TREE_SITTER_LIBDIR: path.join(root, '.tmp/tree-sitter-lib') }, } ); - const highlights = [...html.matchAll(/(Ts|P|Us)<\/span>/g)].map(([, kind, name]) => [ + const highlights = [...html.matchAll(/(Ts|P|Us|Vs)<\/span>/g)].map(([, kind, name]) => [ name, kind, ]); expect(highlights).toEqual([ ['Ts', 'type'], ['P', 'type'], + ['Vs', 'type'], + ['Vs', 'type'], ['Us', 'type'], ['Us', 'type'], ['Ts', 'constructor'], From 99c5a79e0ac82c6fa07a0df181eec5588aae5f07 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 04:34:08 +0900 Subject: [PATCH 3/7] refactor: share bounded highlight rendering in tests Co-authored-by: WillBooster (Codex CLI) --- test/helpers/renderHighlights.ts | 24 ++++++++++ test/unit/lazyImports.test.ts | 26 ++--------- test/unit/variadicTypeHighlights.test.ts | 56 +++++++----------------- 3 files changed, 45 insertions(+), 61 deletions(-) create mode 100644 test/helpers/renderHighlights.ts diff --git a/test/helpers/renderHighlights.ts b/test/helpers/renderHighlights.ts new file mode 100644 index 0000000..0f34f91 --- /dev/null +++ b/test/helpers/renderHighlights.ts @@ -0,0 +1,24 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +export function renderHighlights(source: string): string { + const root = path.resolve(import.meta.dirname, '../..'); + const directory = fs.mkdtempSync(path.join(root, '.tmp/highlights-')); + const input = path.join(directory, 'input.py'); + try { + fs.writeFileSync(input, source); + return execFileSync( + path.join(root, 'script/tree-sitter'), + ['highlight', '-p', root, '-H', '--layout', 'fragment', '--style', 'minimal', input], + { + cwd: root, + encoding: 'utf8', + timeout: 170_000, + env: { ...process.env, TREE_SITTER_LIBDIR: path.join(root, '.tmp/tree-sitter-lib') }, + } + ); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +} diff --git a/test/unit/lazyImports.test.ts b/test/unit/lazyImports.test.ts index 80ed8c5..98858f6 100644 --- a/test/unit/lazyImports.test.ts +++ b/test/unit/lazyImports.test.ts @@ -1,26 +1,8 @@ -import { execFileSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; import { expect, test } from 'vitest'; +import { renderHighlights } from '../helpers/renderHighlights.js'; test('highlights lazy only as an import modifier', () => { - const root = path.resolve(import.meta.dirname, '../..'); - const directory = fs.mkdtempSync(path.join(root, '.tmp/lazy-highlights-')); - const input = path.join(directory, 'imports.py'); - try { - fs.writeFileSync(input, 'lazy import json\nlazy from json import dumps\nlazy = 1\nimport lazy\n'); - const html = execFileSync( - path.join(root, 'script/tree-sitter'), - ['highlight', '-p', root, '-H', '--layout', 'fragment', '--style', 'minimal', input], - { - cwd: root, - encoding: 'utf8', - env: { ...process.env, TREE_SITTER_LIBDIR: path.join(root, '.tmp/tree-sitter-lib') }, - } - ); - const classes = [...html.matchAll(/lazy<\/span>/g)].map(([, capture]) => capture); - expect(classes).toEqual(['keyword', 'keyword', 'variable', 'variable']); - } finally { - fs.rmSync(directory, { recursive: true, force: true }); - } + const html = renderHighlights('lazy import json\nlazy from json import dumps\nlazy = 1\nimport lazy\n'); + const classes = [...html.matchAll(/lazy<\/span>/g)].map(([, capture]) => capture); + expect(classes).toEqual(['keyword', 'keyword', 'variable', 'variable']); }, 180_000); diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts index 9494916..1509d64 100644 --- a/test/unit/variadicTypeHighlights.test.ts +++ b/test/unit/variadicTypeHighlights.test.ts @@ -1,48 +1,26 @@ -import { execFileSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; import { expect, test } from 'vitest'; +import { renderHighlights } from '../helpers/renderHighlights.js'; test('renders variadic type parameter names as types', () => { - const root = path.resolve(import.meta.dirname, '../..'); - const directory = fs.mkdtempSync(path.join(root, '.tmp/variadic-highlights-')); - const input = path.join(directory, 'types.py'); - try { - fs.writeFileSync( - input, - `class C[*Ts]: pass + const html = renderHighlights(`class C[*Ts]: pass def f[**P](): pass def g[*Vs](*args: *Vs): pass type A[*Us] = tuple[*Us] Ts = 1 P = 2 -` - ); - const html = execFileSync( - path.join(root, 'script/tree-sitter'), - ['highlight', '-p', root, '-H', '--layout', 'fragment', '--style', 'minimal', input], - { - cwd: root, - encoding: 'utf8', - timeout: 170_000, - env: { ...process.env, TREE_SITTER_LIBDIR: path.join(root, '.tmp/tree-sitter-lib') }, - } - ); - const highlights = [...html.matchAll(/(Ts|P|Us|Vs)<\/span>/g)].map(([, kind, name]) => [ - name, - kind, - ]); - expect(highlights).toEqual([ - ['Ts', 'type'], - ['P', 'type'], - ['Vs', 'type'], - ['Vs', 'type'], - ['Us', 'type'], - ['Us', 'type'], - ['Ts', 'constructor'], - ['P', 'constant'], - ]); - } finally { - fs.rmSync(directory, { recursive: true, force: true }); - } +`); + const highlights = [...html.matchAll(/(Ts|P|Us|Vs)<\/span>/g)].map(([, kind, name]) => [ + name, + kind, + ]); + expect(highlights).toEqual([ + ['Ts', 'type'], + ['P', 'type'], + ['Vs', 'type'], + ['Vs', 'type'], + ['Us', 'type'], + ['Us', 'type'], + ['Ts', 'constructor'], + ['P', 'constant'], + ]); }, 180_000); From 82edc9e7ca0dd9eba8acfa5d3a9f364d73b477eb Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 06:33:02 +0900 Subject: [PATCH 4/7] fix: create highlight test workspace on fresh checkouts Co-authored-by: WillBooster (Codex CLI) --- test/helpers/renderHighlights.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/helpers/renderHighlights.ts b/test/helpers/renderHighlights.ts index 0f34f91..f61d591 100644 --- a/test/helpers/renderHighlights.ts +++ b/test/helpers/renderHighlights.ts @@ -4,6 +4,7 @@ import path from 'node:path'; export function renderHighlights(source: string): string { const root = path.resolve(import.meta.dirname, '../..'); + fs.mkdirSync(path.join(root, '.tmp'), { recursive: true }); const directory = fs.mkdtempSync(path.join(root, '.tmp/highlights-')); const input = path.join(directory, 'input.py'); try { From aaa5d85f24fc9333de61f282dbc3feaf835fb705 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 07:08:15 +0900 Subject: [PATCH 5/7] fix: highlight variadic class-base arguments Co-authored-by: WillBooster (Codex CLI) --- queries/highlights.scm | 3 +++ test/fixtures/highlights/variadicTypes.py | 3 +++ test/unit/variadicTypeHighlights.test.ts | 8 +++++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/queries/highlights.scm b/queries/highlights.scm index 5660f4a..ea1db07 100644 --- a/queries/highlights.scm +++ b/queries/highlights.scm @@ -64,6 +64,9 @@ (attribute attribute: (identifier) @property) (type (identifier) @type) (splat_type (identifier) @type) +(class_definition + superclasses: (argument_list + (subscript subscript: (list_splat (identifier) @type)))) (member_type (identifier) @type) (generic_type (identifier) @type) (default_type_parameter name: (identifier) @type) diff --git a/test/fixtures/highlights/variadicTypes.py b/test/fixtures/highlights/variadicTypes.py index de38177..9ae6ab7 100644 --- a/test/fixtures/highlights/variadicTypes.py +++ b/test/fixtures/highlights/variadicTypes.py @@ -2,5 +2,8 @@ class C[*Ts]: pass def f[**P](): pass def g[*Vs](*args: *Vs): pass type A[*Us] = tuple[*Us] +class B(Generic[*Ts]): pass +class D(typing.Generic[*Us]): pass Ts = 1 P = 2 +values = [*Vs] diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts index 8af7968..402f92f 100644 --- a/test/unit/variadicTypeHighlights.test.ts +++ b/test/unit/variadicTypeHighlights.test.ts @@ -22,16 +22,18 @@ testCommand( name, kind, ]); - expect(highlights).toEqual([ + expect(highlights.slice(0, 8)).toEqual([ ['Ts', 'type'], ['P', 'type'], ['Vs', 'type'], ['Vs', 'type'], ['Us', 'type'], ['Us', 'type'], - ['Ts', 'constructor'], - ['P', 'constant'], + ['Ts', 'type'], + ['Us', 'type'], ]); + expect(highlights.slice(8).map(([name]) => name)).toEqual(['Ts', 'P', 'Vs']); + expect(highlights.slice(8).map(([, kind]) => kind)).not.toContain('type'); }, } ); From 4a38d1a3b4dda346e3b18684af322c5bef198bc9 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 07:20:13 +0900 Subject: [PATCH 6/7] test: detect type highlighting in value subscripts Co-authored-by: WillBooster (Codex CLI) --- test/fixtures/highlights/variadicTypes.py | 1 + test/unit/variadicTypeHighlights.test.ts | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/fixtures/highlights/variadicTypes.py b/test/fixtures/highlights/variadicTypes.py index 9ae6ab7..299ef72 100644 --- a/test/fixtures/highlights/variadicTypes.py +++ b/test/fixtures/highlights/variadicTypes.py @@ -7,3 +7,4 @@ class D(typing.Generic[*Us]): pass Ts = 1 P = 2 values = [*Vs] +lookup = table[*keys] diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts index 402f92f..0984d96 100644 --- a/test/unit/variadicTypeHighlights.test.ts +++ b/test/unit/variadicTypeHighlights.test.ts @@ -18,10 +18,9 @@ testCommand( 170_000, { check: (html) => { - const highlights = [...html.matchAll(/(Ts|P|Us|Vs)<\/span>/g)].map(([, kind, name]) => [ - name, - kind, - ]); + const highlights = [...html.matchAll(/(Ts|P|Us|Vs|keys)<\/span>/g)].map( + ([, kind, name]) => [name, kind] + ); expect(highlights.slice(0, 8)).toEqual([ ['Ts', 'type'], ['P', 'type'], @@ -32,7 +31,7 @@ testCommand( ['Ts', 'type'], ['Us', 'type'], ]); - expect(highlights.slice(8).map(([name]) => name)).toEqual(['Ts', 'P', 'Vs']); + expect(highlights.slice(8).map(([name]) => name)).toEqual(['Ts', 'P', 'Vs', 'keys']); expect(highlights.slice(8).map(([, kind]) => kind)).not.toContain('type'); }, } From a82969f884874d30adfec6a7e1c3d8e0a0822988 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 19 Oct 2026 09:49:05 +0900 Subject: [PATCH 7/7] test: distinguish variadic class bases from call arguments Co-authored-by: WillBooster (Codex CLI) --- test/fixtures/highlights/variadicTypes.py | 1 + test/unit/variadicTypeHighlights.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fixtures/highlights/variadicTypes.py b/test/fixtures/highlights/variadicTypes.py index 299ef72..3f27aa5 100644 --- a/test/fixtures/highlights/variadicTypes.py +++ b/test/fixtures/highlights/variadicTypes.py @@ -8,3 +8,4 @@ class D(typing.Generic[*Us]): pass P = 2 values = [*Vs] lookup = table[*keys] +register(Generic[*idx]) diff --git a/test/unit/variadicTypeHighlights.test.ts b/test/unit/variadicTypeHighlights.test.ts index 0984d96..76f4e13 100644 --- a/test/unit/variadicTypeHighlights.test.ts +++ b/test/unit/variadicTypeHighlights.test.ts @@ -18,7 +18,7 @@ testCommand( 170_000, { check: (html) => { - const highlights = [...html.matchAll(/(Ts|P|Us|Vs|keys)<\/span>/g)].map( + const highlights = [...html.matchAll(/(Ts|P|Us|Vs|keys|idx)<\/span>/g)].map( ([, kind, name]) => [name, kind] ); expect(highlights.slice(0, 8)).toEqual([ @@ -31,7 +31,7 @@ testCommand( ['Ts', 'type'], ['Us', 'type'], ]); - expect(highlights.slice(8).map(([name]) => name)).toEqual(['Ts', 'P', 'Vs', 'keys']); + expect(highlights.slice(8).map(([name]) => name)).toEqual(['Ts', 'P', 'Vs', 'keys', 'idx']); expect(highlights.slice(8).map(([, kind]) => kind)).not.toContain('type'); }, }