@openrewrite/rewrite 8.79.4 → 8.79.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/java/rpc.ts CHANGED
@@ -52,29 +52,34 @@ class TypeSender extends TypeVisitor<RpcSendQueue> {
52
52
 
53
53
  protected async visitAnnotation(annotation: Type.Annotation, q: RpcSendQueue): Promise<Type | undefined> {
54
54
  await q.getAndSend(annotation, a => asRef(a.type), t => this.visit(t, q));
55
- // await q.getAndSendList(annotation, a => (a.values || []).map(v => asRef(v)), v => {
56
- // let value: any;
57
- // if (v.kind === Type.Kind.SingleElementValue) {
58
- // const single = v as Type.Annotation.SingleElementValue;
59
- // value = single.constantValue !== undefined ? single.constantValue : single.referenceValue;
60
- // } else {
61
- // const array = v as Type.Annotation.ArrayElementValue;
62
- // value = array.constantValues || array.referenceValues;
63
- // }
64
- // return `${Type.signature(v.element)}:${value == null ? "null" : value.toString()}`;
65
- // }, async v => {
66
- // // Handle element values inline like the Java implementation
67
- // await q.getAndSend(v, e => asRef(e.element), elem => this.visit(elem, q));
68
- // if (v.kind === Type.Kind.SingleElementValue) {
69
- // const single = v as Type.Annotation.SingleElementValue;
70
- // await q.getAndSend(single, s => s.constantValue);
71
- // await q.getAndSend(single, s => asRef(s.referenceValue), ref => this.visit(ref, q));
72
- // } else if (v.kind === Type.Kind.ArrayElementValue) {
73
- // const array = v as Type.Annotation.ArrayElementValue;
74
- // await q.getAndSendList(array, a => a.constantValues || [], val => val == null ? "null" : val.toString());
75
- // await q.getAndSendList(array, a => (a.referenceValues || []).map(r => asRef(r)), t => Type.signature(t), r => this.visit(r, q));
76
- // }
77
- // });
55
+ await q.getAndSendList(
56
+ annotation,
57
+ a => (a.values || []).map(v => asRef(v)),
58
+ v => Type.signature(v.element),
59
+ async v => {
60
+ await q.getAndSend(v, e => asRef(e.element), elem => this.visit(elem, q));
61
+ if (v.kind === Type.Kind.ArrayElementValue) {
62
+ const array = v as Type.Annotation.ArrayElementValue;
63
+ // constantValues are sent as raw JSON-native values; numeric subtypes
64
+ // (int/long/float/double) and char/string distinctions are not preserved.
65
+ await q.getAndSendList(
66
+ array,
67
+ a => a.constantValues,
68
+ v => v == null ? "null" : v.toString(),
69
+ );
70
+ await q.getAndSendList(
71
+ array,
72
+ a => (a.referenceValues || []).map(r => asRef(r)),
73
+ t => Type.signature(t),
74
+ t => this.visit(t, q),
75
+ );
76
+ } else {
77
+ const single = v as Type.Annotation.SingleElementValue;
78
+ await q.getAndSend(single, s => s.constantValue);
79
+ await q.getAndSend(single, s => asRef(s.referenceValue), ref => this.visit(ref, q));
80
+ }
81
+ },
82
+ );
78
83
  return annotation;
79
84
  }
80
85
 
@@ -173,33 +178,29 @@ class TypeReceiver extends TypeVisitor<RpcReceiveQueue> {
173
178
 
174
179
  protected async visitAnnotation(annotation: Type.Annotation, q: RpcReceiveQueue): Promise<Type | undefined> {
175
180
  annotation.type = await q.receive(annotation.type, t => this.visit(t, q));
176
- // annotation.values = await q.receiveList(annotation.values, async v => {
177
- // // Handle element values inline like the Java implementation
178
- // if (v.kind === Type.Kind.SingleElementValue) {
179
- // const single = v as Type.Annotation.SingleElementValue;
180
- // const element = await q.receive(single.element, elem => this.visit(elem, q));
181
- // const constantValue = await q.receive(single.constantValue);
182
- // const referenceValue = await q.receive(single.referenceValue, ref => this.visit(ref, q));
183
- // return {
184
- // kind: Type.Kind.SingleElementValue,
185
- // element,
186
- // constantValue,
187
- // referenceValue
188
- // } as Type.Annotation.SingleElementValue;
189
- // } else if (v.kind === Type.Kind.ArrayElementValue) {
190
- // const array = v as Type.Annotation.ArrayElementValue;
191
- // const element = await q.receive(array.element, elem => this.visit(elem, q));
192
- // const constantValues = await q.receiveList(array.constantValues);
193
- // const referenceValues = await q.receiveList(array.referenceValues, r => this.visit(r, q));
194
- // return {
195
- // kind: Type.Kind.ArrayElementValue,
196
- // element,
197
- // constantValues,
198
- // referenceValues
199
- // } as Type.Annotation.ArrayElementValue;
200
- // }
201
- // return v;
202
- // }) || [];
181
+ annotation.values = (await q.receiveList(annotation.values, async v => {
182
+ const element = await q.receive((v as Type.Annotation.ElementValue).element, elem => this.visit(elem, q));
183
+ if (v.kind === Type.Kind.ArrayElementValue) {
184
+ const array = v as Type.Annotation.ArrayElementValue;
185
+ const constantValues = await q.receiveList(array.constantValues);
186
+ const referenceValues = await q.receiveList(array.referenceValues, r => this.visit(r, q));
187
+ return {
188
+ kind: Type.Kind.ArrayElementValue,
189
+ element,
190
+ constantValues,
191
+ referenceValues,
192
+ } as Type.Annotation.ArrayElementValue;
193
+ }
194
+ const single = v as Type.Annotation.SingleElementValue;
195
+ const constantValue = await q.receive(single.constantValue);
196
+ const referenceValue = await q.receive(single.referenceValue, ref => this.visit(ref, q));
197
+ return {
198
+ kind: Type.Kind.SingleElementValue,
199
+ element,
200
+ constantValue,
201
+ referenceValue,
202
+ } as Type.Annotation.SingleElementValue;
203
+ })) || [];
203
204
  return annotation;
204
205
  }
205
206
 
@@ -733,7 +733,7 @@ export class JavaScriptParserVisitor {
733
733
  )),
734
734
  end: this.prefix(node.getLastToken()!)
735
735
  },
736
- type: this.mapType(node)
736
+ type: this.mapDeclarationType(node)
737
737
  };
738
738
  }
739
739
 
@@ -2723,7 +2723,7 @@ export class JavaScriptParserVisitor {
2723
2723
  })),
2724
2724
  end: this.prefix(node.getLastToken()!)
2725
2725
  },
2726
- type: this.mapType(node)
2726
+ type: this.mapDeclarationType(node)
2727
2727
  } satisfies J.ClassDeclaration as J.ClassDeclaration,
2728
2728
  }
2729
2729
  }
@@ -3414,7 +3414,7 @@ export class JavaScriptParserVisitor {
3414
3414
  })),
3415
3415
  end: this.prefix(node.getLastToken()!)
3416
3416
  },
3417
- type: this.mapType(node)
3417
+ type: this.mapDeclarationType(node)
3418
3418
  };
3419
3419
  }
3420
3420
 
@@ -3475,7 +3475,7 @@ export class JavaScriptParserVisitor {
3475
3475
  emptySpace)],
3476
3476
  end: this.prefix(node.getLastToken()!)
3477
3477
  },
3478
- type: this.mapType(node) as Type.Class
3478
+ type: this.mapDeclarationType(node) as Type.Class
3479
3479
  };
3480
3480
  }
3481
3481
 
@@ -4427,6 +4427,10 @@ export class JavaScriptParserVisitor {
4427
4427
  return this.typeMapping?.type(node);
4428
4428
  }
4429
4429
 
4430
+ private mapDeclarationType(node: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.EnumDeclaration): Type.FullyQualified | undefined {
4431
+ return this.typeMapping?.declarationType(node);
4432
+ }
4433
+
4430
4434
  private mapPrimitiveType(node: ts.Node): Type.Primitive {
4431
4435
  return this.typeMapping?.primitiveType(node) ?? Type.Primitive.None;
4432
4436
  }
@@ -83,6 +83,86 @@ export class JavaScriptTypeMapping {
83
83
  return type && this.getType(type);
84
84
  }
85
85
 
86
+ /**
87
+ * Resolve the declared type of a class / interface / enum / class-expression.
88
+ *
89
+ * Using {@code getTypeAtLocation} on these declaration nodes is unsafe: TypeScript returns
90
+ * the type of the declared *value*, not the type itself. For string or numeric enums that
91
+ * ends up being the union of enum literals, which on the JS side resolves to
92
+ * {@link Type.Primitive} (e.g. String for string enums). The Java-side RPC receiver then
93
+ * rejects it with "A class can only be type attributed with a fully qualified type name",
94
+ * because {@code J.ClassDeclaration.type} must be {@link Type.FullyQualified}.
95
+ *
96
+ * Instead, we resolve through the declaration's own symbol. For classes and interfaces we
97
+ * reuse the full type mapping pipeline (so members, methods, supertypes and type parameters
98
+ * are populated). For enums — which have no class-shaped representation in TypeScript — and
99
+ * any residual non-FQ result, we build a minimal class shell whose FQN and {@code classKind}
100
+ * are derived from the declaration itself.
101
+ */
102
+ declarationType(node: ts.ClassDeclaration | ts.ClassExpression | ts.InterfaceDeclaration | ts.EnumDeclaration): Type.FullyQualified | undefined {
103
+ const symbol = (node as { symbol?: ts.Symbol }).symbol
104
+ ?? (node.name ? this.checker.getSymbolAtLocation(node.name) : undefined);
105
+ if (!symbol) {
106
+ return undefined;
107
+ }
108
+
109
+ // For classes and interfaces `getDeclaredTypeOfSymbol` yields a class-shaped ts.Type whose
110
+ // symbol we can feed through the normal pipeline. For enums, however, TypeScript returns the
111
+ // union of enum literals — which on the JS side resolves to `Type.Primitive` (e.g. String for
112
+ // string enums) and therefore is NOT `FullyQualified`. The Java side then rejects it with
113
+ // "A class can only be type attributed with a fully qualified type name".
114
+ //
115
+ // Since JavaScript/TypeScript has no separate enum runtime representation, we build the
116
+ // class-shaped type directly from the declaration's own symbol. The `classKind` is derived
117
+ // from the declaration node kind, not the (lossy) resolved type.
118
+ let classKind: Type.Class.Kind;
119
+ if (ts.isEnumDeclaration(node)) {
120
+ classKind = Type.Class.Kind.Enum;
121
+ } else if (ts.isInterfaceDeclaration(node)) {
122
+ classKind = Type.Class.Kind.Interface;
123
+ } else {
124
+ classKind = Type.Class.Kind.Class;
125
+ }
126
+
127
+ const fullyQualifiedName = this.getFullyQualifiedNameFromSymbol(symbol);
128
+ const cacheKey = `decl:${classKind}:${fullyQualifiedName}`;
129
+ const cached = this.typeCache.get(cacheKey);
130
+ if (cached && Type.isFullyQualified(cached)) {
131
+ return cached as Type.FullyQualified;
132
+ }
133
+
134
+ // For classes and interfaces reuse the existing type-based path so that members, methods,
135
+ // supertypes and type parameters are populated. Enums (and any fallback that produced a
136
+ // non-FQ result) get a minimal class shell with the correct FQN + kind.
137
+ if (!ts.isEnumDeclaration(node)) {
138
+ const declaredType = this.checker.getDeclaredTypeOfSymbol(symbol);
139
+ if (declaredType) {
140
+ const mapped = this.getType(declaredType);
141
+ if (Type.isFullyQualified(mapped)) {
142
+ this.typeCache.set(cacheKey, mapped);
143
+ return mapped;
144
+ }
145
+ }
146
+ }
147
+
148
+ const classType: Type.Class = {
149
+ kind: Type.Kind.Class,
150
+ flags: 0,
151
+ classKind,
152
+ fullyQualifiedName,
153
+ typeParameters: [],
154
+ annotations: [],
155
+ interfaces: [],
156
+ members: [],
157
+ methods: [],
158
+ toJSON: function () {
159
+ return Type.signature(this);
160
+ }
161
+ } as Type.Class;
162
+ this.typeCache.set(cacheKey, classType);
163
+ return classType;
164
+ }
165
+
86
166
  private getType(type: ts.Type): Type {
87
167
  // Check for error types first - these indicate type-checking failures
88
168
  // and should not be processed further
@@ -990,7 +1070,10 @@ export class JavaScriptTypeMapping {
990
1070
  if (!symbol) {
991
1071
  return "unknown";
992
1072
  }
1073
+ return this.getFullyQualifiedNameFromSymbol(symbol);
1074
+ }
993
1075
 
1076
+ private getFullyQualifiedNameFromSymbol(symbol: ts.Symbol): string {
994
1077
  // First, check if this symbol is an import/alias
995
1078
  // For imported types, we want to use the module specifier instead of the file path
996
1079
  if (symbol.flags & ts.SymbolFlags.Alias) {
@@ -28,6 +28,12 @@ import {replaceMarkerByKind} from "../../markers";
28
28
  export interface ParseProjectResponseItem {
29
29
  id: UUID;
30
30
  sourceFileType: string;
31
+ /**
32
+ * Relative source path of the discovered file. Used by the Java side to report
33
+ * per-file failures (e.g., a failed `GetObject` deserialization) without aborting
34
+ * the rest of the stream. Optional for older callers.
35
+ */
36
+ sourcePath?: string;
31
37
  }
32
38
 
33
39
  /**
@@ -88,7 +94,7 @@ export class ParseProject {
88
94
  });
89
95
  const generator = parser.parse(...discovered.packageJsonFiles);
90
96
 
91
- for (const _ of discovered.packageJsonFiles) {
97
+ for (const filePath of discovered.packageJsonFiles) {
92
98
  const id = randomId();
93
99
  localObjects.set(id, async (id: string) => {
94
100
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -96,7 +102,8 @@ export class ParseProject {
96
102
  });
97
103
  resultItems.push({
98
104
  id,
99
- sourceFileType: "org.openrewrite.json.tree.Json$Document" // break cycle
105
+ sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle
106
+ sourcePath: path.relative(relativeTo, filePath)
100
107
  });
101
108
  }
102
109
  }
@@ -106,7 +113,7 @@ export class ParseProject {
106
113
  const parser = Parsers.createParser("json", {ctx, relativeTo});
107
114
  const generator = parser.parse(...discovered.lockFiles.json);
108
115
 
109
- for (const _ of discovered.lockFiles.json) {
116
+ for (const filePath of discovered.lockFiles.json) {
110
117
  const id = randomId();
111
118
  localObjects.set(id, async (id: string) => {
112
119
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -114,7 +121,8 @@ export class ParseProject {
114
121
  });
115
122
  resultItems.push({
116
123
  id,
117
- sourceFileType: "org.openrewrite.json.tree.Json$Document" // break cycle
124
+ sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle
125
+ sourcePath: path.relative(relativeTo, filePath)
118
126
  });
119
127
  }
120
128
  }
@@ -124,7 +132,7 @@ export class ParseProject {
124
132
  const parser = Parsers.createParser("yaml", {ctx, relativeTo});
125
133
  const generator = parser.parse(...discovered.lockFiles.yaml);
126
134
 
127
- for (const _ of discovered.lockFiles.yaml) {
135
+ for (const filePath of discovered.lockFiles.yaml) {
128
136
  const id = randomId();
129
137
  localObjects.set(id, async (id: string) => {
130
138
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -132,7 +140,8 @@ export class ParseProject {
132
140
  });
133
141
  resultItems.push({
134
142
  id,
135
- sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents" // break cycle
143
+ sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents", // break cycle
144
+ sourcePath: path.relative(relativeTo, filePath)
136
145
  });
137
146
  }
138
147
  }
@@ -142,7 +151,7 @@ export class ParseProject {
142
151
  const parser = Parsers.createParser("plainText", {ctx, relativeTo});
143
152
  const generator = parser.parse(...discovered.lockFiles.text);
144
153
 
145
- for (const _ of discovered.lockFiles.text) {
154
+ for (const filePath of discovered.lockFiles.text) {
146
155
  const id = randomId();
147
156
  localObjects.set(id, async (id: string) => {
148
157
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -150,7 +159,8 @@ export class ParseProject {
150
159
  });
151
160
  resultItems.push({
152
161
  id,
153
- sourceFileType: "org.openrewrite.text.PlainText" // break cycle
162
+ sourceFileType: "org.openrewrite.text.PlainText", // break cycle
163
+ sourcePath: path.relative(relativeTo, filePath)
154
164
  });
155
165
  }
156
166
  }
@@ -185,16 +195,18 @@ export class ParseProject {
185
195
  });
186
196
  resultItems.push({
187
197
  id,
188
- sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit" // break cycle
198
+ sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit", // break cycle
199
+ sourcePath: path.relative(relativeTo, filePath)
189
200
  });
190
201
  }
191
202
  } else {
192
203
  // Prettier is NOT available: auto-detect styles from parsed files
193
204
  // Parse all files first to sample them
194
- const parsedFiles: {id: string, sourceFile: SourceFile}[] = [];
205
+ const parsedFiles: {id: string, sourceFile: SourceFile, filePath: string}[] = [];
206
+ let fileIndex = 0;
195
207
  for await (const sourceFile of parser.parse(...discovered.jsFiles)) {
196
208
  const id = randomId();
197
- parsedFiles.push({id, sourceFile});
209
+ parsedFiles.push({id, sourceFile, filePath: discovered.jsFiles[fileIndex++]});
198
210
  }
199
211
 
200
212
  // Sample all parsed files and build Autodetect marker using ProjectParser helper
@@ -203,7 +215,7 @@ export class ParseProject {
203
215
  );
204
216
 
205
217
  // Store thunks that add the Autodetect marker
206
- for (const {id, sourceFile} of parsedFiles) {
218
+ for (const {id, sourceFile, filePath} of parsedFiles) {
207
219
  localObjects.set(id, async (newId: string) => {
208
220
  return {
209
221
  ...sourceFile,
@@ -213,7 +225,8 @@ export class ParseProject {
213
225
  });
214
226
  resultItems.push({
215
227
  id,
216
- sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit" // break cycle
228
+ sourceFileType: "org.openrewrite.javascript.tree.JS$CompilationUnit", // break cycle
229
+ sourcePath: path.relative(relativeTo, filePath)
217
230
  });
218
231
  }
219
232
  }
@@ -224,7 +237,7 @@ export class ParseProject {
224
237
  const parser = Parsers.createParser("yaml", {ctx, relativeTo});
225
238
  const generator = parser.parse(...discovered.yamlFiles);
226
239
 
227
- for (const _ of discovered.yamlFiles) {
240
+ for (const filePath of discovered.yamlFiles) {
228
241
  const id = randomId();
229
242
  localObjects.set(id, async (id: string) => {
230
243
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -232,7 +245,8 @@ export class ParseProject {
232
245
  });
233
246
  resultItems.push({
234
247
  id,
235
- sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents" // break cycle
248
+ sourceFileType: "org.openrewrite.yaml.tree.Yaml$Documents", // break cycle
249
+ sourcePath: path.relative(relativeTo, filePath)
236
250
  });
237
251
  }
238
252
  }
@@ -242,7 +256,7 @@ export class ParseProject {
242
256
  const parser = Parsers.createParser("json", {ctx, relativeTo});
243
257
  const generator = parser.parse(...discovered.jsonFiles);
244
258
 
245
- for (const _ of discovered.jsonFiles) {
259
+ for (const filePath of discovered.jsonFiles) {
246
260
  const id = randomId();
247
261
  localObjects.set(id, async (id: string) => {
248
262
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -250,7 +264,8 @@ export class ParseProject {
250
264
  });
251
265
  resultItems.push({
252
266
  id,
253
- sourceFileType: "org.openrewrite.json.tree.Json$Document" // break cycle
267
+ sourceFileType: "org.openrewrite.json.tree.Json$Document", // break cycle
268
+ sourcePath: path.relative(relativeTo, filePath)
254
269
  });
255
270
  }
256
271
  }
@@ -260,7 +275,7 @@ export class ParseProject {
260
275
  const parser = Parsers.createParser("plainText", {ctx, relativeTo});
261
276
  const generator = parser.parse(...discovered.textFiles);
262
277
 
263
- for (const _ of discovered.textFiles) {
278
+ for (const filePath of discovered.textFiles) {
264
279
  const id = randomId();
265
280
  localObjects.set(id, async (id: string) => {
266
281
  const sourceFile: SourceFile = (await generator.next()).value;
@@ -268,7 +283,8 @@ export class ParseProject {
268
283
  });
269
284
  resultItems.push({
270
285
  id,
271
- sourceFileType: "org.openrewrite.text.PlainText" // break cycle
286
+ sourceFileType: "org.openrewrite.text.PlainText", // break cycle
287
+ sourcePath: path.relative(relativeTo, filePath)
272
288
  });
273
289
  }
274
290
  }