Skip to content
5 changes: 4 additions & 1 deletion rewrite-javascript/rewrite/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
141 changes: 137 additions & 4 deletions rewrite-javascript/rewrite/src/javascript/templating/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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');

Expand All @@ -83,11 +85,13 @@ export class CaptureImpl<T = any> implements Capture<T> {
[CAPTURE_CONSTRAINT_SYMBOL]: ConstraintFunction<T> | undefined;
[CAPTURE_CAPTURING_SYMBOL]: boolean;
[CAPTURE_TYPE_SYMBOL]: string | Type | undefined;
[CAPTURE_KIND_SYMBOL]: CaptureKind;

constructor(name: string, options?: CaptureOptions<T>, capturing: boolean = true) {
constructor(name: string, options?: CaptureOptions<T> & { type?: string | Type }, capturing: boolean = true, kind: CaptureKind = CaptureKind.Expression) {
this.name = name;
this[CAPTURE_NAME_SYMBOL] = name;
this[CAPTURE_CAPTURING_SYMBOL] = capturing;
this[CAPTURE_KIND_SYMBOL] = kind;

// Normalize variadic options
if (options?.variadic) {
Expand All @@ -106,7 +110,7 @@ export class CaptureImpl<T = any> implements Capture<T> {
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;
}
Expand Down Expand Up @@ -135,6 +139,10 @@ export class CaptureImpl<T = any> implements Capture<T> {
getType(): string | Type | undefined {
return this[CAPTURE_TYPE_SYMBOL];
}

getKind(): CaptureKind {
return this[CAPTURE_KIND_SYMBOL];
}
}

export class TemplateParamImpl<T = any> implements TemplateParam<T> {
Expand Down Expand Up @@ -309,14 +317,17 @@ function createCaptureProxy<T>(impl: CaptureImpl<T>): 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') {
return () => target[CAPTURE_NAME_SYMBOL];
}

// 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);
}

Expand Down Expand Up @@ -388,6 +399,17 @@ export function capture<T = any>(nameOrOptions?: string | CaptureOptions<T>): Ca
// Static counter for generating unique IDs for unnamed captures
capture.nextUnnamedId = 1;

// 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;
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.
*
Expand Down Expand Up @@ -581,6 +603,117 @@ export function raw(code: string): RawCode {
return new RawCode(code);
}

/**
* Options specific to expression captures, extending CaptureOptions with type attribution.
*/
export interface ExprCaptureOptions<T = any> extends CaptureOptions<T> {
/**
* 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<any>")
* - A Type instance from the AST
*
* @example
* ```typescript
* const chain = expr({ name: 'chain', type: 'Promise<any>' });
* 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<T = any>(name?: string): Capture<T> & T;
export function expr<T = any>(options: ExprCaptureOptions<T> & { variadic?: never }): Capture<T> & T;
export function expr<T = any>(options: ExprCaptureOptions<T> & { variadic: true | VariadicOptions }): Capture<T[]> & T[];
export function expr<T = any>(nameOrOptions?: string | ExprCaptureOptions<T>): Capture<T> & T {
return createKindCapture(CaptureKind.Expression, nameOrOptions);
}

/**
* Creates an identifier/name capture.
*
* @example
* const n = ident('method');
* pattern`${n}()`
*/
export function ident<T = any>(name?: string): Capture<T> & T;
export function ident<T = any>(options: CaptureOptions<T> & { variadic?: never }): Capture<T> & T;
export function ident<T = any>(options: CaptureOptions<T> & { variadic: true | VariadicOptions }): Capture<T[]> & T[];
export function ident<T = any>(nameOrOptions?: string | CaptureOptions<T>): Capture<T> & T {
return createKindCapture(CaptureKind.Identifier, nameOrOptions);
}

/**
* Creates a type reference capture.
*
* @example
* const t = typeRef('ret');
* pattern`function foo(): ${t}`
*/
export function typeRef<T = any>(name?: string): Capture<T> & T;
export function typeRef<T = any>(options: CaptureOptions<T> & { variadic?: never }): Capture<T> & T;
export function typeRef<T = any>(options: CaptureOptions<T> & { variadic: true | VariadicOptions }): Capture<T[]> & T[];
export function typeRef<T = any>(nameOrOptions?: string | CaptureOptions<T>): Capture<T> & T {
return createKindCapture(CaptureKind.TypeReference, nameOrOptions);
}

/**
* Creates a statement capture.
*
* @example
* const s = stmt('body');
* pattern`if (cond) ${s}`
*/
export function stmt<T = any>(name?: string): Capture<T> & T;
export function stmt<T = any>(options: CaptureOptions<T> & { variadic?: never }): Capture<T> & T;
export function stmt<T = any>(options: CaptureOptions<T> & { variadic: true | VariadicOptions }): Capture<T[]> & T[];
export function stmt<T = any>(nameOrOptions?: string | CaptureOptions<T>): Capture<T> & T {
return createKindCapture(CaptureKind.Statement, nameOrOptions);
}

/**
* Internal helper for kind-specific capture factory functions.
*/
function createKindCapture<T>(kind: CaptureKind, nameOrOptions?: string | ExprCaptureOptions<T>): Capture<T> & T {
let name: string | undefined;
let options: ExprCaptureOptions<T> | undefined;

if (typeof nameOrOptions === 'string') {
name = nameOrOptions;
} else {
options = nameOrOptions;
name = options?.name;
}

const captureName = name || `unnamed_${capture.nextUnnamedId++}`;
const impl = new CaptureImpl<T>(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.
*
Expand Down
20 changes: 16 additions & 4 deletions rewrite-javascript/rewrite/src/javascript/templating/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -429,14 +436,20 @@ export class TemplateEngine {
contextStatements: string[] = [],
dependencies: Record<string, string> = {}
): Promise<J> {
// 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
if (capture instanceof RawCode || (capture && typeof capture === 'object' && (capture as any)[RAW_CODE_SYMBOL])) {
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) {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type {
MatchAttemptResult
} from './types';

export type { ExprCaptureOptions } from './capture';

// Export capture functionality
export {
and,
Expand All @@ -42,7 +44,11 @@ export {
any,
param,
raw,
_
_,
expr,
ident,
typeRef,
stmt
} from './capture';

// Export pattern functionality
Expand Down
53 changes: 15 additions & 38 deletions rewrite-javascript/rewrite/src/javascript/templating/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -136,44 +151,6 @@ export interface CaptureOptions<T = any> {
* ```
*/
constraint?: ConstraintFunction<T>;
/**
* 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<any>", "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<any>', // 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;
}

/**
Expand Down
Loading
Loading