From 50eca53aad43035ef93fa80647b0a2adce26c9ac Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 07:19:49 +0100 Subject: [PATCH 1/7] JS: Add CaptureKind to template captures for position-aware scaffolding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a CaptureKind enum (Expression, Identifier, TypeReference, Statement) and kind-specific factory functions (expr, ident, typeRef, stmt) as an alternative to the generic capture() function. Factory functions are also available as namespace-qualified methods on capture (e.g. capture.expr()). The existing capture() function defaults to CaptureKind.Expression for backwards compatibility. The kind is stored but not yet consumed by the engine — scaffold-aware placeholder generation will follow. --- .../src/javascript/templating/capture.ts | 103 +++++++++++- .../src/javascript/templating/index.ts | 10 +- .../src/javascript/templating/types.ts | 20 +++ .../templating/capture-kinds.test.ts | 155 ++++++++++++++++++ 4 files changed, 284 insertions(+), 4 deletions(-) create mode 100644 rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts diff --git a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts index e8f55961303..b9e26b5f28f 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts @@ -15,7 +15,7 @@ */ import {Cursor} from '../..'; import {J, Type} from '../../java'; -import {Any, Capture, CaptureConstraintContext, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types'; +import {Any, Capture, CaptureConstraintContext, CaptureKind, CaptureOptions, ConstraintFunction, TemplateParam, VariadicOptions} from './types'; /** * Combines multiple constraints with AND logic. @@ -73,6 +73,8 @@ export const CAPTURE_CONSTRAINT_SYMBOL = Symbol('captureConstraint'); export const CAPTURE_CAPTURING_SYMBOL = Symbol('captureCapturing'); // Symbol to access type information without triggering Proxy export const CAPTURE_TYPE_SYMBOL = Symbol('captureType'); +// Symbol to access capture kind without triggering Proxy +export const CAPTURE_KIND_SYMBOL = Symbol('captureKind'); // Symbol to identify RawCode instances export const RAW_CODE_SYMBOL = Symbol('rawCode'); @@ -83,11 +85,13 @@ export class CaptureImpl implements Capture { [CAPTURE_CONSTRAINT_SYMBOL]: ConstraintFunction | undefined; [CAPTURE_CAPTURING_SYMBOL]: boolean; [CAPTURE_TYPE_SYMBOL]: string | Type | undefined; + [CAPTURE_KIND_SYMBOL]: CaptureKind; - constructor(name: string, options?: CaptureOptions, capturing: boolean = true) { + constructor(name: string, options?: CaptureOptions, capturing: boolean = true, kind: CaptureKind = CaptureKind.Expression) { this.name = name; this[CAPTURE_NAME_SYMBOL] = name; this[CAPTURE_CAPTURING_SYMBOL] = capturing; + this[CAPTURE_KIND_SYMBOL] = options?.kind ?? kind; // Normalize variadic options if (options?.variadic) { @@ -135,6 +139,10 @@ export class CaptureImpl implements Capture { getType(): string | Type | undefined { return this[CAPTURE_TYPE_SYMBOL]; } + + getKind(): CaptureKind { + return this[CAPTURE_KIND_SYMBOL]; + } } export class TemplateParamImpl implements TemplateParam { @@ -309,6 +317,9 @@ function createCaptureProxy(impl: CaptureImpl): any { if (prop === CAPTURE_TYPE_SYMBOL) { return target[CAPTURE_TYPE_SYMBOL]; } + if (prop === CAPTURE_KIND_SYMBOL) { + return target[CAPTURE_KIND_SYMBOL]; + } // Support using Capture as object key via computed properties {[x]: value} if (prop === Symbol.toPrimitive || prop === 'toString' || prop === 'valueOf') { @@ -316,7 +327,7 @@ function createCaptureProxy(impl: CaptureImpl): any { } // Allow methods to be called directly on the target - if (prop === 'getName' || prop === 'isVariadic' || prop === 'getVariadicOptions' || prop === 'getConstraint' || prop === 'isCapturing' || prop === 'getType') { + if (prop === 'getName' || prop === 'isVariadic' || prop === 'getVariadicOptions' || prop === 'getConstraint' || prop === 'isCapturing' || prop === 'getType' || prop === 'getKind') { return target[prop].bind(target); } @@ -388,6 +399,15 @@ export function capture(nameOrOptions?: string | CaptureOptions): Ca // Static counter for generating unique IDs for unnamed captures capture.nextUnnamedId = 1; +// Type declarations for namespace properties on capture +export namespace capture { + export let nextUnnamedId: number; + export let expr: typeof import('./capture').expr; + export let ident: typeof import('./capture').ident; + export let typeRef: typeof import('./capture').typeRef; + export let stmt: typeof import('./capture').stmt; +} + /** * Creates a non-capturing pattern match for use in patterns. * @@ -581,6 +601,83 @@ export function raw(code: string): RawCode { return new RawCode(code); } +/** + * Creates an expression capture. This is the most common capture kind. + * + * @example + * const e = expr('x'); + * pattern`foo(${e})` + */ +export function expr(name?: string): Capture & T; +export function expr(options: CaptureOptions): Capture & T; +export function expr(nameOrOptions?: string | CaptureOptions): Capture & T { + return createKindCapture(CaptureKind.Expression, nameOrOptions); +} + +/** + * Creates an identifier/name capture. + * + * @example + * const n = ident('method'); + * pattern`${n}()` + */ +export function ident(name?: string): Capture & T; +export function ident(options: CaptureOptions): Capture & T; +export function ident(nameOrOptions?: string | CaptureOptions): Capture & T { + return createKindCapture(CaptureKind.Identifier, nameOrOptions); +} + +/** + * Creates a type reference capture. + * + * @example + * const t = typeRef('ret'); + * pattern`function foo(): ${t}` + */ +export function typeRef(name?: string): Capture & T; +export function typeRef(options: CaptureOptions): Capture & T; +export function typeRef(nameOrOptions?: string | CaptureOptions): Capture & T { + return createKindCapture(CaptureKind.TypeReference, nameOrOptions); +} + +/** + * Creates a statement capture. + * + * @example + * const s = stmt('body'); + * pattern`if (cond) ${s}` + */ +export function stmt(name?: string): Capture & T; +export function stmt(options: CaptureOptions): Capture & T; +export function stmt(nameOrOptions?: string | CaptureOptions): Capture & T { + return createKindCapture(CaptureKind.Statement, nameOrOptions); +} + +/** + * Internal helper for kind-specific capture factory functions. + */ +function createKindCapture(kind: CaptureKind, nameOrOptions?: string | CaptureOptions): Capture & T { + let name: string | undefined; + let options: CaptureOptions | undefined; + + if (typeof nameOrOptions === 'string') { + name = nameOrOptions; + } else { + options = nameOrOptions; + name = options?.name; + } + + const captureName = name || `unnamed_${capture.nextUnnamedId++}`; + const impl = new CaptureImpl(captureName, options, true, kind); + return createCaptureProxy(impl); +} + +// Attach kind-specific factories to capture for namespace-qualified access +capture.expr = expr; +capture.ident = ident; +capture.typeRef = typeRef; +capture.stmt = stmt; + /** * Concise alias for `capture`. Works well for inline captures in patterns and templates. * diff --git a/rewrite-javascript/rewrite/src/javascript/templating/index.ts b/rewrite-javascript/rewrite/src/javascript/templating/index.ts index d0db0c15d6e..a138a20d726 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/index.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/index.ts @@ -33,6 +33,10 @@ export type { MatchAttemptResult } from './types'; +export { + CaptureKind +} from './types'; + // Export capture functionality export { and, @@ -42,7 +46,11 @@ export { any, param, raw, - _ + _, + expr, + ident, + typeRef, + stmt } from './capture'; // Export pattern functionality diff --git a/rewrite-javascript/rewrite/src/javascript/templating/types.ts b/rewrite-javascript/rewrite/src/javascript/templating/types.ts index a008f95d154..04c184a2d5d 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/types.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/types.ts @@ -19,6 +19,21 @@ import type {Pattern} from "./pattern"; import type {Template} from "./template"; import type {CaptureValue, RawCode} from "./capture"; +/** + * The kind of syntactic element a capture represents. + * Used by the template engine to generate appropriate scaffold placeholders. + */ +export enum CaptureKind { + /** An expression (the default). */ + Expression = 'expression', + /** An identifier / name. */ + Identifier = 'identifier', + /** A type reference. */ + TypeReference = 'type-reference', + /** A statement. */ + Statement = 'statement', +} + /** * Options for variadic captures that match zero or more nodes in a sequence. */ @@ -99,6 +114,11 @@ export type ConstraintFunction = (node: T, context: CaptureConstraintContext) export interface CaptureOptions { name?: string; variadic?: boolean | VariadicOptions; + /** + * The syntactic kind of this capture. Defaults to {@link CaptureKind.Expression}. + * Used by the template engine to generate the correct scaffold placeholder. + */ + kind?: CaptureKind; /** * Optional constraint function that validates whether a captured node should be accepted. * The function receives: diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts new file mode 100644 index 00000000000..6054220db64 --- /dev/null +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + capture, expr, ident, typeRef, stmt, any, CaptureKind, + Pattern, pattern +} from "../../../src/javascript"; +import { + CAPTURE_KIND_SYMBOL, CAPTURE_NAME_SYMBOL +} from "../../../src/javascript/templating/capture"; + +describe('capture kinds', () => { + describe('CaptureKind enum', () => { + test('has expected values', () => { + expect(CaptureKind.Expression).toBe('expression'); + expect(CaptureKind.Identifier).toBe('identifier'); + expect(CaptureKind.TypeReference).toBe('type-reference'); + expect(CaptureKind.Statement).toBe('statement'); + }); + }); + + describe('factory functions', () => { + test('expr() creates capture with Expression kind', () => { + const c = expr('x'); + expect(c.getName()).toBe('x'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + + test('ident() creates capture with Identifier kind', () => { + const c = ident('name'); + expect(c.getName()).toBe('name'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); + }); + + test('typeRef() creates capture with TypeReference kind', () => { + const c = typeRef('t'); + expect(c.getName()).toBe('t'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.TypeReference); + }); + + test('stmt() creates capture with Statement kind', () => { + const c = stmt('s'); + expect(c.getName()).toBe('s'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Statement); + }); + + test('factory functions work without a name', () => { + const e = expr(); + const i = ident(); + const t = typeRef(); + const s = stmt(); + expect(e.getName()).toMatch(/^unnamed_/); + expect(i.getName()).toMatch(/^unnamed_/); + expect(t.getName()).toMatch(/^unnamed_/); + expect(s.getName()).toMatch(/^unnamed_/); + }); + + test('factory functions accept options', () => { + const c = expr({ + name: 'x', + constraint: (node) => true + }); + expect(c.getName()).toBe('x'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + expect(c.getConstraint()).toBeDefined(); + }); + + test('factory functions accept variadic options', () => { + const c = expr({ + name: 'args', + variadic: true + }); + expect(c.getName()).toBe('args'); + expect(c.isVariadic()).toBe(true); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + }); + + describe('backwards compatibility', () => { + test('capture() defaults to Expression kind', () => { + const c = capture('x'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + + test('capture() with options defaults to Expression kind', () => { + const c = capture({ name: 'x' }); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + + test('capture() can override kind via options', () => { + const c = capture({ name: 'x', kind: CaptureKind.Identifier }); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); + }); + }); + + describe('namespace access on capture', () => { + test('capture.expr() works', () => { + const c = capture.expr('x'); + expect(c.getName()).toBe('x'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + + test('capture.ident() works', () => { + const c = capture.ident('n'); + expect(c.getName()).toBe('n'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); + }); + + test('capture.typeRef() works', () => { + const c = capture.typeRef('t'); + expect(c.getName()).toBe('t'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.TypeReference); + }); + + test('capture.stmt() works', () => { + const c = capture.stmt('s'); + expect(c.getName()).toBe('s'); + expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Statement); + }); + }); + + describe('any() with kinds', () => { + test('any() defaults to Expression kind', () => { + const a = any(); + expect((a as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); + }); + + test('any() can specify kind via options', () => { + const a = any({ kind: CaptureKind.Identifier }); + expect((a as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); + }); + }); + + describe('usage in patterns', () => { + test('factory captures work in pattern template literals', () => { + const e = expr('x'); + const n = ident('method'); + const p = pattern`${n}(${e})`; + expect(p).toBeInstanceOf(Pattern); + expect(p.captures.length).toBe(2); + }); + }); +}); From 24783d70a0ad1c39a9ba83ef44979e50597e1227 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 07:34:15 +0100 Subject: [PATCH 2/7] JS: Migrate templating tests to use kind-specific capture factories Replace generic capture() calls with expr(), ident(), and stmt() across all templating tests to document the semantic intent of each capture: - expr() for expression captures (operands, arguments, conditions) - ident() for identifier captures (function names, property names) - stmt() for statement captures (variadic statement bodies) Only basic.test.ts retains capture() since it tests the base function. --- .../javascript/templating/builder-api.test.ts | 34 +++--- .../templating/capture-constraints.test.ts | 78 ++++++------ .../capture-property-access.test.ts | 10 +- .../templating/capture-types.test.ts | 20 +-- .../templating/dependencies.test.ts | 8 +- .../templating/flatten-block.test.ts | 14 +-- .../javascript/templating/from-recipe.test.ts | 6 +- .../templating/lenient-type-matching.test.ts | 24 ++-- .../test/javascript/templating/match.test.ts | 16 +-- .../templating/non-capturing-any.test.ts | 16 +-- .../templating/pattern-debug-logging.test.ts | 26 ++-- .../templating/pattern-debug.test.ts | 12 +- .../templating/pattern-matching.test.ts | 4 +- .../javascript/templating/raw-code.test.ts | 20 +-- .../templating/replace-with-context.test.ts | 14 +-- .../javascript/templating/replace.test.ts | 10 +- .../javascript/templating/rewrite.test.ts | 114 +++++++++--------- .../statement-expression-wrapping.test.ts | 10 +- .../templating/unnamed-capture.test.ts | 20 +-- .../templating/variadic-array-proxy.test.ts | 18 +-- .../templating/variadic-basic.test.ts | 12 +- .../templating/variadic-constraints.test.ts | 18 +-- .../templating/variadic-container.test.ts | 32 ++--- .../templating/variadic-expansion.test.ts | 28 ++--- .../templating/variadic-marker.test.ts | 10 +- .../templating/variadic-matching.test.ts | 20 +-- .../templating/variadic-statement.test.ts | 30 ++--- 27 files changed, 312 insertions(+), 312 deletions(-) diff --git a/rewrite-javascript/rewrite/test/javascript/templating/builder-api.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/builder-api.test.ts index 218e46341a1..898e8f18f0e 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/builder-api.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/builder-api.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, Capture, JavaScriptVisitor, Pattern, template, Template, typescript} from "../../../src/javascript"; +import {expr, Capture, JavaScriptVisitor, Pattern, template, Template, typescript} from "../../../src/javascript"; import {Expression, J} from "../../../src/java"; describe('Builder API', () => { @@ -31,7 +31,7 @@ describe('Builder API', () => { }); test('creates template with code and parameters', () => { - const value = capture('value'); + const value = expr('value'); const tmpl = Template.builder() .code('const x = ') .param(value) @@ -62,7 +62,7 @@ describe('Builder API', () => { test('handles conditional construction', async () => { const needsValidation = true; - const value = capture('value'); + const value = expr('value'); const builder = Template.builder().code('function validate(x) {'); if (needsValidation) { @@ -100,7 +100,7 @@ describe('Builder API', () => { const argCount = 3; for (let i = 0; i < argCount; i++) { if (i > 0) builder.code(', '); - builder.param(capture(`arg${i}`)); + builder.param(expr(`arg${i}`)); } builder.code(')'); @@ -120,8 +120,8 @@ describe('Builder API', () => { }); test('handles multiple consecutive param calls', () => { - const a = capture('a'); - const b = capture('b'); + const a = expr('a'); + const b = expr('b'); const tmpl = Template.builder() .param(a) .param(b) @@ -179,7 +179,7 @@ describe('Builder API', () => { }); test('creates pattern with code and captures', () => { - const value = capture('value'); + const value = expr('value'); const pat = Pattern.builder() .code('const x = ') .capture(value) @@ -189,8 +189,8 @@ describe('Builder API', () => { }); test('creates pattern equivalent to pattern literal', async () => { - const left = capture('left'); - const right = capture('right'); + const left = expr('left'); + const right = expr('right'); // Using builder const builderPat = Pattern.builder() @@ -222,7 +222,7 @@ describe('Builder API', () => { const captures: Capture[] = []; for (let i = 0; i < argCount; i++) { if (i > 0) builder.code(', '); - const cap = capture(); + const cap = expr(); captures.push(cap); builder.capture(cap); } @@ -283,8 +283,8 @@ describe('Builder API', () => { }); test('handles multiple consecutive capture calls', () => { - const a = capture('a'); - const b = capture('b'); + const a = expr('a'); + const b = expr('b'); const pat = Pattern.builder() .capture(a) .capture(b) @@ -294,7 +294,7 @@ describe('Builder API', () => { }); test('pattern from builder can be configured', () => { - const value = capture('value'); + const value = expr('value'); const pat = Pattern.builder() .code('const x = ') .capture(value) @@ -312,8 +312,8 @@ describe('Builder API', () => { describe('Builder API Integration', () => { test('pattern and template builders work together', async () => { - const left = capture('left'); - const right = capture('right'); + const left = expr('left'); + const right = expr('right'); const pat = Pattern.builder() .capture(left) @@ -349,7 +349,7 @@ describe('Builder API', () => { const builder = Pattern.builder().code('processArgs('); for (let i = 0; i < expectedArgs.length; i++) { if (i > 0) builder.code(', '); - builder.capture(capture(expectedArgs[i])); + builder.capture(expr(expectedArgs[i])); } builder.code(')'); @@ -359,7 +359,7 @@ describe('Builder API', () => { const tmplBuilder = Template.builder().code('processArgs('); for (let i = expectedArgs.length - 1; i >= 0; i--) { if (i < expectedArgs.length - 1) tmplBuilder.code(', '); - tmplBuilder.param(capture(expectedArgs[i])); + tmplBuilder.param(expr(expectedArgs[i])); } tmplBuilder.code(')'); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts index 332cc8c2bd1..d638ce76517 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-constraints.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {Cursor} from "../../../src"; -import {and, capture, JavaScriptParser, not, or, pattern} from "../../../src/javascript"; +import {and, expr, JavaScriptParser, not, or, pattern} from "../../../src/javascript"; import {isBinary, J} from "../../../src/java"; describe('Capture Constraints', () => { @@ -43,7 +43,7 @@ describe('Capture Constraints', () => { describe('Simple constraints', () => { test('number constraint with both success and failure cases', async () => { - const value = capture({ + const value = expr({ constraint: (node) => typeof node.value === 'number' && node.value > 100 }); const pat = pattern`${value}`; @@ -59,7 +59,7 @@ describe('Capture Constraints', () => { }); test('string constraint with both success and failure cases', async () => { - const text = capture({ + const text = expr({ constraint: (node) => typeof node.value === 'string' && node.value.startsWith('hello') }); const pat = pattern`${text}`; @@ -77,7 +77,7 @@ describe('Capture Constraints', () => { describe('and() composition', () => { test('validates all constraints must pass', async () => { - const value = capture({ + const value = expr({ constraint: and( (node) => typeof node.value === 'number', (node) => (node.value as number) > 50, @@ -100,7 +100,7 @@ describe('Capture Constraints', () => { describe('or() composition', () => { test('validates at least one constraint must pass', async () => { - const value = capture({ + const value = expr({ constraint: or( (node) => typeof node.value === 'string', (node) => typeof node.value === 'number' && node.value > 1000 @@ -124,7 +124,7 @@ describe('Capture Constraints', () => { describe('not() composition', () => { test('inverts constraint result', async () => { - const value = capture({ + const value = expr({ constraint: not((node) => typeof node.value === 'string') }); const pat = pattern`${value}`; @@ -143,7 +143,7 @@ describe('Capture Constraints', () => { test('combines and, or, not', async () => { // Match numbers > 50 that are either even OR > 200 // But NOT divisible by 10 - const value = capture({ + const value = expr({ constraint: and( (node) => typeof node.value === 'number', (node) => (node.value as number) > 50, @@ -178,7 +178,7 @@ describe('Capture Constraints', () => { describe('Constraints on identifiers', () => { test('validates identifier names with both success and failure', async () => { - const name = capture({ + const name = expr({ constraint: (node) => node.simpleName.startsWith('get') && !node.simpleName.includes('_') }); const pat = pattern`${name}`; @@ -196,7 +196,7 @@ describe('Capture Constraints', () => { describe('Constraints in complex patterns', () => { test('applies constraint in method invocation', async () => { - const arg = capture({ + const arg = expr({ constraint: (node) => typeof node.value === 'number' && node.value > 10 }); const pat = pattern`foo(${arg})`; @@ -213,10 +213,10 @@ describe('Capture Constraints', () => { }); test('multiple captures with different constraints', async () => { - const left = capture({ + const left = expr({ constraint: (node) => typeof node.value === 'number' && node.value > 5 }); - const right = capture({ + const right = expr({ constraint: (node) => typeof node.value === 'number' && node.value < 5 }); const pat = pattern`${left} + ${right}`; @@ -237,7 +237,7 @@ describe('Capture Constraints', () => { let constraintCalled = false; let parentIsBinary = false; - const left = capture({ + const left = expr({ constraint: (node, context) => { constraintCalled = true; // Check that cursor can navigate to parent @@ -251,8 +251,8 @@ describe('Capture Constraints', () => { const pat = pattern`${left} + 20`; // Match against a binary expression - const expr = await parseExpression('10 + 20') as J.Binary; - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('10 + 20') as J.Binary; + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); expect(constraintCalled).toBe(true); @@ -264,7 +264,7 @@ describe('Capture Constraints', () => { // Test 1: Cursor at ast root when not explicitly provided let cursorReceived: any = 'not-called'; let astNode: J | undefined; - const value1 = capture({ + const value1 = expr({ constraint: (node, context) => { cursorReceived = context.cursor; astNode = node; @@ -282,7 +282,7 @@ describe('Capture Constraints', () => { // Test 2: Composition functions forward cursor let cursorReceivedInAnd: any = 'not-called'; - const value2 = capture({ + const value2 = expr({ constraint: and( (node) => typeof node.value === 'number', (node, context) => { @@ -299,7 +299,7 @@ describe('Capture Constraints', () => { expect(cursorReceivedInAnd.parent?.value).toBe(await parseExpression('50')); // Test 3: or composition with cursor-aware constraints - const value3 = capture({ + const value3 = expr({ constraint: or( (node) => typeof node.value === 'string', (node, context) => { @@ -319,7 +319,7 @@ describe('Capture Constraints', () => { expect(match3b).toBeUndefined(); // Test 4: not composition with cursor-aware constraint - const value4 = capture({ + const value4 = expr({ constraint: not((node, context) => { // Reject if cursor has a grandparent (i.e., not at root) return context.cursor.parent?.parent !== undefined; @@ -339,7 +339,7 @@ describe('Capture Constraints', () => { let capturedNode: J | undefined; let cursorValue: J | undefined; - const left = capture({ + const left = expr({ constraint: (node, context) => { capturedNode = node; cursorValue = context.cursor.value as J; @@ -349,8 +349,8 @@ describe('Capture Constraints', () => { const pat = pattern`${left} + 20`; // Match against the expression - const expr = await parseExpression('10 + 20') as J.Binary; - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('10 + 20') as J.Binary; + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); expect(capturedNode).toBeDefined(); @@ -367,7 +367,7 @@ describe('Capture Constraints', () => { let capturedArg: J | undefined; let parentKind: typeof J.Kind | undefined; - const arg = capture({ + const arg = expr({ constraint: (node, context) => { capturedArg = node; // The cursor's parent should be at a higher level in the tree @@ -380,8 +380,8 @@ describe('Capture Constraints', () => { }); const pat = pattern`foo(${arg})`; - const expr = await parseExpression('foo(42)') as J.MethodInvocation; - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('foo(42)') as J.MethodInvocation; + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); expect(capturedArg).toBeDefined(); @@ -397,8 +397,8 @@ describe('Capture Constraints', () => { describe('Constraints with capture access', () => { test('constraint can access previously matched captures', async () => { // Pattern: ${left} + ${right} where right must equal left - const left = capture('left'); - const right = capture({ + const left = expr('left'); + const right = expr({ name: 'right', constraint: (node, context) => { const leftValue = context.captures.get(left); @@ -421,8 +421,8 @@ describe('Capture Constraints', () => { test('constraint can access captures by string name', async () => { // Test that captures can be accessed by string name as well as Capture object - const left = capture('left'); - const right = capture({ + const left = expr('left'); + const right = expr({ name: 'right', constraint: (node, context) => { // Access by string name instead of Capture object @@ -445,8 +445,8 @@ describe('Capture Constraints', () => { let hasLeftByCapture = false; let hasLeftByString = false; - const left = capture('hasTestLeft'); - const right = capture({ + const left = expr('hasTestLeft'); + const right = expr({ name: 'hasTestRight', constraint: (node, context) => { // Verify has() works with both Capture object and string @@ -457,8 +457,8 @@ describe('Capture Constraints', () => { }); const pat = pattern`${left} + ${right}`; - const expr = await parseExpression('15 + 25'); - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('15 + 25'); + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); expect(hasLeftByCapture).toBe(true); expect(hasLeftByString).toBe(true); @@ -466,8 +466,8 @@ describe('Capture Constraints', () => { test('multiple captures with dependent constraints', async () => { // Pattern: ${a} + ${b} + ${c} where b > a and c > b - const a = capture('a'); - const b = capture({ + const a = expr('a'); + const b = expr({ name: 'b', constraint: (node, context) => { const aValue = context.captures.get(a) as J.Literal | undefined; @@ -477,7 +477,7 @@ describe('Capture Constraints', () => { node.value > aValue.value; } }); - const c = capture({ + const c = expr({ name: 'c', constraint: (node, context) => { const bValue = context.captures.get(b) as J.Literal | undefined; @@ -514,8 +514,8 @@ describe('Capture Constraints', () => { let leftWasAvailable = false; let rightSawItself = false; - const left = capture('stateTestLeft'); - const right = capture({ + const left = expr('stateTestLeft'); + const right = expr({ name: 'stateTestRight', constraint: (node, context) => { rightConstraintCalled = true; @@ -532,8 +532,8 @@ describe('Capture Constraints', () => { }); const pat = pattern`${left} + ${right}`; - const expr = await parseExpression('30 + 40'); - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('30 + 40'); + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); expect(rightConstraintCalled).toBe(true); expect(leftWasAvailable).toBe(true); // Should see left capture diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts index 8dc75de1abf..17c7b039e88 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-property-access.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, pattern, template, typescript} from "../../../src/javascript"; import {J} from "../../../src/java"; describe('forwardRef pattern with replacement', () => { @@ -23,7 +23,7 @@ describe('forwardRef pattern with replacement', () => { test('capture with property access', () => { // Test replacing forwardRef(Type) with Type // This captures the argument to forwardRef and replaces it with just the argument - const arg = capture(); + const arg = expr(); const pat = pattern`forwardRef(${arg})`; const tmpl = template`${arg}`; @@ -62,7 +62,7 @@ describe('forwardRef pattern with replacement', () => { test('capture with nested property access in template', () => { // Test: forwardRef((props) => ...) becomes (props) => ... // This tests a more complex case where we're matching the full forwardRef pattern - const componentDef = capture(); + const componentDef = expr(); const pat = pattern`forwardRef(${componentDef})`; const tmpl = template`${componentDef}`; @@ -104,7 +104,7 @@ describe('forwardRef pattern with replacement', () => { test('capture with property access in template', () => { // Test accessing properties of captured nodes in templates - const method = capture('method'); + const method = expr('method'); const pat = pattern`foo(${method})`; // Access the name property of the captured method invocation @@ -148,7 +148,7 @@ describe('forwardRef pattern with replacement', () => { test('capture with array access in template', () => { // Test accessing array elements via bracket notation - const invocation = capture('invocation'); + const invocation = expr('invocation'); const pat = pattern`${invocation}`; // Access the first argument via array index diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts index 0553b26d918..e6fcff572af 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-types.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, JS, pattern, template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, JS, pattern, template, typescript} from "../../../src/javascript"; import {J, Type} from "../../../src/java"; describe('capture types', () => { @@ -24,7 +24,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture with a type annotation - const x = capture({type: 'boolean'}); + const x = expr({type: 'boolean'}); const pat = pattern`${x} || false`; const match = await pat.match(binary, this.cursor); @@ -45,7 +45,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture with a type annotation - const condition = capture({type: 'boolean'}); + const condition = expr({type: 'boolean'}); const pat = pattern`${condition} && true`; const match = await pat.match(binary, this.cursor); @@ -66,8 +66,8 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create captures with different type annotations - const x = capture({type: 'number'}); - const y = capture({type: 'number'}); + const x = expr({type: 'number'}); + const y = expr({type: 'number'}); const pat = pattern`${x} + ${y}`; const match = await pat.match(binary, this.cursor); @@ -88,7 +88,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture without a type annotation - const x = capture(); + const x = expr(); const pat = pattern`${x} + 1`; const match = await pat.match(binary, this.cursor); @@ -148,7 +148,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture with Type.Primitive.Boolean - const condition = capture({type: Type.Primitive.Boolean}); + const condition = expr({type: Type.Primitive.Boolean}); const pat = pattern`${condition} && true`; const match = await pat.match(binary, this.cursor); @@ -168,7 +168,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture with Type.Primitive.String - const str = capture({type: Type.Primitive.String}); + const str = expr({type: Type.Primitive.String}); const pat = pattern`${str} + ""`; const match = await pat.match(binary, this.cursor); @@ -188,7 +188,7 @@ describe('capture types', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { // Create a capture with Type.Primitive.Double - const num = capture({type: Type.Primitive.Double}); + const num = expr({type: Type.Primitive.Double}); const pat = pattern`${num} + 0`; const match = await pat.match(binary, this.cursor); @@ -217,7 +217,7 @@ describe('capture types', () => { } as Type.Array; // Use the array type in a capture within a pattern - const arr = capture({type: arrayType}); + const arr = expr({type: arrayType}); const pat = pattern`oldMethod(${arr})`; const match = await pat.match(method, this.cursor); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/dependencies.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/dependencies.test.ts index 50926781e8a..e3ae8b0a97d 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/dependencies.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/dependencies.test.ts @@ -17,7 +17,7 @@ */ import { _, - capture, + expr, JavaScriptParser, JavaScriptVisitor, MethodMatcher, @@ -302,7 +302,7 @@ describe('template dependencies integration', () => { // This builds on the existing functionality to ensure our semantic equality checking works const spec = new RecipeSpec(); const swapOperands = rewrite(() => { - const {left, right} = {left: capture(), right: capture()}; + const {left, right} = {left: expr(), right: expr()}; return { before: pattern`${left} + ${right}`, after: template`${right} + ${left}` @@ -348,7 +348,7 @@ describe('template dependencies integration', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, _p: any): Promise { // Create rewrite rules fresh for each invocation - const arg = capture(); + const arg = expr(); const replaceUtilIsArray = rewrite(() => ({ before: pattern`util.isArray(${arg})`.configure({ context: ["import * as util from 'util'"], @@ -450,7 +450,7 @@ describe('template dependencies integration', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, _p: any): Promise { - const dateArg = capture('dateArg'); + const dateArg = expr('dateArg'); // Single pattern that matches both isDate(x) and util.isDate(x) via type attribution const replaceIsDate = rewrite(() => ({ before: pattern`isDate(${dateArg})`.configure({ diff --git a/rewrite-javascript/rewrite/test/javascript/templating/flatten-block.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/flatten-block.test.ts index 8e6efda0e30..a12520711de 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/flatten-block.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/flatten-block.test.ts @@ -19,15 +19,15 @@ import {typescript} from "../../../src/javascript"; import {ExecutionContext} from "../../../src"; import {JavaScriptVisitor} from "../../../src/javascript/visitor"; import {J} from "../../../src/java"; -import {rewrite, pattern, template, capture, flattenBlock} from "../../../src/javascript/templating"; +import {rewrite, pattern, template, expr, flattenBlock} from "../../../src/javascript/templating"; describe('flattenBlock', () => { test('flattens block statements into parent block', async () => { const spec = new RecipeSpec(); - const cond = capture('cond'); - const arr = capture('arr'); - const cb = capture('cb'); + const cond = expr('cond'); + const arr = expr('arr'); + const cb = expr('cb'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitReturn(ret: J.Return, ctx: ExecutionContext): Promise { @@ -74,9 +74,9 @@ describe('flattenBlock', () => { test('does not affect non-matching returns', async () => { const spec = new RecipeSpec(); - const cond = capture('cond'); - const arr = capture('arr'); - const cb = capture('cb'); + const cond = expr('cond'); + const arr = expr('arr'); + const cb = expr('cb'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitReturn(ret: J.Return, ctx: ExecutionContext): Promise { diff --git a/rewrite-javascript/rewrite/test/javascript/templating/from-recipe.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/from-recipe.test.ts index e1df6e95dba..ce279a65b6c 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/from-recipe.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/from-recipe.test.ts @@ -15,7 +15,7 @@ */ import {fromVisitor, RecipeSpec} from "../../../src/test"; import { - capture, + expr, fromRecipe, JavaScriptVisitor, pattern, @@ -88,8 +88,8 @@ describe('fromRecipe', () => { // Rule: Swap operands const swapRule = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`${capture('b')} + ${capture('a')}` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`${expr('b')} + ${expr('a')}` })); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { diff --git a/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts index c066957811d..26586c874ac 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/lenient-type-matching.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, npm, packageJson, pattern, template, tsx, typescript} from "../../../src/javascript"; +import {expr, ident, JavaScriptVisitor, npm, packageJson, pattern, template, tsx, typescript} from "../../../src/javascript"; import {J, Type} from "../../../src/java"; import {withDir} from "tmp-promise"; @@ -26,7 +26,7 @@ describe('lenient type matching in patterns', () => { const tempDir = repo.path; // Pattern with unconstrained captures for props, ref, and body - const pat = pattern`React.forwardRef((${capture('props')}, ${capture('ref')}) => ${capture('body')})` + const pat = pattern`React.forwardRef((${expr('props')}, ${expr('ref')}) => ${expr('body')})` .configure({ context: [`import * as React from 'react'`], dependencies: {'@types/react': '^18.0.0'} @@ -91,7 +91,7 @@ describe('lenient type matching in patterns', () => { test('untyped function pattern matches typed function with return type', async () => { // Pattern with untyped function (no return type) should match typed function - const pat = pattern`function ${capture('name')}() { return "hello"; }`; + const pat = pattern`function ${ident('name')}() { return "hello"; }`; const testCode = ` function greet(): string { return "hello"; } @@ -123,13 +123,13 @@ function greet(): string { return "hello"; } test('untyped pattern matches and transforms with template replacement', async () => { // Pattern without type annotations matches typed code and replaces it with template // This demonstrates: matching untyped pattern against typed code + capturing + template replacement - const pat = pattern`forwardRef(function ${capture('name')}(props, ref) { return null; })` + const pat = pattern`forwardRef(function ${ident('name')}(props, ref) { return null; })` .configure({ context: [`import { forwardRef } from 'react'`] }); // Template that uses the captured name as an identifier - const tmpl = template`console.log(${capture('name')})`; + const tmpl = template`console.log(${ident('name')})`; spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(methodInvocation: J.MethodInvocation, _p: any): Promise { @@ -158,7 +158,7 @@ function greet(): string { return "hello"; } test('strict type matching mode rejects untyped pattern against typed code', async () => { // Pattern with strict type matching (lenientTypeMatching: false) should NOT match typed function - const pat = pattern`function ${capture('name')}() { return "hello"; }` + const pat = pattern`function ${ident('name')}() { return "hello"; }` .configure({ lenientTypeMatching: false }); @@ -189,7 +189,7 @@ function greet(): string { return "hello"; } test('lenient type matching can be explicitly enabled', async () => { // Pattern with explicit lenient type matching should match typed function - const pat = pattern`function ${capture('name')}() { return "hello"; }` + const pat = pattern`function ${ident('name')}() { return "hello"; }` .configure({ lenientTypeMatching: true }); @@ -223,7 +223,7 @@ function greet(): string { return "hello"; } test('strict mode with matching types does match', async () => { // Pattern with strict type matching and matching return type SHOULD match - const pat = pattern`function ${capture('name')}(): string { return "hello"; }` + const pat = pattern`function ${ident('name')}(): string { return "hello"; }` .configure({ lenientTypeMatching: false }); @@ -261,7 +261,7 @@ function greet(): string { return "hello"; } const tempDir = repo.path; // Pattern uses the original import name - const pat = pattern`isDate(${capture('arg')})` + const pat = pattern`isDate(${expr('arg')})` .configure({ context: [`import { isDate } from 'node:util/types'`], lenientTypeMatching: false, // Strict type matching @@ -319,7 +319,7 @@ function greet(): string { return "hello"; } const tempDir = repo.path; // Pattern uses the original import name, with lenient mode (default) - const pat = pattern`isDate(${capture('arg')})` + const pat = pattern`isDate(${expr('arg')})` .configure({ context: [`import { isDate } from 'node:util/types'`], // lenientTypeMatching defaults to true @@ -375,7 +375,7 @@ function greet(): string { return "hello"; } test('aliased import matching without type attribution (import-based resolution)', async () => { // Pattern uses the original import name // Testing if parser tracks import origins (module + original name) without type attribution - const pat = pattern`isDate(${capture('arg')})` + const pat = pattern`isDate(${expr('arg')})` .configure({ context: [`import { isDate } from 'node:util/types'`] // Note: NO dependencies - pattern won't have type attribution @@ -419,7 +419,7 @@ function greet(): string { return "hello"; } const tempDir = repo.path; // Pattern uses named import: forwardRef() - const pat = pattern`forwardRef(${capture('fn')})` + const pat = pattern`forwardRef(${expr('fn')})` .configure({ context: [`import { forwardRef } from 'react'`], dependencies: {'@types/react': '^18.0.0'} diff --git a/rewrite-javascript/rewrite/test/javascript/templating/match.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/match.test.ts index bf913530d4c..ea4479d6423 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/match.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/match.test.ts @@ -16,7 +16,7 @@ import { fromVisitor, RecipeSpec } from "../../../src/test"; import { Cursor } from "../../../src"; import { - capture, + expr, JavaScriptParser, JavaScriptVisitor, pattern, @@ -63,7 +63,7 @@ describe('match extraction', () => { override async visitBinary(binary: J.Binary, _p: any): Promise { // Create capture objects - const left = capture(), right = capture(); + const left = expr(), right = expr(); // Create a pattern that matches "a + b" using the capture objects const m = await pattern`${left} + ${right}`.match(binary, this.cursor); @@ -85,8 +85,8 @@ describe('match extraction', () => { override async visitBinary(binary: J.Binary, p: any): Promise { const swapOperands = rewrite(() => ({ - before: pattern`${capture('left')} + ${capture('right')}`, - after: template`${capture('right')} + ${capture('left')}` + before: pattern`${expr('left')} + ${expr('right')}`, + after: template`${expr('right')} + ${expr('left')}` }) ); return await swapOperands.tryOn(this.cursor, binary); @@ -104,7 +104,7 @@ describe('match extraction', () => { override async visitBinary(binary: J.Binary, p: any): Promise { if (binary.operator.element === J.Binary.Type.Addition) { // Create capture objects without explicit names - const { left, right } = { left: capture(), right: capture() }; + const { left, right } = { left: expr(), right: expr() }; // Create a pattern that matches "a + b" using the capture objects const m = await pattern`${left} + ${right}`.match(binary, this.cursor); @@ -137,10 +137,10 @@ describe('match extraction', () => { override async visitBinary(binary: J.Binary, p: any): Promise { if (binary.operator.element === J.Binary.Type.Addition) { // Use inline named captures - const m = await pattern`${capture('left')} + ${capture('right')}`.match(binary, this.cursor); + const m = await pattern`${expr('left')} + ${expr('right')}`.match(binary, this.cursor); if (m) { // Can retrieve by string name - return await template`${capture('right')} + ${capture('left')}`.apply(binary, this.cursor, { values: m }); + return await template`${expr('right')} + ${expr('left')}`.apply(binary, this.cursor, { values: m }); } } return binary; @@ -157,7 +157,7 @@ describe('match extraction', () => { // Verify that specifying a non-existent package causes npm install to fail const nonExistentPackage = 'this-package-definitely-does-not-exist-12345'; - const pat = pattern`${capture('left')} + ${capture('right')}` + const pat = pattern`${expr('left')} + ${expr('right')}` .configure({ context: [`import { SomeType } from "${nonExistentPackage}"`], dependencies: { [nonExistentPackage]: '^1.0.0' } diff --git a/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts index 321ce3e9e5c..780dd59c55e 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/non-capturing-any.test.ts @@ -16,7 +16,7 @@ import { Cursor } from "../../../src"; import { any, - capture, + expr, JavaScriptParser, JavaScriptVisitor, pattern, @@ -174,11 +174,11 @@ describe('Non-Capturing any() Function', () => { describe('Mixed captures and any()', () => { test('captures important value, ignores others', async () => { - const important = capture('important'); + const important = expr('important'); const pat = pattern`compute(${any()}, ${important}, ${any()})`; - const expr = await parseExpression('compute(10, 20, 30)'); - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('compute(10, 20, 30)'); + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); // Only the middle value should be captured @@ -191,12 +191,12 @@ describe('Non-Capturing any() Function', () => { }); test('first capture, rest any()', async () => { - const first = capture('first'); + const first = expr('first'); const rest = any({ variadic: true }); const pat = pattern`foo(${first}, ${rest})`; - const expr = await parseExpression('foo(1, 2, 3, 4)'); - const match = await pat.match(expr, new Cursor(expr, undefined)); + const parsed = await parseExpression('foo(1, 2, 3, 4)'); + const match = await pat.match(parsed, new Cursor(parsed, undefined)); expect(match).toBeDefined(); // First should be captured @@ -234,7 +234,7 @@ describe('Non-Capturing any() Function', () => { test('mix captures and any() in rewrite', async () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { - const value = capture('value'); + const value = expr('value'); return await rewrite(() => ({ before: pattern`process(${any()}, ${value})`, after: template`process(${value})` diff --git a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts index 2f7b7d19008..501082090e2 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug-logging.test.ts @@ -1,5 +1,5 @@ import {type MockInstance} from 'vitest'; -import {capture, isExpressionStatement, JavaScriptParser, JS, pattern} from '../../../src/javascript'; +import {expr, isExpressionStatement, JavaScriptParser, JS, pattern} from '../../../src/javascript'; import {J} from '../../../src/java'; describe('Pattern Debug Logging', () => { @@ -27,7 +27,7 @@ describe('Pattern Debug Logging', () => { }); test('no debug logging by default', async () => { - const value = capture('value'); + const value = expr('value'); const pat = pattern`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -38,7 +38,7 @@ describe('Pattern Debug Logging', () => { }); test('call-level debug: { debug: true }', async () => { - const value = capture('value'); + const value = expr('value'); const pat = pattern`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -53,7 +53,7 @@ describe('Pattern Debug Logging', () => { }); test('pattern-level debug: pattern({ debug: true })', async () => { - const value = capture('value'); + const value = expr('value'); const pat = pattern({ debug: true })`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -69,7 +69,7 @@ describe('Pattern Debug Logging', () => { test('global debug: PATTERN_DEBUG=true', async () => { process.env.PATTERN_DEBUG = 'true'; - const value = capture('value'); + const value = expr('value'); const pat = pattern`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -85,7 +85,7 @@ describe('Pattern Debug Logging', () => { test('precedence: call > pattern > global', async () => { process.env.PATTERN_DEBUG = 'true'; - const value = capture('value'); + const value = expr('value'); // Pattern has debug: false, but global is true const pat = pattern({ debug: false })`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -100,7 +100,7 @@ describe('Pattern Debug Logging', () => { test('explicit debug: false disables when global is true', async () => { process.env.PATTERN_DEBUG = 'true'; - const value = capture('value'); + const value = expr('value'); const pat = pattern`console.log(${value})`; const node = await parseExpression('console.log(42)'); @@ -112,7 +112,7 @@ describe('Pattern Debug Logging', () => { }); test('logs failure with path and explanation', async () => { - const value = capture('value'); + const value = expr('value'); const pat = pattern`console.log(${value})`; // Use console.error instead - should not match const node = await parseExpression('console.error(42)'); @@ -132,8 +132,8 @@ describe('Pattern Debug Logging', () => { }); test('pattern source includes capture names', async () => { - const x = capture('x'); - const y = capture('y'); + const x = expr('x'); + const y = expr('y'); const pat = pattern({ debug: true })`foo(${x}, ${y})`; const node = await parseExpression('foo(1, 2)'); @@ -145,7 +145,7 @@ describe('Pattern Debug Logging', () => { }); test('variadic captures show array format', async () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern({ debug: true })`console.log(${args})`; const node = await parseExpression('console.log(1, 2, 3)'); @@ -160,8 +160,8 @@ describe('Pattern Debug Logging', () => { }); test('shows path for nested mismatch', async () => { - const x = capture('x'); - const y = capture('y'); + const x = expr('x'); + const y = expr('y'); const pat = pattern({ debug: true })`${x} + ${y}`; // Pattern expects addition, but we provide subtraction const node = await parseExpression('a - b'); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts index 1dbf7f87d3d..13f8fe7011f 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/pattern-debug.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, isExpressionStatement, JavaScriptParser, JS, pattern} from '../../../src/javascript'; +import {expr, isExpressionStatement, JavaScriptParser, JS, pattern} from '../../../src/javascript'; import {J} from '../../../src/java'; /** @@ -35,7 +35,7 @@ describe('Pattern Debugging', () => { } test('successful match returns matched=true with result', async () => { - const x = capture('x'); + const x = expr('x'); const pat = pattern`console.log(${x})`; const node = await parseExpression('console.log(42)'); @@ -63,7 +63,7 @@ describe('Pattern Debugging', () => { }); test('constraint failure provides detailed explanation', async () => { - const value = capture({ + const value = expr({ constraint: (node: J) => { // This constraint will fail return false; @@ -81,7 +81,7 @@ describe('Pattern Debugging', () => { }); test('debug log contains constraint evaluation entries', async () => { - const value = capture({ + const value = expr({ constraint: (node: J) => true }); const pat = pattern`${value}`; @@ -144,7 +144,7 @@ describe('Pattern Debugging', () => { }); test('variadic constraint failure is logged in debug', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes: J[]) => { // Require exactly 2 arguments @@ -244,7 +244,7 @@ describe('Pattern Debugging', () => { }); test('path tracking includes intermediate steps for object destructuring', async () => { - const name = capture(); + const name = expr(); const pat = pattern`const {${name}} = obj;`; // Target has two variables, pattern expects one diff --git a/rewrite-javascript/rewrite/test/javascript/templating/pattern-matching.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/pattern-matching.test.ts index e2a43d6c757..0b7afa065d2 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/pattern-matching.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/pattern-matching.test.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, pattern} from "../../../src/javascript"; +import {expr, pattern} from "../../../src/javascript"; describe('Pattern Matching', () => { describe('Pattern', () => { test('creates a pattern with correct template parts and captures', () => { - const p = pattern`${capture()} + ${capture()}`; + const p = pattern`${expr()} + ${expr()}`; expect(p.templateParts).toBeDefined(); expect(p.captures).toBeDefined(); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts index 271bde47224..580ebdb83f8 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/raw-code.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, raw, rewrite, template, typescript, _} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, pattern, raw, rewrite, template, typescript, _} from "../../../src/javascript"; import {J} from "../../../src/java"; describe('raw() function', () => { @@ -23,7 +23,7 @@ describe('raw() function', () => { describe('construction-time string interpolation', () => { test('splices raw code directly into template', () => { const methodName = "info"; - const msg = capture('msg'); + const msg = expr('msg'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { @@ -51,7 +51,7 @@ describe('raw() function', () => { test('works with multiple raw() interpolations', () => { const obj = "console"; const method = "log"; - const msg = capture('msg'); + const msg = expr('msg'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(invocation: J.MethodInvocation, p: any): Promise { @@ -78,7 +78,7 @@ describe('raw() function', () => { test('works with operators', () => { const operator = ">="; - const value = capture('value'); + const value = expr('value'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { @@ -132,7 +132,7 @@ describe('raw() function', () => { describe('mixed with other parameters', () => { test('can mix raw() with capture()', () => { const prefix = "user"; - const value = capture('value'); + const value = expr('value'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { @@ -190,7 +190,7 @@ describe('raw() function', () => { describe('raw() in patterns', () => { test('matches pattern with raw() method name', async () => { const methodName = "log"; - const msg = capture('msg'); + const msg = expr('msg'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { @@ -215,7 +215,7 @@ describe('raw() function', () => { test('combines raw() in both pattern and template', async () => { const oldMethod = "warn"; const newMethod = "error"; - const msg = capture('msg'); + const msg = expr('msg'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { @@ -266,7 +266,7 @@ describe('raw() function', () => { test('multiple raw() in single pattern', async () => { const obj = "console"; const method = "log"; - const msg = capture('msg'); + const msg = expr('msg'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(invocation: J.MethodInvocation, p: any): Promise { @@ -290,8 +290,8 @@ describe('raw() function', () => { test('raw() with captures in complex pattern', async () => { const prefix = "user"; - const field = capture('field'); - const value = capture('value'); + const field = expr('field'); + const value = expr('value'); spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { diff --git a/rewrite-javascript/rewrite/test/javascript/templating/replace-with-context.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/replace-with-context.test.ts index 63d17463431..eaacf0e7d36 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/replace-with-context.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/replace-with-context.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, JavaScriptVisitor, pattern, rewrite, template, typescript} from '../../../src/javascript'; +import {expr, JavaScriptVisitor, pattern, rewrite, template, typescript} from '../../../src/javascript'; import {RecipeSpec, fromVisitor} from '../../../src/test'; import {J} from '../../../src/java'; @@ -25,8 +25,8 @@ describe('replace with context', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { const rule = rewrite(() => { - const left = capture("left"); - const right = capture("right"); + const left = expr("left"); + const right = expr("right"); return { before: [ pattern`${left} == ${right}`, @@ -53,13 +53,13 @@ describe('replace with context', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitBinary(binary: J.Binary, p: any): Promise { const rule = rewrite(() => { - const expr = capture(); + const e = expr(); return { before: [ - pattern`${expr} || false`, - pattern`false || ${expr}` + pattern`${e} || false`, + pattern`false || ${e}` ], - after: template`${expr}` + after: template`${e}` }; }); return await rule.tryOn(this.cursor, binary) || binary; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts index ab768757b9c..6bf35ba8e96 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/replace.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, pattern, template, typescript} from "../../../src/javascript"; import {Expression, J} from "../../../src/java"; import {create as produce} from "mutative"; import {produceAsync} from "../../../src"; @@ -93,7 +93,7 @@ describe('template2 replace', () => { }); // Use capture for late binding - myValue capture is looked up in the values map - const myValue = capture(); + const myValue = expr(); return template`${myValue}`.apply(literal, this.cursor, {values: new Map([[myValue, replacement]])}); } return literal; @@ -106,7 +106,7 @@ describe('template2 replace', () => { }); test('scalar capture preserves trailing semicolon', () => { - const arg = capture(); + const arg = expr(); const pat = pattern`foo(${arg})`; const tmpl = template`bar(${arg})`; @@ -129,7 +129,7 @@ describe('template2 replace', () => { }); test('scalar capture preserves comments', () => { - const arg = capture(); + const arg = expr(); const pat = pattern`oldFunc(${arg})`; const tmpl = template`newFunc(${arg})`; @@ -155,7 +155,7 @@ describe('template2 replace', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitMethodInvocation(method: J.MethodInvocation, p: any): Promise { if ((method.name as J.Identifier).simpleName === 'oldMethod' && method.select) { - const select = capture(); + const select = expr(); return await template`${select}.newMethod()`.apply( method, this.cursor, diff --git a/rewrite-javascript/rewrite/test/javascript/templating/rewrite.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/rewrite.test.ts index 3f3cad34485..0fa1033ee21 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/rewrite.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/rewrite.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, rewrite, template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, pattern, rewrite, template, typescript} from "../../../src/javascript"; import {J} from "../../../src/java"; describe('RewriteRule composition', () => { @@ -24,14 +24,14 @@ describe('RewriteRule composition', () => { test('chains two rules that both match', () => { // Rule 1: Swap operands of addition const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`${capture('b')} + ${capture('a')}` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`${expr('b')} + ${expr('a')}` })); // Rule 2: Change '1 + x' to '2 + x' const rule2 = rewrite(() => ({ - before: pattern`1 + ${capture('x')}`, - after: template`2 + ${capture('x')}` + before: pattern`1 + ${expr('x')}`, + after: template`2 + ${expr('x')}` })); const combined = rule1.andThen(rule2); @@ -51,14 +51,14 @@ describe('RewriteRule composition', () => { test('first rule matches, second does not', () => { // Rule 1: Swap operands of addition const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`${capture('b')} + ${capture('a')}` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`${expr('b')} + ${expr('a')}` })); // Rule 2: Change 'foo + x' to 'bar + x' (will not match after swap) const rule2 = rewrite(() => ({ - before: pattern`foo + ${capture('x')}`, - after: template`bar + ${capture('x')}` + before: pattern`foo + ${expr('x')}`, + after: template`bar + ${expr('x')}` })); const combined = rule1.andThen(rule2); @@ -79,13 +79,13 @@ describe('RewriteRule composition', () => { test('first rule does not match, returns undefined', () => { // Rule 1: Match subtraction const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`${capture('b')} - ${capture('a')}` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`${expr('b')} - ${expr('a')}` })); // Rule 2: This should never be called const rule2 = rewrite(() => ({ - before: pattern`${capture('x')} + ${capture('y')}`, + before: pattern`${expr('x')} + ${expr('y')}`, after: template`0` })); @@ -107,20 +107,20 @@ describe('RewriteRule composition', () => { test('chains three rules', () => { // Rule 1: Swap operands const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`${capture('b')} + ${capture('a')}` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`${expr('b')} + ${expr('a')}` })); // Rule 2: Change '1 + x' to '2 + x' const rule2 = rewrite(() => ({ - before: pattern`1 + ${capture('x')}`, - after: template`2 + ${capture('x')}` + before: pattern`1 + ${expr('x')}`, + after: template`2 + ${expr('x')}` })); // Rule 3: Change '2 + x' to '3 + x' const rule3 = rewrite(() => ({ - before: pattern`2 + ${capture('x')}`, - after: template`3 + ${capture('x')}` + before: pattern`2 + ${expr('x')}`, + after: template`3 + ${expr('x')}` })); const combined = rule1.andThen(rule2).andThen(rule3); @@ -141,14 +141,14 @@ describe('RewriteRule composition', () => { test('neither rule matches', () => { // Rule 1: Match subtraction const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`${capture('b')} - ${capture('a')}` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`${expr('b')} - ${expr('a')}` })); // Rule 2: Match multiplication const rule2 = rewrite(() => ({ - before: pattern`${capture('a')} * ${capture('b')}`, - after: template`${capture('b')} * ${capture('a')}` + before: pattern`${expr('a')} * ${expr('b')}`, + after: template`${expr('b')} * ${expr('a')}` })); const combined = rule1.andThen(rule2); @@ -171,13 +171,13 @@ describe('RewriteRule composition', () => { test('first rule matches, alternative is not tried', () => { // Rule 1: Match addition const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`${capture('b')} + ${capture('a')}` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`${expr('b')} + ${expr('a')}` })); // Rule 2: This should never be called when rule1 matches const rule2 = rewrite(() => ({ - before: pattern`${capture('x')} + ${capture('y')}`, + before: pattern`${expr('x')} + ${expr('y')}`, after: template`0` })); @@ -199,14 +199,14 @@ describe('RewriteRule composition', () => { test('first rule does not match, alternative matches', () => { // Rule 1: Match subtraction const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`${capture('b')} - ${capture('a')}` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`${expr('b')} - ${expr('a')}` })); // Rule 2: Match addition const rule2 = rewrite(() => ({ - before: pattern`${capture('x')} + ${capture('y')}`, - after: template`${capture('y')} + ${capture('x')}` + before: pattern`${expr('x')} + ${expr('y')}`, + after: template`${expr('y')} + ${expr('x')}` })); const combined = rule1.orElse(rule2); @@ -227,14 +227,14 @@ describe('RewriteRule composition', () => { test('neither rule matches', () => { // Rule 1: Match subtraction const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`${capture('b')} - ${capture('a')}` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`${expr('b')} - ${expr('a')}` })); // Rule 2: Match multiplication const rule2 = rewrite(() => ({ - before: pattern`${capture('x')} * ${capture('y')}`, - after: template`${capture('y')} * ${capture('x')}` + before: pattern`${expr('x')} * ${expr('y')}`, + after: template`${expr('y')} * ${expr('x')}` })); const combined = rule1.orElse(rule2); @@ -255,14 +255,14 @@ describe('RewriteRule composition', () => { test('specific pattern with general fallback', () => { // Specific: Match foo with second argument being 0 const specific = rewrite(() => ({ - before: pattern`foo(${capture('x')}, 0)`, - after: template`bar(${capture('x')})` + before: pattern`foo(${expr('x')}, 0)`, + after: template`bar(${expr('x')})` })); // General: Match foo with any two arguments const general = rewrite(() => ({ - before: pattern`foo(${capture('x')}, ${capture('y')})`, - after: template`baz(${capture('x')}, ${capture('y')})` + before: pattern`foo(${expr('x')}, ${expr('y')})`, + after: template`baz(${expr('x')}, ${expr('y')})` })); const combined = specific.orElse(general); @@ -287,20 +287,20 @@ const b = baz(x, 1);` test('chains three rules with orElse', () => { // Rule 1: Match subtraction const rule1 = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`subtract(${capture('a')}, ${capture('b')})` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`subtract(${expr('a')}, ${expr('b')})` })); // Rule 2: Match multiplication const rule2 = rewrite(() => ({ - before: pattern`${capture('a')} * ${capture('b')}`, - after: template`multiply(${capture('a')}, ${capture('b')})` + before: pattern`${expr('a')} * ${expr('b')}`, + after: template`multiply(${expr('a')}, ${expr('b')})` })); // Rule 3: Match addition const rule3 = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`add(${capture('a')}, ${capture('b')})` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`add(${expr('a')}, ${expr('b')})` })); const combined = rule1.orElse(rule2).orElse(rule3); @@ -329,20 +329,20 @@ const c = multiply(x, y);` test('orElse followed by andThen', () => { // Transform foo(x, 0) to bar(x), then wrap in parens const specific = rewrite(() => ({ - before: pattern`foo(${capture('x')}, 0)`, - after: template`bar(${capture('x')})` + before: pattern`foo(${expr('x')}, 0)`, + after: template`bar(${expr('x')})` })); // Fallback: transform foo(x, y) to baz(x, y), then wrap in parens const general = rewrite(() => ({ - before: pattern`foo(${capture('x')}, ${capture('y')})`, - after: template`baz(${capture('x')}, ${capture('y')})` + before: pattern`foo(${expr('x')}, ${expr('y')})`, + after: template`baz(${expr('x')}, ${expr('y')})` })); // Add parentheses const addParens = rewrite(() => ({ - before: pattern`${capture('expr')}`, - after: template`(${capture('expr')})` + before: pattern`${expr('expr')}`, + after: template`(${expr('expr')})` })); const combined = specific.orElse(general).andThen(addParens); @@ -367,26 +367,26 @@ const b = (baz(x, 1));` test('andThen followed by orElse', () => { // Rule 1: Transform subtraction to function call, then try to optimize const subToFunc = rewrite(() => ({ - before: pattern`${capture('a')} - ${capture('b')}`, - after: template`subtract(${capture('a')}, ${capture('b')})` + before: pattern`${expr('a')} - ${expr('b')}`, + after: template`subtract(${expr('a')}, ${expr('b')})` })); // Optimize: subtract(x, 0) -> x const optimizeSub = rewrite(() => ({ - before: pattern`subtract(${capture('x')}, 0)`, - after: template`${capture('x')}` + before: pattern`subtract(${expr('x')}, 0)`, + after: template`${expr('x')}` })); // Rule 2: Transform addition to function call, then try to optimize const addToFunc = rewrite(() => ({ - before: pattern`${capture('a')} + ${capture('b')}`, - after: template`add(${capture('a')}, ${capture('b')})` + before: pattern`${expr('a')} + ${expr('b')}`, + after: template`add(${expr('a')}, ${expr('b')})` })); // Optimize: add(x, 0) -> x const optimizeAdd = rewrite(() => ({ - before: pattern`add(${capture('x')}, 0)`, - after: template`${capture('x')}` + before: pattern`add(${expr('x')}, 0)`, + after: template`${expr('x')}` })); const combined = subToFunc.andThen(optimizeSub).orElse(addToFunc.andThen(optimizeAdd)); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts index 38d6343bcca..a80019b6db3 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/statement-expression-wrapping.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, JavaScriptParser, JavaScriptVisitor, JS, pattern, template, typescript} from '../../../src/javascript'; +import {expr, stmt, JavaScriptParser, JavaScriptVisitor, JS, pattern, template, typescript} from '../../../src/javascript'; import {J} from '../../../src/java'; import {Cursor} from '../../../src'; import {fromVisitor, RecipeSpec} from '../../../src/test'; @@ -39,7 +39,7 @@ describe('Statement Expression Wrapping', () => { const initializerCursor = new Cursor(initializer, varDeclCursor); // Pattern to match the 'x' identifier - const pat = pattern`${capture('expr')}`; + const pat = pattern`${expr('expr')}`; const match = await pat.match(initializer, initializerCursor); expect(match).toBeTruthy(); @@ -69,7 +69,7 @@ describe('Statement Expression Wrapping', () => { const stmtCursor = new Cursor(exprStmt, cuCursor); // Pattern to match the statement - const pat = pattern`${capture('stmt')};`; + const pat = pattern`${stmt('stmt')};`; const match = await pat.match(exprStmt, stmtCursor); expect(match).toBeTruthy(); @@ -96,7 +96,7 @@ describe('Statement Expression Wrapping', () => { const initializerCursor = new Cursor(initializer); // Pattern to match the 'x' identifier - const pat = pattern`${capture('expr')}`; + const pat = pattern`${expr('expr')}`; const match = await pat.match(initializer, initializerCursor); expect(match).toBeTruthy(); @@ -126,7 +126,7 @@ describe('Statement Expression Wrapping', () => { const initializerCursor = new Cursor(initializer, varDeclCursor); // Pattern to match the method invocation - const pat = pattern`${capture('call')}`; + const pat = pattern`${expr('call')}`; const match = await pat.match(initializer, initializerCursor); expect(match).toBeTruthy(); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/unnamed-capture.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/unnamed-capture.test.ts index c858196b78c..6ee6e55ff17 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/unnamed-capture.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/unnamed-capture.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, rewrite, template, typescript} from "../../../src/javascript"; +import {expr, ident, JavaScriptVisitor, pattern, rewrite, template, typescript} from "../../../src/javascript"; import {Expression, J} from "../../../src/java"; describe('unnamed capture', () => { @@ -24,15 +24,15 @@ describe('unnamed capture', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { - protected override async visitExpression(expr: Expression, p: any): Promise { - const left = capture(); - const right = capture(); + protected override async visitExpression(expression: Expression, p: any): Promise { + const left = expr(); + const right = expr(); //language=typescript - let m = await pattern`${left} + ${right}`.match(expr, this.cursor) || - await pattern`${left} * ${right}`.match(expr, this.cursor); + let m = await pattern`${left} + ${right}`.match(expression, this.cursor) || + await pattern`${left} * ${right}`.match(expression, this.cursor); - return m && await template`${right} + ${left}`.apply(expr, this.cursor, {values: m}) || expr; + return m && await template`${right} + ${left}`.apply(expression, this.cursor, {values: m}) || expression; } }); @@ -50,9 +50,9 @@ describe('unnamed capture', () => { spec.recipe = fromVisitor(new class extends JavaScriptVisitor { override async visitTernary(ternary: J.Ternary, p: any): Promise { - const obj = capture(); - const property = capture(); - const defaultValue = capture(); + const obj = expr(); + const property = ident(); + const defaultValue = expr(); //language=typescript return await rewrite(() => ({ diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-array-proxy.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-array-proxy.test.ts index 618d3c6fc3a..d224cc2bfc4 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-array-proxy.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-array-proxy.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, pattern, Pattern, template, Template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptVisitor, pattern, Pattern, template, Template, typescript} from "../../../src/javascript"; import {Expression, J} from "../../../src/java"; describe('variadic array proxy behavior', () => { @@ -36,7 +36,7 @@ describe('variadic array proxy behavior', () => { } test('access first element with index [0]', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${args[0]})`)); return spec.rewriteRun( @@ -45,7 +45,7 @@ describe('variadic array proxy behavior', () => { }); test('access second element with index [1]', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${args[1]})`)); return spec.rewriteRun( @@ -54,7 +54,7 @@ describe('variadic array proxy behavior', () => { }); test('access last element with index [2]', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${args[2]})`)); return spec.rewriteRun( @@ -63,7 +63,7 @@ describe('variadic array proxy behavior', () => { }); test('slice from index 1', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${(args.slice(1))})`)); return spec.rewriteRun( @@ -72,7 +72,7 @@ describe('variadic array proxy behavior', () => { }); test('slice with start and end indices', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${(args.slice(1, 3))})`)); return spec.rewriteRun( @@ -81,7 +81,7 @@ describe('variadic array proxy behavior', () => { }); test('combine first element and slice for rest', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${args[0]}, ${(args.slice(1))})`)); return spec.rewriteRun( @@ -90,7 +90,7 @@ describe('variadic array proxy behavior', () => { }); test('slice can return empty array', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace(pattern`foo(${args})`, template`bar(${(args.slice(10))})`)); return spec.rewriteRun( @@ -99,7 +99,7 @@ describe('variadic array proxy behavior', () => { }); test('reorder arguments using indices', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); spec.recipe = fromVisitor(matchAndReplace( pattern`foo(${args})`, template`bar(${args[1]}, ${args[0]}, ${args.slice(2)})`) diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-basic.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-basic.test.ts index a8c8b522e11..1daf6956b87 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-basic.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-basic.test.ts @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture} from "../../../src/javascript"; +import {expr} from "../../../src/javascript"; describe('variadic capture basic functionality', () => { test('regular capture is not variadic', () => { - const arg = capture('arg'); + const arg = expr('arg'); expect(arg.isVariadic()).toBe(false); expect(arg.getVariadicOptions()).toBeUndefined(); }); test('variadic: true creates variadic capture with defaults', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); expect(args.isVariadic()).toBe(true); const options = args.getVariadicOptions(); @@ -33,7 +33,7 @@ describe('variadic capture basic functionality', () => { }); test('variadic with min/max bounds', () => { - const args = capture({ variadic: { min: 1, max: 3 } }); + const args = expr({ variadic: { min: 1, max: 3 } }); expect(args.isVariadic()).toBe(true); const options = args.getVariadicOptions(); @@ -42,7 +42,7 @@ describe('variadic capture basic functionality', () => { }); test('variadic with all options', () => { - const args = capture({ name: 'args', + const args = expr({ name: 'args', variadic: { min: 2, max: 5 } }); @@ -52,7 +52,7 @@ describe('variadic capture basic functionality', () => { }); test('unnamed variadic capture', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); expect(args.isVariadic()).toBe(true); expect(args.getName()).toMatch(/^unnamed_\d+$/); }); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts index 0c11af5f346..ccf481309d4 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, JavaScriptParser, pattern} from "../../../src/javascript"; +import {expr, JavaScriptParser, pattern} from "../../../src/javascript"; import {J} from "../../../src/java"; describe('Variadic Capture Constraints', () => { @@ -38,7 +38,7 @@ describe('Variadic Capture Constraints', () => { describe('Array-level validation', () => { test('constraint validates entire array', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes) => nodes.every(n => typeof n.value === 'number') }); @@ -57,21 +57,21 @@ describe('Variadic Capture Constraints', () => { }); test('constraint works with empty array', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes) => nodes.length === 0 || nodes.every(n => typeof n.value === 'number') }); const pat = pattern`foo(${args})`; // Should match - empty array - const expr = await parseExpression('foo()'); - const match = await pat.match(expr, undefined!); + const expr0 = await parseExpression('foo()'); + const match = await pat.match(expr0, undefined!); expect(match).toBeDefined(); expect((match?.get(args) as unknown as J[]).length).toBe(0); }); test('constraint can check array length', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes) => nodes.length >= 2 }); @@ -96,7 +96,7 @@ describe('Variadic Capture Constraints', () => { describe('Relationship validation', () => { test('constraint can validate relationships between elements', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes) => { // All must be numbers and in ascending order @@ -136,7 +136,7 @@ describe('Variadic Capture Constraints', () => { }); test('constraint can check first/last elements', async () => { - const args = capture({ + const args = expr({ variadic: true, constraint: (nodes) => { // First and last must be numbers @@ -167,7 +167,7 @@ describe('Variadic Capture Constraints', () => { describe('Combined with min/max', () => { test('constraint works together with min/max bounds', async () => { - const args = capture({ + const args = expr({ variadic: { min: 2, max: 4 }, constraint: (nodes) => nodes.every(n => typeof n.value === 'number') }); diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts index 3ecb29c0750..a9fc86ae5a7 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-container.test.ts @@ -28,7 +28,7 @@ * visitor to use matchSequence logic instead of strict length comparison when variadic * captures are present, similar to how method invocations are handled. */ -import {capture, JavaScriptParser, JavaScriptVisitor, JS, pattern, template, typescript} from "../../../src/javascript"; +import {expr, JavaScriptParser, JavaScriptVisitor, JS, pattern, template, typescript} from "../../../src/javascript"; import {J} from "../../../src/java"; import {fromVisitor, RecipeSpec} from "../../../src/test"; import {Cursor} from "../../../src"; @@ -52,7 +52,7 @@ describe('variadic pattern matching in containers', () => { } test('variadic capture in object destructuring pattern', async () => { - const props = capture({ variadic: true }); + const props = expr({ variadic: true }); const pat = pattern`function foo({${props}}) {}`; // The pattern has 1 element (variadic capture), but the target has 2+ elements @@ -81,8 +81,8 @@ describe('variadic pattern matching in containers', () => { }); test('variadic capture with required property in object destructuring', async () => { - const first = capture('first'); - const rest = capture({ variadic: true }); + const first = expr('first'); + const rest = expr({ variadic: true }); const pat = pattern`function foo({${first}, ${rest}}) {}`; // The pattern has 2 elements (first + variadic), but target has 3+ elements @@ -108,7 +108,7 @@ describe('variadic pattern matching in containers', () => { }); test('variadic capture in array destructuring pattern', async () => { - const elements = capture({ variadic: true }); + const elements = expr({ variadic: true }); const pat = pattern`function foo([${elements}]) {}`; // The pattern has 1 element (variadic capture), but the target has 2+ elements @@ -138,7 +138,7 @@ describe('variadic pattern matching in containers', () => { test('variadic capture with min/max constraints in containers', async () => { // Test min constraint - const props1 = capture({ variadic: { min: 2 } }); + const props1 = expr({ variadic: { min: 2 } }); const pat1 = pattern`function foo({${props1}}) {}`; expect(await pat1.match(await parse('function foo({}) {}'), undefined!)).toBeUndefined(); // min not satisfied @@ -147,7 +147,7 @@ describe('variadic pattern matching in containers', () => { expect(await pat1.match(await parse('function foo({a, b, c}) {}'), undefined!)).toBeDefined(); // more than min // Test max constraint - const props2 = capture({ variadic: { max: 2 } }); + const props2 = expr({ variadic: { max: 2 } }); const pat2 = pattern`function foo({${props2}}) {}`; expect(await pat2.match(await parse('function foo({}) {}'), undefined!)).toBeDefined(); // within max @@ -162,7 +162,7 @@ describe('variadic pattern matching in containers', () => { let receivedCursor: any = null; // Capture with constraint that checks the array length and verifies cursor is present - const props = capture({ + const props = expr({ variadic: true, constraint: (nodes: J[], context) => { receivedCursor = context.cursor; @@ -201,10 +201,10 @@ describe('variadic pattern matching in containers', () => { // This test reproduces the issue where variadic captures in containers // are not properly expanded during template replacement - const propsWithBindings = capture({ variadic: true }); - const ref = capture(); - const body = capture({ variadic: true }); - const name = capture(); + const propsWithBindings = expr({ variadic: true }); + const ref = expr(); + const body = expr({ variadic: true }); + const name = expr(); // Pattern: forwardRef(function Name({...props}, ref) {...}) const beforePattern = pattern`forwardRef(function ${name}({${propsWithBindings}}, ${ref}) {${body}})`; @@ -239,10 +239,10 @@ describe('variadic pattern matching in containers', () => { // This test verifies that { ref: ${ref} } preserves the "ref:" property name // when replacing ${ref} with the captured value - const ref = capture(); - const props = capture({ variadic: true }); - const body = capture({ variadic: true }); - const name = capture(); + const ref = expr(); + const props = expr({ variadic: true }); + const body = expr({ variadic: true }); + const name = expr(); // Reuse the forwardRef pattern but with a template that has propertyName const beforePattern = pattern`forwardRef(function ${name}({${props}}, ${ref}) {${body}})`; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-expansion.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-expansion.test.ts index 6b06dae8147..55fb827adb9 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-expansion.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-expansion.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {any, capture, JavaScriptVisitor, pattern, Pattern, template, Template, typescript} from "../../../src/javascript"; +import {any, expr, JavaScriptVisitor, pattern, Pattern, template, Template, typescript} from "../../../src/javascript"; import {J} from "../../../src/java"; describe('variadic template expansion', () => { @@ -36,7 +36,7 @@ describe('variadic template expansion', () => { } test('expand variadic capture - zero arguments', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args})`; const tmpl = template`bar(${args})`; @@ -48,7 +48,7 @@ describe('variadic template expansion', () => { }); test('expand variadic capture - single argument', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args})`; const tmpl = template`bar(${args})`; @@ -60,7 +60,7 @@ describe('variadic template expansion', () => { }); test('expand variadic capture - multiple arguments', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args})`; const tmpl = template`bar(${args})`; @@ -72,8 +72,8 @@ describe('variadic template expansion', () => { }); test('expand variadic with fixed arguments before', () => { - const first = capture(); - const rest = capture({ variadic: true }); + const first = expr(); + const rest = expr({ variadic: true }); const pat = pattern`foo(${first}, ${rest})`; const tmpl = template`bar(${first}, ${rest})`; @@ -85,8 +85,8 @@ describe('variadic template expansion', () => { }); test('expand variadic with fixed arguments after', () => { - const first = capture({ variadic: true }); - const last = capture(); + const first = expr({ variadic: true }); + const last = expr(); const pat = pattern`foo(${first}, ${last})`; const tmpl = template`bar(${first}, ${last})`; @@ -99,7 +99,7 @@ describe('variadic template expansion', () => { test('match with any() before-middle-after - zero before, zero after', () => { const before = any({ variadic: true }); - const middle = capture({constraint: node => node.simpleName === 'x'}); + const middle = expr({constraint: node => node.simpleName === 'x'}); const after = any({ variadic: true }); const pat = pattern`foo(${before}, ${middle}, ${after})`; const tmpl = template`bar(${middle})`; @@ -113,7 +113,7 @@ describe('variadic template expansion', () => { test('match with any() before-middle-after - one before, zero after', () => { const before = any({ variadic: true }); - const middle = capture({constraint: node => node.simpleName === 'x'}); + const middle = expr({constraint: node => node.simpleName === 'x'}); const after = any({ variadic: true }); const pat = pattern`foo(${before}, ${middle}, ${after})`; const tmpl = template`bar(${middle})`; @@ -127,7 +127,7 @@ describe('variadic template expansion', () => { test('match with any() before-middle-after - zero before, one after', () => { const before = any({ variadic: true }); - const middle = capture({constraint: node => node.simpleName === 'x'}); + const middle = expr({constraint: node => node.simpleName === 'x'}); const after = any({ variadic: true }); const pat = pattern`foo(${before}, ${middle}, ${after})`; const tmpl = template`bar(${middle})`; @@ -141,7 +141,7 @@ describe('variadic template expansion', () => { test('match with any() before-middle-after - one before, one after', () => { const before = any({ variadic: true }); - const middle = capture({constraint: node => node.simpleName === 'x'}); + const middle = expr({constraint: node => node.simpleName === 'x'}); const after = any({ variadic: true }); const pat = pattern`foo(${before}, ${middle}, ${after})`; const tmpl = template`bar(${middle})`; @@ -154,7 +154,7 @@ describe('variadic template expansion', () => { }); test('variadic followed by literal - should not consume literal', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args}, 'bar')`; const tmpl = template`baz(${args})`; @@ -166,7 +166,7 @@ describe('variadic template expansion', () => { }); test('variadic followed by literal - no match if literal missing', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args}, 'bar')`; const tmpl = template`baz(${args})`; diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-marker.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-marker.test.ts index 6d00c9e710d..a89230ccc6b 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-marker.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-marker.test.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, pattern} from "../../../src/javascript"; +import {expr, pattern} from "../../../src/javascript"; describe('variadic marker attachment', () => { test('regular capture does not have variadic marker', () => { - const arg = capture('arg'); + const arg = expr('arg'); const pat = pattern`foo(${arg})`; // Verify the capture object itself @@ -26,7 +26,7 @@ describe('variadic marker attachment', () => { }); test('variadic capture stores options in capture object', () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args})`; // Verify the capture object itself @@ -35,7 +35,7 @@ describe('variadic marker attachment', () => { }); test('variadic capture with custom options stores them correctly', () => { - const args = capture({ + const args = expr({ variadic: { min: 1, max: 3 @@ -49,7 +49,7 @@ describe('variadic marker attachment', () => { }); test('pattern captures array includes variadic capture', () => { - const args = capture({ name: 'args', variadic: true }); + const args = expr({ name: 'args', variadic: true }); const pat = pattern`foo(${args})`; // Pattern should have the captures array diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts index b8368c9c590..c83ee28d57b 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-matching.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {capture, JavaScriptParser, JS, pattern} from "../../../src/javascript"; +import {expr, JavaScriptParser, JS, pattern} from "../../../src/javascript"; describe('variadic pattern matching against real code', () => { const parser = new JavaScriptParser(); @@ -34,7 +34,7 @@ describe('variadic pattern matching against real code', () => { } test('variadic capture matches 0, 1, or many arguments', async () => { - const args = capture({ variadic: true }); + const args = expr({ variadic: true }); const pat = pattern`foo(${args})`; // Zero arguments @@ -63,8 +63,8 @@ describe('variadic pattern matching against real code', () => { }); test('required first argument + variadic rest', async () => { - const first = capture('first'); - const rest = capture({ variadic: true }); + const first = expr('first'); + const rest = expr({ variadic: true }); const pat = pattern`foo(${first}, ${rest})`; // Should NOT match foo() - missing required first @@ -88,7 +88,7 @@ describe('variadic pattern matching against real code', () => { test('variadic with min, max, and min+max constraints', async () => { // Test 1: min constraint - const args1 = capture({ variadic: { min: 2 } }); + const args1 = expr({ variadic: { min: 2 } }); const pat1 = pattern`foo(${args1})`; expect(await pat1.match(await parseExpr('foo()'), undefined!)).toBeUndefined(); // min not satisfied @@ -97,7 +97,7 @@ describe('variadic pattern matching against real code', () => { expect(await pat1.match(await parseExpr('foo(1, 2, 3)'), undefined!)).toBeDefined(); // more than min // Test 2: max constraint - const args2 = capture({ variadic: { max: 2 } }); + const args2 = expr({ variadic: { max: 2 } }); const pat2 = pattern`foo(${args2})`; expect(await pat2.match(await parseExpr('foo()'), undefined!)).toBeDefined(); // within max @@ -105,7 +105,7 @@ describe('variadic pattern matching against real code', () => { expect(await pat2.match(await parseExpr('foo(1, 2, 3)'), undefined!)).toBeUndefined(); // exceeds max // Test 3: min and max constraints - const args3 = capture({ variadic: { min: 1, max: 2 } }); + const args3 = expr({ variadic: { min: 1, max: 2 } }); const pat3 = pattern`foo(${args3})`; expect(await pat3.match(await parseExpr('foo()'), undefined!)).toBeUndefined(); // below min @@ -115,9 +115,9 @@ describe('variadic pattern matching against real code', () => { }); test('pattern with regular captures and variadic', async () => { - const first = capture('first'); - const middle = capture({ variadic: true }); - const last = capture('last'); + const first = expr('first'); + const middle = expr({ variadic: true }); + const last = expr('last'); const pat = pattern`foo(${first}, ${middle}, ${last})`; // Should match foo(1, 2) - first=1, middle=[], last=2 diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-statement.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-statement.test.ts index 10add1f3163..4bf1d31010c 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-statement.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-statement.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import {fromVisitor, RecipeSpec} from "../../../src/test"; -import {capture, JavaScriptVisitor, Pattern, pattern, Template, template, typescript} from "../../../src/javascript"; +import {expr, stmt, JavaScriptVisitor, Pattern, pattern, Template, template, typescript} from "../../../src/javascript"; import {J} from "../../../src/java"; import {create as produce} from "mutative"; @@ -44,7 +44,7 @@ describe('variadic statement matching and expansion', () => { } test('match block with zero leading statements using any()', () => { - const leadingStmts = capture({ variadic: true }); + const leadingStmts = stmt({ variadic: true }); const pat = pattern`{ ${leadingStmts} return x; @@ -72,7 +72,7 @@ describe('variadic statement matching and expansion', () => { }); test('match block with one leading statement using any()', () => { - const leadingStmts = capture({ variadic: true }); + const leadingStmts = stmt({ variadic: true }); const pat = pattern`{ ${leadingStmts} return x; @@ -102,7 +102,7 @@ describe('variadic statement matching and expansion', () => { }); test('match block with multiple leading statements using any()', () => { - const leadingStmts = capture({ variadic: true }); + const leadingStmts = stmt({ variadic: true }); const pat = pattern`{ ${leadingStmts} return x; @@ -134,7 +134,7 @@ describe('variadic statement matching and expansion', () => { }); test('match block with trailing statements using any()', () => { - const trailingStmts = capture({ variadic: true }); + const trailingStmts = stmt({ variadic: true }); const pat = pattern`{ console.log('start'); ${trailingStmts} @@ -164,8 +164,8 @@ describe('variadic statement matching and expansion', () => { }); test('capture and reorder statements', () => { - const first = capture(); - const second = capture(); + const first = stmt(); + const second = stmt(); const pat = pattern`{ ${first} ${second} @@ -193,7 +193,7 @@ describe('variadic statement matching and expansion', () => { }); test('match with variadic min constraint', () => { - const leadingStmts = capture({ variadic: { min: 1 } }); + const leadingStmts = stmt({ variadic: { min: 1 } }); const pat = pattern`{ ${leadingStmts} return x; @@ -230,7 +230,7 @@ describe('variadic statement matching and expansion', () => { }); test('match with variadic max constraint', () => { - const leadingStmts = capture({ variadic: { max: 1 } }); + const leadingStmts = stmt({ variadic: { max: 1 } }); const pat = pattern`{ ${leadingStmts} return x; @@ -280,7 +280,7 @@ describe('variadic statement matching and expansion', () => { }); test('match empty block with variadic capture', () => { - const stmts = capture({ variadic: true }); + const stmts = stmt({ variadic: true }); const pat = pattern`{ ${stmts} }`; @@ -304,7 +304,7 @@ describe('variadic statement matching and expansion', () => { }); test('capture variadic statements for reuse', () => { - const stmts = capture({ variadic: true }); + const stmts = stmt({ variadic: true }); const pat = pattern`{ try { ${stmts} @@ -344,8 +344,8 @@ describe('variadic statement matching and expansion', () => { }); test('non-variadic capture should preserve trailing semicolons', () => { - // Bug report: using capture() (non-variadic) for function bodies loses trailing semicolons - const body = capture(); + // Bug report: using stmt() (non-variadic) for function bodies loses trailing semicolons + const body = stmt(); const pat = pattern`{${body}}`; const tmpl = template`{ console.log('before'); @@ -370,7 +370,7 @@ describe('variadic statement matching and expansion', () => { test('variadic capture should preserve trailing semicolons', () => { // Variadic captures should also preserve semicolons and formatting - const body = capture({ variadic: true }); + const body = stmt({ variadic: true }); const pat = pattern`{${body}}`; const tmpl = template`{ console.log('before'); @@ -395,7 +395,7 @@ describe('variadic statement matching and expansion', () => { test('function body capture with wrapper pattern should preserve semicolons', () => { // More complex example: extracting function body from wrapper pattern - const {args, body} = {args: capture(), body: capture({ variadic: true })}; + const {args, body} = {args: expr(), body: stmt({ variadic: true })}; const pat = pattern`{ return wrapper(function(${args}) {${body}}); }`; From 4e891d20c2831fd02c2091b4ad7be5aa77c85588 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 08:33:06 +0100 Subject: [PATCH 3/7] JS: Address review feedback on CaptureKind implementation - Clarify namespace block is type-only declarations for TS augmentation - Eliminate duplicated logic in createKindCapture by delegating to capture() - Rename expr0 to parsed for consistency with other test files --- .../src/javascript/templating/capture.ts | 19 +++++++------------ .../templating/variadic-constraints.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts index b9e26b5f28f..1cac3c79508 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts @@ -399,7 +399,9 @@ export function capture(nameOrOptions?: string | CaptureOptions): Ca // Static counter for generating unique IDs for unnamed captures capture.nextUnnamedId = 1; -// Type declarations for namespace properties on capture +// Type-only declarations so TypeScript recognizes the runtime properties +// attached to the capture function (nextUnnamedId, expr, ident, typeRef, stmt). +// The actual values are assigned imperatively below the factory definitions. export namespace capture { export let nextUnnamedId: number; export let expr: typeof import('./capture').expr; @@ -655,21 +657,14 @@ export function stmt(nameOrOptions?: string | CaptureOptions): Captu /** * Internal helper for kind-specific capture factory functions. + * Delegates to capture() with the kind option set, avoiding duplication + * of name-resolution and option-handling logic. */ function createKindCapture(kind: CaptureKind, nameOrOptions?: string | CaptureOptions): Capture & T { - let name: string | undefined; - let options: CaptureOptions | undefined; - if (typeof nameOrOptions === 'string') { - name = nameOrOptions; - } else { - options = nameOrOptions; - name = options?.name; + return capture({ name: nameOrOptions, kind } as CaptureOptions); } - - const captureName = name || `unnamed_${capture.nextUnnamedId++}`; - const impl = new CaptureImpl(captureName, options, true, kind); - return createCaptureProxy(impl); + return capture({ ...nameOrOptions, kind } as CaptureOptions); } // Attach kind-specific factories to capture for namespace-qualified access diff --git a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts index ccf481309d4..3ab1ce83aba 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/variadic-constraints.test.ts @@ -64,8 +64,8 @@ describe('Variadic Capture Constraints', () => { const pat = pattern`foo(${args})`; // Should match - empty array - const expr0 = await parseExpression('foo()'); - const match = await pat.match(expr0, undefined!); + const parsed = await parseExpression('foo()'); + const match = await pat.match(parsed, undefined!); expect(match).toBeDefined(); expect((match?.get(args) as unknown as J[]).length).toBe(0); }); From 939a08b70a33925946b0fb1960242ed55f6030b8 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 08:39:22 +0100 Subject: [PATCH 4/7] =?UTF-8?q?JS:=20Make=20CaptureKind=20internal=20?= =?UTF-8?q?=E2=80=94=20factory=20functions=20are=20the=20public=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove CaptureKind from public exports and the kind option from CaptureOptions. The factory functions (expr, ident, typeRef, stmt) are the intended API for setting capture kinds; there is no need to expose the enum to callers. --- .../src/javascript/templating/capture.ts | 17 ++++++++++++----- .../rewrite/src/javascript/templating/index.ts | 4 ---- .../rewrite/src/javascript/templating/types.ts | 5 ----- .../javascript/templating/capture-kinds.test.ts | 17 ++++------------- 4 files changed, 16 insertions(+), 27 deletions(-) diff --git a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts index 1cac3c79508..2725d2b3fd6 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts @@ -91,7 +91,7 @@ export class CaptureImpl implements Capture { this.name = name; this[CAPTURE_NAME_SYMBOL] = name; this[CAPTURE_CAPTURING_SYMBOL] = capturing; - this[CAPTURE_KIND_SYMBOL] = options?.kind ?? kind; + this[CAPTURE_KIND_SYMBOL] = kind; // Normalize variadic options if (options?.variadic) { @@ -657,14 +657,21 @@ export function stmt(nameOrOptions?: string | CaptureOptions): Captu /** * Internal helper for kind-specific capture factory functions. - * Delegates to capture() with the kind option set, avoiding duplication - * of name-resolution and option-handling logic. */ function createKindCapture(kind: CaptureKind, nameOrOptions?: string | CaptureOptions): Capture & T { + let name: string | undefined; + let options: CaptureOptions | undefined; + if (typeof nameOrOptions === 'string') { - return capture({ name: nameOrOptions, kind } as CaptureOptions); + name = nameOrOptions; + } else { + options = nameOrOptions; + name = options?.name; } - return capture({ ...nameOrOptions, kind } as CaptureOptions); + + const captureName = name || `unnamed_${capture.nextUnnamedId++}`; + const impl = new CaptureImpl(captureName, options, true, kind); + return createCaptureProxy(impl); } // Attach kind-specific factories to capture for namespace-qualified access diff --git a/rewrite-javascript/rewrite/src/javascript/templating/index.ts b/rewrite-javascript/rewrite/src/javascript/templating/index.ts index a138a20d726..982b27ba5db 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/index.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/index.ts @@ -33,10 +33,6 @@ export type { MatchAttemptResult } from './types'; -export { - CaptureKind -} from './types'; - // Export capture functionality export { and, diff --git a/rewrite-javascript/rewrite/src/javascript/templating/types.ts b/rewrite-javascript/rewrite/src/javascript/templating/types.ts index 04c184a2d5d..7441f8c6110 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/types.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/types.ts @@ -114,11 +114,6 @@ export type ConstraintFunction = (node: T, context: CaptureConstraintContext) export interface CaptureOptions { name?: string; variadic?: boolean | VariadicOptions; - /** - * The syntactic kind of this capture. Defaults to {@link CaptureKind.Expression}. - * Used by the template engine to generate the correct scaffold placeholder. - */ - kind?: CaptureKind; /** * Optional constraint function that validates whether a captured node should be accepted. * The function receives: diff --git a/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts b/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts index 6054220db64..85021f41efc 100644 --- a/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts +++ b/rewrite-javascript/rewrite/test/javascript/templating/capture-kinds.test.ts @@ -14,12 +14,13 @@ * limitations under the License. */ import { - capture, expr, ident, typeRef, stmt, any, CaptureKind, + capture, expr, ident, typeRef, stmt, any, Pattern, pattern } from "../../../src/javascript"; import { - CAPTURE_KIND_SYMBOL, CAPTURE_NAME_SYMBOL + CAPTURE_KIND_SYMBOL } from "../../../src/javascript/templating/capture"; +import { CaptureKind } from "../../../src/javascript/templating/types"; describe('capture kinds', () => { describe('CaptureKind enum', () => { @@ -98,11 +99,6 @@ describe('capture kinds', () => { const c = capture({ name: 'x' }); expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); }); - - test('capture() can override kind via options', () => { - const c = capture({ name: 'x', kind: CaptureKind.Identifier }); - expect((c as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); - }); }); describe('namespace access on capture', () => { @@ -131,16 +127,11 @@ describe('capture kinds', () => { }); }); - describe('any() with kinds', () => { + describe('any() defaults', () => { test('any() defaults to Expression kind', () => { const a = any(); expect((a as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Expression); }); - - test('any() can specify kind via options', () => { - const a = any({ kind: CaptureKind.Identifier }); - expect((a as any)[CAPTURE_KIND_SYMBOL]).toBe(CaptureKind.Identifier); - }); }); describe('usage in patterns', () => { From ca968f47b7fddf7412c7eeab4147379d4c6509f0 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 08:46:15 +0100 Subject: [PATCH 5/7] JS: Restrict type option to expr() and preamble to expression captures Move the `type` option from CaptureOptions to a new ExprCaptureOptions interface only accepted by expr(). The template engine now skips preamble generation for non-expression captures (ident, typeRef, stmt), since type attribution declarations only make sense for expression placeholders. --- .../src/javascript/templating/capture.ts | 42 ++++++++++++++++--- .../src/javascript/templating/engine.ts | 20 +++++++-- .../src/javascript/templating/index.ts | 2 + .../src/javascript/templating/types.ts | 38 ----------------- 4 files changed, 54 insertions(+), 48 deletions(-) diff --git a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts index 2725d2b3fd6..60e488d614b 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts @@ -87,7 +87,7 @@ export class CaptureImpl implements Capture { [CAPTURE_TYPE_SYMBOL]: string | Type | undefined; [CAPTURE_KIND_SYMBOL]: CaptureKind; - constructor(name: string, options?: CaptureOptions, capturing: boolean = true, kind: CaptureKind = CaptureKind.Expression) { + constructor(name: string, options?: CaptureOptions & { type?: string | Type }, capturing: boolean = true, kind: CaptureKind = CaptureKind.Expression) { this.name = name; this[CAPTURE_NAME_SYMBOL] = name; this[CAPTURE_CAPTURING_SYMBOL] = capturing; @@ -110,7 +110,7 @@ export class CaptureImpl implements Capture { this[CAPTURE_CONSTRAINT_SYMBOL] = options.constraint; } - // Store type if provided + // Store type if provided (only meaningful for Expression kind) if (options?.type) { this[CAPTURE_TYPE_SYMBOL] = options.type; } @@ -603,16 +603,46 @@ export function raw(code: string): RawCode { return new RawCode(code); } +/** + * Options specific to expression captures, extending CaptureOptions with type attribution. + */ +export interface ExprCaptureOptions extends CaptureOptions { + /** + * Type annotation for this expression capture. When provided, the template engine + * generates a preamble declaring the capture identifier with this type annotation, + * allowing the TypeScript parser/compiler to produce a properly type-attributed AST. + * + * Can be specified as: + * - A string type annotation (e.g., "boolean", "number", "Promise") + * - A Type instance from the AST + * + * @example + * ```typescript + * const chain = expr({ name: 'chain', type: 'Promise' }); + * pattern`${chain}.catch(err => console.log(err))` + * + * const items = expr({ name: 'items', type: 'number[]' }); + * pattern`${items}.map(x => x * 2)` + * ``` + */ + type?: string | Type; +} + /** * Creates an expression capture. This is the most common capture kind. + * Only expression captures support the `type` option for type attribution. * * @example * const e = expr('x'); * pattern`foo(${e})` + * + * @example + * const e = expr({ name: 'x', type: 'boolean' }); + * pattern`${e} || false` */ export function expr(name?: string): Capture & T; -export function expr(options: CaptureOptions): Capture & T; -export function expr(nameOrOptions?: string | CaptureOptions): Capture & T { +export function expr(options: ExprCaptureOptions): Capture & T; +export function expr(nameOrOptions?: string | ExprCaptureOptions): Capture & T { return createKindCapture(CaptureKind.Expression, nameOrOptions); } @@ -658,9 +688,9 @@ export function stmt(nameOrOptions?: string | CaptureOptions): Captu /** * Internal helper for kind-specific capture factory functions. */ -function createKindCapture(kind: CaptureKind, nameOrOptions?: string | CaptureOptions): Capture & T { +function createKindCapture(kind: CaptureKind, nameOrOptions?: string | ExprCaptureOptions): Capture & T { let name: string | undefined; - let options: CaptureOptions | undefined; + let options: ExprCaptureOptions | undefined; if (typeof nameOrOptions === 'string') { name = nameOrOptions; diff --git a/rewrite-javascript/rewrite/src/javascript/templating/engine.ts b/rewrite-javascript/rewrite/src/javascript/templating/engine.ts index bc2d030429e..30402d00c2d 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/engine.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/engine.ts @@ -18,7 +18,8 @@ import {emptySpace, J, Statement, Type} from '../../java'; import {Any, Capture, JavaScriptParser, JavaScriptVisitor, JS} from '..'; import {create as produce} from 'mutative'; import {CaptureMarker, PlaceholderUtils, WRAPPER_FUNCTION_NAME} from './utils'; -import {CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture'; +import {CAPTURE_KIND_SYMBOL, CAPTURE_NAME_SYMBOL, CAPTURE_TYPE_SYMBOL, CaptureImpl, CaptureValue, RAW_CODE_SYMBOL, RawCode} from './capture'; +import {CaptureKind} from './types'; import {PlaceholderReplacementVisitor} from './placeholder-replacement'; import {JavaCoordinates} from './template'; import {maybeAutoFormat} from '../format'; @@ -269,7 +270,8 @@ export class TemplateEngine { } /** - * Generates type preamble declarations for captures/parameters with type annotations. + * Generates type preamble declarations for expression captures/parameters with type annotations. + * Only expression captures get preamble declarations — identifiers, types, and statements don't. * * @param parameters The parameters * @returns Array of preamble statements @@ -288,6 +290,11 @@ export class TemplateEngine { const isTreeArray = Array.isArray(param) && param.length > 0 && isTree(param[0]); if (isCapture) { + // Only expression captures get preamble declarations + const captureKind = param[CAPTURE_KIND_SYMBOL]; + if (captureKind !== undefined && captureKind !== CaptureKind.Expression) { + continue; + } const captureType = param[CAPTURE_TYPE_SYMBOL]; if (captureType) { const typeString = typeof captureType === 'string' @@ -429,7 +436,7 @@ export class TemplateEngine { contextStatements: string[] = [], dependencies: Record = {} ): Promise { - // Generate type preamble for captures with types (skip RawCode) + // Generate type preamble for expression captures with types (skip RawCode and non-expression captures) const preamble: string[] = []; for (const capture of captures) { // Skip raw code - it's not a capture @@ -437,6 +444,12 @@ export class TemplateEngine { continue; } + // Only expression captures get preamble declarations + const captureKind = (capture as any)[CAPTURE_KIND_SYMBOL]; + if (captureKind !== undefined && captureKind !== CaptureKind.Expression) { + continue; + } + const captureName = (capture as any)[CAPTURE_NAME_SYMBOL] || capture.getName(); const captureType = (capture as any)[CAPTURE_TYPE_SYMBOL]; if (captureType) { @@ -450,7 +463,6 @@ export class TemplateEngine { preamble.push(`let ${placeholder}: ${typeString};`); } } - // Don't add preamble declarations without types - they don't provide type attribution } // Build the template string with placeholders for captures and raw code diff --git a/rewrite-javascript/rewrite/src/javascript/templating/index.ts b/rewrite-javascript/rewrite/src/javascript/templating/index.ts index 982b27ba5db..a3c4fccd1ca 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/index.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/index.ts @@ -33,6 +33,8 @@ export type { MatchAttemptResult } from './types'; +export type { ExprCaptureOptions } from './capture'; + // Export capture functionality export { and, diff --git a/rewrite-javascript/rewrite/src/javascript/templating/types.ts b/rewrite-javascript/rewrite/src/javascript/templating/types.ts index 7441f8c6110..bbcb538fdd3 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/types.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/types.ts @@ -151,44 +151,6 @@ export interface CaptureOptions { * ``` */ constraint?: ConstraintFunction; - /** - * Type annotation for this capture. When provided, the template engine will generate - * a preamble declaring the capture identifier with this type annotation, allowing - * the TypeScript parser/compiler to produce a properly type-attributed AST. - * - * **Why Use Type Attribution:** - * When matching against TypeScript code with type information, providing a type ensures - * the pattern's AST has matching type attribution, which can be important for: - * - Semantic matching based on types - * - Matching code that depends on type inference - * - Ensuring pattern parses with correct type context - * - * Can be specified as: - * - A string type annotation (e.g., "boolean", "string", "number", "Promise", "User[]") - * - A Type instance from the AST (the type will be inferred from the Type) - * - * @example - * ```typescript - * // Match promise chains with proper type attribution - * const chain = capture({ - * name: 'chain', - * type: 'Promise', // TypeScript will attribute this as Promise type - * constraint: (call: J.MethodInvocation) => { - * // Validate promise chain structure - * return call.name.simpleName === 'then'; - * } - * }); - * pattern`${chain}.catch(err => console.log(err))` - * - * // Match arrays with type annotation - * const items = capture({ - * name: 'items', - * type: 'number[]', // Array of numbers - * }); - * pattern`${items}.map(x => x * 2)` - * ``` - */ - type?: string | Type; } /** From e64d3ec2e3f9cd8725c48ee7f2e6ebb799cbb096 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 09:56:41 +0100 Subject: [PATCH 6/7] JS: Add variadic overloads to capture factory functions Add non-variadic and variadic overload signatures to expr(), ident(), typeRef(), and stmt() matching the existing capture() overloads. This fixes TypeScript type inference for typed captures with constraints (e.g. expr({constraint: ...})) and variadic captures with array operations (e.g. args[0], args.slice(1)). --- .../rewrite/src/javascript/templating/capture.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts index 60e488d614b..bc443f8deac 100644 --- a/rewrite-javascript/rewrite/src/javascript/templating/capture.ts +++ b/rewrite-javascript/rewrite/src/javascript/templating/capture.ts @@ -641,7 +641,8 @@ export interface ExprCaptureOptions extends CaptureOptions { * pattern`${e} || false` */ export function expr(name?: string): Capture & T; -export function expr(options: ExprCaptureOptions): Capture & T; +export function expr(options: ExprCaptureOptions & { variadic?: never }): Capture & T; +export function expr(options: ExprCaptureOptions & { variadic: true | VariadicOptions }): Capture & T[]; export function expr(nameOrOptions?: string | ExprCaptureOptions): Capture & T { return createKindCapture(CaptureKind.Expression, nameOrOptions); } @@ -654,7 +655,8 @@ export function expr(nameOrOptions?: string | ExprCaptureOptions): C * pattern`${n}()` */ export function ident(name?: string): Capture & T; -export function ident(options: CaptureOptions): Capture & T; +export function ident(options: CaptureOptions & { variadic?: never }): Capture & T; +export function ident(options: CaptureOptions & { variadic: true | VariadicOptions }): Capture & T[]; export function ident(nameOrOptions?: string | CaptureOptions): Capture & T { return createKindCapture(CaptureKind.Identifier, nameOrOptions); } @@ -667,7 +669,8 @@ export function ident(nameOrOptions?: string | CaptureOptions): Capt * pattern`function foo(): ${t}` */ export function typeRef(name?: string): Capture & T; -export function typeRef(options: CaptureOptions): Capture & T; +export function typeRef(options: CaptureOptions & { variadic?: never }): Capture & T; +export function typeRef(options: CaptureOptions & { variadic: true | VariadicOptions }): Capture & T[]; export function typeRef(nameOrOptions?: string | CaptureOptions): Capture & T { return createKindCapture(CaptureKind.TypeReference, nameOrOptions); } @@ -680,7 +683,8 @@ export function typeRef(nameOrOptions?: string | CaptureOptions): Ca * pattern`if (cond) ${s}` */ export function stmt(name?: string): Capture & T; -export function stmt(options: CaptureOptions): Capture & T; +export function stmt(options: CaptureOptions & { variadic?: never }): Capture & T; +export function stmt(options: CaptureOptions & { variadic: true | VariadicOptions }): Capture & T[]; export function stmt(nameOrOptions?: string | CaptureOptions): Capture & T { return createKindCapture(CaptureKind.Statement, nameOrOptions); } From 9c87bb55f75d08c2013a50b9e5370d84c0fd455a Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Fri, 20 Mar 2026 10:06:36 +0100 Subject: [PATCH 7/7] JS: Document typecheck limitation in CLAUDE.md npm run typecheck misses test file type errors because vitest types are not resolved by tsc alone. Note to prefer npm test as the final verification before pushing. --- rewrite-javascript/rewrite/CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rewrite-javascript/rewrite/CLAUDE.md b/rewrite-javascript/rewrite/CLAUDE.md index 3e814024eeb..98d1f449e70 100644 --- a/rewrite-javascript/rewrite/CLAUDE.md +++ b/rewrite-javascript/rewrite/CLAUDE.md @@ -203,4 +203,7 @@ Each language module has `rpc.ts` with a Sender (visit tree → serialize to que 4. Check `src/rpc/queue.ts` for deadlock in read/write operations ### Type Checking -Run `npm run typecheck` frequently to catch type mismatches early. +`npm run typecheck` only checks `src/` and `test/` via `tsc --noEmit`, but vitest type +definitions are not resolved by `tsc` alone. This means type errors inside test files +(e.g., incorrect generic inference on overloaded functions) can be missed locally while +CI catches them. Prefer `npm test` (typecheck + vitest) as the final check before pushing.