Skip to content

Commit 648060e

Browse files
committed
Use intersection types for RightPadded/LeftPadded in JavaScript AST
This changes `RightPadded<T extends J>` and `LeftPadded<T extends J>` from wrapper objects with an `element` property to intersection types where the padded value IS the element with padding mixed in: - `RightPadded<T extends J>` = `T & RightPaddingMixin` - `LeftPadded<T extends J>` = `T & LeftPaddingMixin` This aligns the TypeScript model more closely with the Java model and simplifies property access (no more `.element` for J nodes). Key changes: - Updated type definitions in tree.ts for intersection-based padding - Fixed visitor methods to handle intersection types correctly - Added function overloads for visitLeftPadded/visitRightPadded with type-safe return types (primitives never return undefined) - Fixed unwrap() in comparator.ts for intersection types (parens.tree instead of parens.tree.element) - Added guards for missing padding in formatting methods - Fixed WhitespaceReconciler to use ID comparison instead of reference equality for stopAfter functionality - Added hasSameKind override in semantic comparator for void/undefined equivalence
1 parent 4a46632 commit 648060e

68 files changed

Lines changed: 1840 additions & 983 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

rewrite-javascript/rewrite/fixtures/replace-assignment.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,22 @@ export class ReplaceAssignment extends Recipe {
3838
const envVarValue = "'" + process.env[envVar] + "'";
3939
return new class extends JavaScriptVisitor<ExecutionContext> {
4040
protected async visitVariable(variable: J.VariableDeclarations.NamedVariable, c: ExecutionContext): Promise<J | undefined> {
41-
if ((variable.initializer!.element as J.Literal).valueSource === envVarValue) {
41+
if ((variable.initializer! as J.Literal).valueSource === envVarValue) {
4242
return super.visitVariable(variable, c);
4343
}
4444
const [draft, finishDraft] = create(variable);
4545
draft.initializer = {
46-
kind: J.Kind.LeftPadded,
46+
kind: J.Kind.Literal,
47+
id: randomId(),
48+
prefix: singleSpace,
4749
markers: emptyMarkers,
48-
element: {
49-
kind: J.Kind.Literal,
50-
id: randomId(),
51-
prefix: singleSpace,
50+
valueSource: envVarValue,
51+
type: Type.Primitive.String,
52+
padding: {
53+
before: singleSpace,
5254
markers: emptyMarkers,
53-
valueSource: envVarValue,
54-
type: Type.Primitive.String,
55-
} as J.Literal,
56-
before: singleSpace,
57-
};
55+
}
56+
} as J.LeftPadded<J.Literal>;
5857
return finishDraft();
5958
}
6059
}

rewrite-javascript/rewrite/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rewrite-javascript/rewrite/src/java/formatting-utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ import {create as produce} from "mutative";
2222
* When there are comments, the last whitespace is the suffix of the last comment.
2323
* When there are no comments, it's the whitespace property.
2424
*/
25-
export function lastWhitespace(space: J.Space): string {
25+
export function lastWhitespace(space: J.Space | undefined): string {
26+
if (!space) {
27+
return "";
28+
}
2629
if (space.comments.length > 0) {
2730
return space.comments[space.comments.length - 1].suffix;
2831
}

rewrite-javascript/rewrite/src/java/rpc.ts

Lines changed: 123 additions & 61 deletions
Large diffs are not rendered by default.

rewrite-javascript/rewrite/src/java/tree.ts

Lines changed: 169 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -754,20 +754,88 @@ export namespace J {
754754
readonly text: string;
755755
}
756756

757-
export interface LeftPadded<T extends J | Space | number | string | boolean> {
758-
readonly kind: typeof Kind.LeftPadded;
757+
/**
758+
* Represents padding information that appears before an element.
759+
* Used in LeftPadded types to hold the prefix space and markers.
760+
*/
761+
export interface Prefix {
759762
readonly before: Space;
760-
readonly element: T;
761763
readonly markers: Markers;
762764
}
763765

764-
export interface RightPadded<T extends J | boolean> {
765-
readonly kind: typeof Kind.RightPadded;
766-
readonly element: T;
766+
/**
767+
* Represents padding information that appears after an element.
768+
* Used in RightPadded types to hold the suffix space and markers.
769+
*/
770+
export interface Suffix {
767771
readonly after: Space;
768772
readonly markers: Markers;
769773
}
770774

775+
/**
776+
* Padding mixin for left-padded elements.
777+
* Nests padding info under `padding` to avoid conflicts with element's own `markers`.
778+
*/
779+
export interface LeftPaddingMixin {
780+
readonly padding: Prefix;
781+
}
782+
783+
/**
784+
* Padding mixin for right-padded elements.
785+
* Nests padding info under `padding` to avoid conflicts with element's own `markers`.
786+
*/
787+
export interface RightPaddingMixin {
788+
readonly padding: Suffix;
789+
}
790+
791+
/**
792+
* Wrapper for primitive values in padding (boolean, number, string).
793+
* Primitives cannot use intersection types, so they use this wrapper.
794+
* Space is an object type, so it uses intersection instead.
795+
*/
796+
export interface PaddedPrimitive<T extends number | string | boolean> {
797+
readonly element: T;
798+
}
799+
800+
/**
801+
* LeftPadded represents an element with whitespace/comments before it.
802+
*
803+
* For tree nodes (J) and Space: Uses intersection type - access properties directly.
804+
* Example: `fieldAccess.name.simpleName` (no `.element` needed)
805+
* Example: `arrayType.dimension.whitespace` (Space properties directly)
806+
* Padding accessed via: `node.padding.before`, `node.padding.markers`
807+
*
808+
* For primitives (boolean, number, string): Uses wrapper - access via `.element` property.
809+
* Example: `import.static.element` (boolean value)
810+
* Padding accessed via: `node.padding.before`, `node.padding.markers`
811+
*/
812+
export type LeftPadded<T extends J | Space | number | string | boolean> =
813+
T extends J
814+
? T & LeftPaddingMixin
815+
: T extends Space
816+
? Space & LeftPaddingMixin
817+
: PaddedPrimitive<Extract<T, number | string | boolean>> & LeftPaddingMixin;
818+
819+
/**
820+
* RightPadded represents an element with whitespace/comments after it.
821+
*
822+
* For tree nodes (J): Uses intersection type - access properties directly.
823+
* Example: `stmt.kind` (no `.element` needed)
824+
* Padding accessed via: `stmt.padding.after`, `stmt.padding.markers`
825+
*
826+
* For booleans: Uses wrapper - access via `.element` property.
827+
* Padding accessed via: `node.padding.after`, `node.padding.markers`
828+
*/
829+
export type RightPadded<T extends J | boolean> =
830+
T extends J
831+
? T & RightPaddingMixin
832+
: PaddedPrimitive<T & boolean> & RightPaddingMixin;
833+
834+
/**
835+
* Container represents a bracketed group of elements (e.g., method arguments,
836+
* type parameters). Has space before the opening bracket and a list of
837+
* right-padded elements.
838+
*/
771839
export interface Container<T extends J> {
772840
readonly kind: typeof Kind.Container;
773841
readonly before: Space;
@@ -851,13 +919,98 @@ export const isNewClass = (n: any): n is J.NewClass => n.kind === J.Kind.NewClas
851919
export const isReturn = (n: any): n is J.Return => n.kind === J.Kind.Return;
852920
export const isVariableDeclarations = (n: any): n is J.VariableDeclarations => n.kind === J.Kind.VariableDeclarations;
853921

854-
export function rightPadded<T extends J | boolean>(t: T, trailing: J.Space, markers?: Markers): J.RightPadded<T> {
855-
return {
856-
kind: J.Kind.RightPadded,
857-
element: t,
858-
after: trailing,
859-
markers: markers ?? emptyMarkers
860-
};
922+
/**
923+
* Creates a RightPadded value.
924+
*
925+
* For tree nodes (J): Returns intersection type with nested padding.
926+
* For booleans: Returns wrapper with `element` property and nested padding.
927+
*/
928+
export function rightPadded<T extends J>(t: T, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded<T>;
929+
export function rightPadded(t: boolean, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded<boolean>;
930+
export function rightPadded<T extends J | boolean>(t: T, trailing: J.Space, paddingMarkers?: Markers): J.RightPadded<T> {
931+
const padding: J.Suffix = { after: trailing, markers: paddingMarkers ?? emptyMarkers };
932+
if (typeof t === 'boolean') {
933+
// Primitive: create wrapper with nested padding
934+
return { element: t, padding } as J.RightPadded<T>;
935+
} else {
936+
// Tree node: merge with nested padding
937+
return { ...t as object, padding } as J.RightPadded<T>;
938+
}
939+
}
940+
941+
/**
942+
* Creates a LeftPadded value.
943+
*
944+
* For tree nodes (J) and Space: Returns intersection type with nested padding.
945+
* For primitives (boolean, number, string): Returns wrapper with `element` property and nested padding.
946+
*/
947+
export function leftPadded<T extends J>(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded<T>;
948+
export function leftPadded(t: J.Space, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded<J.Space>;
949+
export function leftPadded<T extends number | string | boolean>(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded<T>;
950+
export function leftPadded<T extends J | J.Space | number | string | boolean>(t: T, leading: J.Space, paddingMarkers?: Markers): J.LeftPadded<T> {
951+
const padding: J.Prefix = { before: leading, markers: paddingMarkers ?? emptyMarkers };
952+
if (typeof t === 'boolean' || typeof t === 'number' || typeof t === 'string') {
953+
// Primitive: create wrapper with nested padding
954+
return { element: t, padding } as J.LeftPadded<T>;
955+
} else {
956+
// Tree node or Space: merge with nested padding (intersection)
957+
return { ...t as object, padding } as J.LeftPadded<T>;
958+
}
959+
}
960+
961+
/**
962+
* Type guard to check if a padded value uses intersection type (tree nodes or Space)
963+
* vs a primitive wrapper (has `element` property for boolean, number, string).
964+
*/
965+
export function isIntersectionPadded(padded: any): boolean {
966+
// All padded values have a `padding` property
967+
// Intersection types (tree nodes, Space) don't have `element`
968+
// Primitive wrappers have both `padding` and `element`
969+
return padded && typeof padded === 'object' && 'padding' in padded && !('element' in padded);
970+
}
971+
972+
/**
973+
* @deprecated Use isIntersectionPadded instead
974+
*/
975+
export const isTreePadded = isIntersectionPadded;
976+
977+
/**
978+
* Extracts the element from a padded value.
979+
* For tree nodes and Space: returns the padded value itself (it IS the element with padding mixed in).
980+
* For primitives (boolean, number, string): returns the `.element` property.
981+
*/
982+
export function getPaddedElement<T extends J>(padded: J.LeftPadded<T> | J.RightPadded<T>): T;
983+
export function getPaddedElement(padded: J.LeftPadded<J.Space>): J.Space;
984+
export function getPaddedElement<T extends number | string | boolean>(padded: J.LeftPadded<T>): T;
985+
export function getPaddedElement<T extends boolean>(padded: J.RightPadded<T>): T;
986+
// Catch-all overloads for union types with J and primitives
987+
export function getPaddedElement<T extends J | boolean>(padded: J.RightPadded<T>): T;
988+
export function getPaddedElement<T extends J | J.Space | number | string | boolean>(padded: J.LeftPadded<T>): T;
989+
export function getPaddedElement<T>(padded: any): T {
990+
if ('element' in padded) {
991+
return padded.element;
992+
}
993+
// For tree nodes and Space, the padded value IS the element
994+
return padded as T;
995+
}
996+
997+
/**
998+
* Sets the element in a padded value, returning a new padded value.
999+
* For tree nodes and Space: merges the new element with existing padding.
1000+
* For primitives (boolean, number, string): creates a new wrapper with the new element.
1001+
*/
1002+
export function withPaddedElement<T extends J>(padded: J.LeftPadded<T>, newElement: T): J.LeftPadded<T>;
1003+
export function withPaddedElement<T extends J>(padded: J.RightPadded<T>, newElement: T): J.RightPadded<T>;
1004+
export function withPaddedElement(padded: J.LeftPadded<J.Space>, newElement: J.Space): J.LeftPadded<J.Space>;
1005+
export function withPaddedElement<T extends number | string | boolean>(padded: J.LeftPadded<T>, newElement: T): J.LeftPadded<T>;
1006+
export function withPaddedElement<T extends boolean>(padded: J.RightPadded<T>, newElement: T): J.RightPadded<T>;
1007+
export function withPaddedElement<T>(padded: any, newElement: T): any {
1008+
if ('element' in padded) {
1009+
// Primitive wrapper - update element, preserve padding
1010+
return { ...padded, element: newElement };
1011+
}
1012+
// Tree node or Space - merge new element with existing padding
1013+
return { ...newElement as object, padding: padded.padding };
8611014
}
8621015

8631016
export namespace TypedTree {
@@ -883,18 +1036,18 @@ export namespace TypedTree {
8831036

8841037
registerTypeGetter(J.Kind.MethodDeclaration, (tree: J.MethodDeclaration) => tree.methodType?.returnType);
8851038
registerTypeGetter(J.Kind.MethodInvocation, (tree: J.MethodInvocation) => tree.methodType?.returnType);
886-
registerTypeGetter(J.Kind.Parentheses, (tree: J.Parentheses<TypedTree>) => getType(tree.tree.element));
1039+
registerTypeGetter(J.Kind.Parentheses, (tree: J.Parentheses<TypedTree>) => getType(tree.tree));
8871040
registerTypeGetter(J.Kind.NewClass, (tree: J.NewClass) => tree.constructorType?.returnType);
8881041

8891042
// TODO ControlParentheses here isn't a TypedTree so why does this compile?
8901043
registerTypeGetter(J.Kind.TypeCast, (tree: J.TypeCast) => getType(tree.class));
8911044

8921045
registerTypeGetter(J.Kind.Empty, () => Type.unknownType);
8931046
registerTypeGetter(J.Kind.MultiCatch, (tree: J.MultiCatch) => {
894-
const bounds = tree.alternatives.map(a => getType(a.element));
1047+
const bounds = tree.alternatives.map(a => getType(a));
8951048
return {kind: Type.Kind.Union, bounds: bounds};
8961049
});
897-
registerTypeGetter(J.Kind.NullableType, (tree: J.NullableType) => getType(tree.typeTree.element));
1050+
registerTypeGetter(J.Kind.NullableType, (tree: J.NullableType) => getType(tree.typeTree));
8981051
registerTypeGetter(J.Kind.Wildcard, () => Type.unknownType);
8991052
registerTypeGetter(J.Kind.Unknown, () => Type.unknownType);
9001053
}

0 commit comments

Comments
 (0)