@openrewrite/rewrite 8.92.16 → 8.92.18

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.
Files changed (254) hide show
  1. package/README.md +48 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/java/formatting-utils.d.ts +2 -1
  6. package/dist/java/formatting-utils.d.ts.map +1 -1
  7. package/dist/java/formatting-utils.js +4 -3
  8. package/dist/java/formatting-utils.js.map +1 -1
  9. package/dist/java/rpc.d.ts.map +1 -1
  10. package/dist/java/rpc.js +2 -0
  11. package/dist/java/rpc.js.map +1 -1
  12. package/dist/java/tree.d.ts +8 -0
  13. package/dist/java/tree.d.ts.map +1 -1
  14. package/dist/java/tree.js +11 -1
  15. package/dist/java/tree.js.map +1 -1
  16. package/dist/java/type.d.ts +7 -1
  17. package/dist/java/type.d.ts.map +1 -1
  18. package/dist/java/type.js +16 -1
  19. package/dist/java/type.js.map +1 -1
  20. package/dist/javascript/add-import.d.ts +27 -21
  21. package/dist/javascript/add-import.d.ts.map +1 -1
  22. package/dist/javascript/add-import.js +303 -143
  23. package/dist/javascript/add-import.js.map +1 -1
  24. package/dist/javascript/amd.d.ts.map +1 -1
  25. package/dist/javascript/amd.js +25 -10
  26. package/dist/javascript/amd.js.map +1 -1
  27. package/dist/javascript/autodetect.js +36 -45
  28. package/dist/javascript/autodetect.js.map +1 -1
  29. package/dist/javascript/binding.d.ts +6 -4
  30. package/dist/javascript/binding.d.ts.map +1 -1
  31. package/dist/javascript/binding.js +54 -18
  32. package/dist/javascript/binding.js.map +1 -1
  33. package/dist/javascript/comparator.d.ts +22 -5
  34. package/dist/javascript/comparator.d.ts.map +1 -1
  35. package/dist/javascript/comparator.js +120 -38
  36. package/dist/javascript/comparator.js.map +1 -1
  37. package/dist/javascript/format/format.d.ts +2 -0
  38. package/dist/javascript/format/format.d.ts.map +1 -1
  39. package/dist/javascript/format/format.js +11 -1
  40. package/dist/javascript/format/format.js.map +1 -1
  41. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  42. package/dist/javascript/format/tabs-and-indents-visitor.js +31 -4
  43. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  44. package/dist/javascript/index.d.ts +4 -2
  45. package/dist/javascript/index.d.ts.map +1 -1
  46. package/dist/javascript/index.js +10 -1
  47. package/dist/javascript/index.js.map +1 -1
  48. package/dist/javascript/markers.d.ts +12 -0
  49. package/dist/javascript/markers.d.ts.map +1 -1
  50. package/dist/javascript/markers.js +81 -2
  51. package/dist/javascript/markers.js.map +1 -1
  52. package/dist/javascript/method-matcher.d.ts +13 -2
  53. package/dist/javascript/method-matcher.d.ts.map +1 -1
  54. package/dist/javascript/method-matcher.js +32 -19
  55. package/dist/javascript/method-matcher.js.map +1 -1
  56. package/dist/javascript/node-resolution-result.js +3 -2
  57. package/dist/javascript/node-resolution-result.js.map +1 -1
  58. package/dist/javascript/package-name.d.ts +6 -0
  59. package/dist/javascript/package-name.d.ts.map +1 -0
  60. package/dist/javascript/package-name.js +47 -0
  61. package/dist/javascript/package-name.js.map +1 -0
  62. package/dist/javascript/parser.d.ts +9 -0
  63. package/dist/javascript/parser.d.ts.map +1 -1
  64. package/dist/javascript/parser.js +185 -88
  65. package/dist/javascript/parser.js.map +1 -1
  66. package/dist/javascript/preconditions.d.ts +4 -1
  67. package/dist/javascript/preconditions.d.ts.map +1 -1
  68. package/dist/javascript/preconditions.js +7 -4
  69. package/dist/javascript/preconditions.js.map +1 -1
  70. package/dist/javascript/print.d.ts +10 -1
  71. package/dist/javascript/print.d.ts.map +1 -1
  72. package/dist/javascript/print.js +120 -77
  73. package/dist/javascript/print.js.map +1 -1
  74. package/dist/javascript/recipes/change-method-target-to-import.d.ts +29 -0
  75. package/dist/javascript/recipes/change-method-target-to-import.d.ts.map +1 -0
  76. package/dist/javascript/recipes/change-method-target-to-import.js +164 -0
  77. package/dist/javascript/recipes/change-method-target-to-import.js.map +1 -0
  78. package/dist/javascript/recipes/index.d.ts +1 -0
  79. package/dist/javascript/recipes/index.d.ts.map +1 -1
  80. package/dist/javascript/recipes/index.js +1 -0
  81. package/dist/javascript/recipes/index.js.map +1 -1
  82. package/dist/javascript/scope.d.ts +60 -16
  83. package/dist/javascript/scope.d.ts.map +1 -1
  84. package/dist/javascript/scope.js +596 -131
  85. package/dist/javascript/scope.js.map +1 -1
  86. package/dist/javascript/search/uses-method.d.ts +1 -1
  87. package/dist/javascript/search/uses-method.d.ts.map +1 -1
  88. package/dist/javascript/search/uses-method.js +2 -2
  89. package/dist/javascript/search/uses-method.js.map +1 -1
  90. package/dist/javascript/search/uses-type.d.ts +14 -1
  91. package/dist/javascript/search/uses-type.d.ts.map +1 -1
  92. package/dist/javascript/search/uses-type.js +24 -9
  93. package/dist/javascript/search/uses-type.js.map +1 -1
  94. package/dist/javascript/templating/bindings.d.ts +1 -1
  95. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  96. package/dist/javascript/templating/bindings.js +10 -50
  97. package/dist/javascript/templating/bindings.js.map +1 -1
  98. package/dist/javascript/templating/comparator.d.ts +8 -11
  99. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  100. package/dist/javascript/templating/comparator.js +12 -110
  101. package/dist/javascript/templating/comparator.js.map +1 -1
  102. package/dist/javascript/templating/engine.d.ts +34 -24
  103. package/dist/javascript/templating/engine.d.ts.map +1 -1
  104. package/dist/javascript/templating/engine.js +208 -115
  105. package/dist/javascript/templating/engine.js.map +1 -1
  106. package/dist/javascript/templating/pattern.d.ts +11 -5
  107. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  108. package/dist/javascript/templating/pattern.js +86 -20
  109. package/dist/javascript/templating/pattern.js.map +1 -1
  110. package/dist/javascript/templating/placeholder-replacement.d.ts +34 -3
  111. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  112. package/dist/javascript/templating/placeholder-replacement.js +126 -60
  113. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  114. package/dist/javascript/templating/precedence.d.ts +3 -1
  115. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  116. package/dist/javascript/templating/precedence.js +28 -8
  117. package/dist/javascript/templating/precedence.js.map +1 -1
  118. package/dist/javascript/templating/template.d.ts +7 -6
  119. package/dist/javascript/templating/template.d.ts.map +1 -1
  120. package/dist/javascript/templating/template.js +31 -28
  121. package/dist/javascript/templating/template.js.map +1 -1
  122. package/dist/javascript/templating/types.d.ts +6 -3
  123. package/dist/javascript/templating/types.d.ts.map +1 -1
  124. package/dist/javascript/templating/utils.d.ts +10 -1
  125. package/dist/javascript/templating/utils.d.ts.map +1 -1
  126. package/dist/javascript/templating/utils.js +22 -3
  127. package/dist/javascript/templating/utils.js.map +1 -1
  128. package/dist/javascript/tree-debug.d.ts +2 -0
  129. package/dist/javascript/tree-debug.d.ts.map +1 -1
  130. package/dist/javascript/tree-debug.js +6 -0
  131. package/dist/javascript/tree-debug.js.map +1 -1
  132. package/dist/javascript/tsconfig.d.ts.map +1 -1
  133. package/dist/javascript/tsconfig.js +7 -17
  134. package/dist/javascript/tsconfig.js.map +1 -1
  135. package/dist/javascript/type-mapping.d.ts +35 -16
  136. package/dist/javascript/type-mapping.d.ts.map +1 -1
  137. package/dist/javascript/type-mapping.js +271 -252
  138. package/dist/javascript/type-mapping.js.map +1 -1
  139. package/dist/javascript/type-report-cli.d.ts +7 -0
  140. package/dist/javascript/type-report-cli.d.ts.map +1 -0
  141. package/dist/javascript/type-report-cli.js +129 -0
  142. package/dist/javascript/type-report-cli.js.map +1 -0
  143. package/dist/javascript/type-report.d.ts +61 -0
  144. package/dist/javascript/type-report.d.ts.map +1 -0
  145. package/dist/javascript/type-report.js +488 -0
  146. package/dist/javascript/type-report.js.map +1 -0
  147. package/dist/markers.d.ts +4 -0
  148. package/dist/markers.d.ts.map +1 -1
  149. package/dist/markers.js +8 -0
  150. package/dist/markers.js.map +1 -1
  151. package/dist/print.d.ts +6 -1
  152. package/dist/print.d.ts.map +1 -1
  153. package/dist/print.js +21 -7
  154. package/dist/print.js.map +1 -1
  155. package/dist/recipe.d.ts +5 -0
  156. package/dist/recipe.d.ts.map +1 -1
  157. package/dist/recipe.js +5 -0
  158. package/dist/recipe.js.map +1 -1
  159. package/dist/rewrite-javascript-version.txt +1 -1
  160. package/dist/rpc/java-rpc-client.d.ts +2 -0
  161. package/dist/rpc/java-rpc-client.d.ts.map +1 -1
  162. package/dist/rpc/java-rpc-client.js +1 -0
  163. package/dist/rpc/java-rpc-client.js.map +1 -1
  164. package/dist/rpc/queue.d.ts +24 -0
  165. package/dist/rpc/queue.d.ts.map +1 -1
  166. package/dist/rpc/queue.js +71 -11
  167. package/dist/rpc/queue.js.map +1 -1
  168. package/dist/rpc/request/get-object.d.ts +4 -0
  169. package/dist/rpc/request/get-object.d.ts.map +1 -1
  170. package/dist/rpc/request/get-object.js +23 -1
  171. package/dist/rpc/request/get-object.js.map +1 -1
  172. package/dist/rpc/request/parse.d.ts.map +1 -1
  173. package/dist/rpc/request/parse.js +3 -1
  174. package/dist/rpc/request/parse.js.map +1 -1
  175. package/dist/rpc/request/prepare-recipe.d.ts +3 -1
  176. package/dist/rpc/request/prepare-recipe.d.ts.map +1 -1
  177. package/dist/rpc/request/prepare-recipe.js +13 -6
  178. package/dist/rpc/request/prepare-recipe.js.map +1 -1
  179. package/dist/rpc/request/print.d.ts +8 -2
  180. package/dist/rpc/request/print.d.ts.map +1 -1
  181. package/dist/rpc/request/print.js +12 -9
  182. package/dist/rpc/request/print.js.map +1 -1
  183. package/dist/rpc/rewrite-rpc.d.ts +3 -2
  184. package/dist/rpc/rewrite-rpc.d.ts.map +1 -1
  185. package/dist/rpc/rewrite-rpc.js +9 -3
  186. package/dist/rpc/rewrite-rpc.js.map +1 -1
  187. package/dist/rpc/server.d.ts.map +1 -1
  188. package/dist/rpc/server.js +19 -5
  189. package/dist/rpc/server.js.map +1 -1
  190. package/dist/search/is-source-file.d.ts +1 -0
  191. package/dist/search/is-source-file.d.ts.map +1 -1
  192. package/dist/search/is-source-file.js +7 -2
  193. package/dist/search/is-source-file.js.map +1 -1
  194. package/dist/test/java-rpc.d.ts.map +1 -1
  195. package/dist/test/java-rpc.js +3 -0
  196. package/dist/test/java-rpc.js.map +1 -1
  197. package/dist/test/rewrite-test.d.ts +5 -0
  198. package/dist/test/rewrite-test.d.ts.map +1 -1
  199. package/dist/test/rewrite-test.js +14 -3
  200. package/dist/test/rewrite-test.js.map +1 -1
  201. package/package.json +4 -3
  202. package/src/index.ts +3 -1
  203. package/src/java/formatting-utils.ts +5 -4
  204. package/src/java/rpc.ts +2 -0
  205. package/src/java/tree.ts +15 -0
  206. package/src/java/type.ts +19 -2
  207. package/src/javascript/add-import.ts +338 -152
  208. package/src/javascript/amd.ts +28 -12
  209. package/src/javascript/autodetect.ts +32 -41
  210. package/src/javascript/binding.ts +64 -22
  211. package/src/javascript/comparator.ts +129 -43
  212. package/src/javascript/format/format.ts +9 -0
  213. package/src/javascript/format/tabs-and-indents-visitor.ts +30 -3
  214. package/src/javascript/index.ts +6 -3
  215. package/src/javascript/markers.ts +77 -5
  216. package/src/javascript/method-matcher.ts +34 -22
  217. package/src/javascript/node-resolution-result.ts +3 -2
  218. package/src/javascript/package-name.ts +45 -0
  219. package/src/javascript/parser.ts +200 -91
  220. package/src/javascript/preconditions.ts +7 -4
  221. package/src/javascript/print.ts +123 -76
  222. package/src/javascript/recipes/change-method-target-to-import.ts +178 -0
  223. package/src/javascript/recipes/index.ts +1 -0
  224. package/src/javascript/scope.ts +705 -169
  225. package/src/javascript/search/uses-method.ts +2 -2
  226. package/src/javascript/search/uses-type.ts +30 -9
  227. package/src/javascript/templating/bindings.ts +13 -38
  228. package/src/javascript/templating/comparator.ts +16 -127
  229. package/src/javascript/templating/engine.ts +236 -129
  230. package/src/javascript/templating/pattern.ts +107 -28
  231. package/src/javascript/templating/placeholder-replacement.ts +154 -62
  232. package/src/javascript/templating/precedence.ts +33 -11
  233. package/src/javascript/templating/template.ts +35 -30
  234. package/src/javascript/templating/types.ts +6 -3
  235. package/src/javascript/templating/utils.ts +24 -2
  236. package/src/javascript/tree-debug.ts +9 -0
  237. package/src/javascript/tsconfig.ts +7 -17
  238. package/src/javascript/type-mapping.ts +277 -255
  239. package/src/javascript/type-report-cli.ts +96 -0
  240. package/src/javascript/type-report.ts +575 -0
  241. package/src/markers.ts +8 -0
  242. package/src/print.ts +23 -8
  243. package/src/recipe.ts +6 -0
  244. package/src/rpc/java-rpc-client.ts +3 -0
  245. package/src/rpc/queue.ts +80 -7
  246. package/src/rpc/request/get-object.ts +24 -0
  247. package/src/rpc/request/parse.ts +4 -1
  248. package/src/rpc/request/prepare-recipe.ts +16 -6
  249. package/src/rpc/request/print.ts +12 -8
  250. package/src/rpc/rewrite-rpc.ts +16 -5
  251. package/src/rpc/server.ts +18 -6
  252. package/src/search/is-source-file.ts +8 -3
  253. package/src/test/java-rpc.ts +3 -0
  254. package/src/test/rewrite-test.ts +14 -3
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright 2025 the original author or authors.
4
+ * <p>
5
+ * Licensed under the Moderne Source Available License (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ * <p>
9
+ * https://docs.moderne.io/licensing/moderne-source-available-license
10
+ * <p>
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import {Command, CommanderError} from "commander";
20
+ import {JavaScriptParser} from "./parser";
21
+ import {JS} from "./tree";
22
+ import {LstDebugPrinter} from "./tree-debug";
23
+ import {printTypes} from "./type-report";
24
+
25
+ /**
26
+ * `rewrite-javascript-types <file>`: print a file's type attribution. The in-process parse
27
+ * behind it is for diagnostics only.
28
+ */
29
+ export async function main(argv: string[]): Promise<number> {
30
+ const program = new Command()
31
+ .name("rewrite-javascript-types")
32
+ .description("Print a parsed JavaScript/TypeScript LST annotated with its type attribution.")
33
+ .argument("<path>", "the file to parse")
34
+ .option("--project-root <dir>",
35
+ "directory node_modules and tsconfig resolve against (default: the nearest one with a package.json)")
36
+ .option("--only-missing", "list only the nodes whose type is missing")
37
+ .option("--all", "list every node with a type slot, not just calls and declarations")
38
+ .option("--supertypes", "show each declaring type's superclass chain, whose types a matchOverrides pattern may name")
39
+ .option("--tree", "print the nested structure with each node's type instead of the listing")
40
+ .option("--json", "print the listing as JSON")
41
+ .exitOverride();
42
+
43
+ try {
44
+ await program.parseAsync(argv, {from: "user"});
45
+ const opts = program.opts();
46
+ if (opts.tree) {
47
+ // The tree lists every node already, so a flag that narrows or reshapes the
48
+ // listing is a mistake worth naming rather than silently ignoring.
49
+ for (const flag of ["onlyMissing", "all", "supertypes", "json"]) {
50
+ if (opts[flag]) {
51
+ program.error(`--tree does not take --${flag.replace(/[A-Z]/g, c => "-" + c.toLowerCase())}`);
52
+ }
53
+ }
54
+ }
55
+
56
+ const file = path.resolve(program.args[0]);
57
+ const root = path.resolve(opts.projectRoot ?? projectRootOf(file));
58
+ const parsed = (await new JavaScriptParser({relativeTo: root}).parse(file).next()).value;
59
+ if (parsed?.kind !== JS.Kind.CompilationUnit) {
60
+ process.stderr.write(`could not parse ${file}\n`);
61
+ return 1;
62
+ }
63
+
64
+ if (opts.tree) {
65
+ new LstDebugPrinter({includeCursorMessages: false, includeTypes: true}).print(parsed);
66
+ } else {
67
+ await printTypes(parsed, {
68
+ onlyMissing: opts.onlyMissing,
69
+ all: opts.all,
70
+ supertypes: opts.supertypes,
71
+ json: opts.json,
72
+ });
73
+ }
74
+ return 0;
75
+ } catch (e) {
76
+ if (e instanceof CommanderError) {
77
+ return e.exitCode;
78
+ }
79
+ throw e;
80
+ }
81
+ }
82
+
83
+ function projectRootOf(file: string): string {
84
+ for (let dir = path.dirname(file); ; dir = path.dirname(dir)) {
85
+ if (fs.existsSync(path.join(dir, "package.json"))) {
86
+ return dir;
87
+ }
88
+ if (path.dirname(dir) === dir) {
89
+ return path.dirname(file);
90
+ }
91
+ }
92
+ }
93
+
94
+ if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
95
+ main(process.argv.slice(2)).then(code => process.exitCode = code);
96
+ }
@@ -0,0 +1,575 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {Cursor, SourceFile, Tree} from "../tree";
17
+ import {isJava, J, Type} from "../java";
18
+ import {PrintOutputCapture} from "../print";
19
+ import {JS} from "./tree";
20
+ import {JavaScriptPrinter} from "./print";
21
+ import {MethodMatcher} from "./method-matcher";
22
+
23
+ /**
24
+ * Print a parsed LST annotated with its type attribution, answering "what type did this
25
+ * expression get, and if none, where was it lost?". The README section "Inspecting type
26
+ * attribution" describes the output and the `rewrite-javascript-types` command.
27
+ */
28
+
29
+ // A slot left undefined and one holding `Type.unknownType` render differently, since
30
+ // they are different ways for attribution to be absent.
31
+ const NONE = "<none>";
32
+ const UNKNOWN = "<unknown>";
33
+ const NO_SUPERTYPE = "(none recorded)";
34
+
35
+ // Nodes whose type slot a recipe gates on. `all` lists every other node with a type slot too.
36
+ const DEFAULT_KINDS: ReadonlySet<string> = new Set([
37
+ J.Kind.MethodInvocation,
38
+ JS.Kind.FunctionCall,
39
+ J.Kind.NewClass,
40
+ J.Kind.MethodDeclaration,
41
+ J.Kind.ClassDeclaration,
42
+ J.Kind.NamedVariable,
43
+ ]);
44
+
45
+ const CALL_KINDS: ReadonlySet<string> = new Set([J.Kind.MethodInvocation, JS.Kind.FunctionCall, J.Kind.NewClass]);
46
+
47
+ const TYPE_SLOTS = ["type", "methodType", "constructorType", "variableType", "fieldType"];
48
+
49
+ // The matcher reads either spelling of these primitives. A TypeScript author writes this one.
50
+ const TS_SPELLING: Record<string, string> = {double: "number", String: "string"};
51
+
52
+ const MAX_TYPE_DEPTH = 3;
53
+ const EXCERPT_WIDTH = 40;
54
+
55
+ export const DUMP_TYPES_ENV = "REWRITE_JAVASCRIPT_DUMP_TYPES";
56
+
57
+ /** One reported node: where it is, what it reads as, and what type it got. */
58
+ export interface TypeEntry {
59
+ line: number;
60
+ column: number;
61
+ kind: string;
62
+ source: string;
63
+ type: string;
64
+ /** The kind of the rendered type, which decides the check a recipe needs (`Primitive`, `Class`, ...). */
65
+ typeKind: string;
66
+ missing: boolean;
67
+ note?: string;
68
+ cause?: TypeEntry;
69
+ /** A call's arguments, each with the type a pattern capture over it sees. */
70
+ arguments?: TypeEntry[];
71
+ supertypes?: string;
72
+ }
73
+
74
+ /** The source-ordered entries reported for one source file. */
75
+ export interface TypeReport {
76
+ sourcePath: string;
77
+ /** The nodes the listing covers, before `onlyMissing` narrows it. */
78
+ nodeCount: number;
79
+ entries: TypeEntry[];
80
+ }
81
+
82
+ export interface TypeReportOptions {
83
+ /** List only the nodes whose type is missing. */
84
+ onlyMissing?: boolean;
85
+ /** List every node with a type slot, not just calls and declarations. */
86
+ all?: boolean;
87
+ /** Show each declaring type's ancestry. */
88
+ supertypes?: boolean;
89
+ }
90
+
91
+ /**
92
+ * Render a type by name, so the output is stable across runs. Type graphs are cyclic, so
93
+ * nesting stops at a type already being rendered and at a fixed depth.
94
+ */
95
+ export function renderType(type: Type | undefined, depth = 0, seen: ReadonlySet<Type> = new Set()): string {
96
+ if (type === undefined || type === null) {
97
+ return NONE;
98
+ }
99
+ if (type.kind === Type.Kind.Unknown) {
100
+ return UNKNOWN;
101
+ }
102
+ if (Type.isPrimitive(type)) {
103
+ return type.keyword;
104
+ }
105
+ if (depth >= MAX_TYPE_DEPTH || seen.has(type)) {
106
+ return "...";
107
+ }
108
+ const inner = new Set(seen).add(type);
109
+ const nested = (t: Type | undefined) => renderType(t, depth + 1, inner);
110
+
111
+ switch (type.kind) {
112
+ case Type.Kind.Parameterized: {
113
+ const p = type as Type.Parameterized;
114
+ const params = (p.typeParameters ?? []).map(nested).join(", ");
115
+ return params ? `${nested(p.type)}<${params}>` : nested(p.type);
116
+ }
117
+ case Type.Kind.Class:
118
+ case Type.Kind.ShallowClass:
119
+ return (type as Type.Class).fullyQualifiedName || UNKNOWN;
120
+ case Type.Kind.Annotation:
121
+ return `@${nested((type as Type.Annotation).type)}`;
122
+ case Type.Kind.Union:
123
+ case Type.Kind.Intersection: {
124
+ // A union of overloads repeats its bounds, which reads as noise.
125
+ const bounds = [...new Set((type as Type.Union).bounds.map(nested))];
126
+ return bounds.length ? bounds.join(type.kind === Type.Kind.Union ? " | " : " & ") : UNKNOWN;
127
+ }
128
+ case Type.Kind.GenericTypeVariable: {
129
+ const g = type as Type.GenericTypeVariable;
130
+ const bounds = (g.bounds ?? []).map(nested).join(" & ");
131
+ return bounds ? `${g.name} extends ${bounds}` : g.name;
132
+ }
133
+ case Type.Kind.Array:
134
+ return `${nested((type as Type.Array).elemType)}[]`;
135
+ case Type.Kind.Method:
136
+ return renderMethod(type as Type.Method, depth, inner);
137
+ case Type.Kind.Variable: {
138
+ const v = type as Type.Variable;
139
+ return `${nested(v.owner)}#${v.name}: ${nested(v.type)}`;
140
+ }
141
+ default:
142
+ return shortKind(type.kind);
143
+ }
144
+ }
145
+
146
+ /** Render a method type as `<declaring type> <name>(..) -> <return type>`. */
147
+ export function renderMethod(method: Type.Method | undefined, depth = 0, seen: ReadonlySet<Type> = new Set()): string {
148
+ if (method === undefined) {
149
+ return NONE;
150
+ }
151
+ return `${renderType(method.declaringType, depth + 1, seen)} ${method.name}(..) -> ` +
152
+ renderType(method.returnType, depth + 1, seen);
153
+ }
154
+
155
+ /** The rendered type of a node's load-bearing type slot, or `undefined` for a node with none. */
156
+ export function renderNodeType(node: J): string | undefined {
157
+ return hasTypeSlot(node) ? describe(node).type : undefined;
158
+ }
159
+
160
+ export async function buildTypeReport(sourceFile: SourceFile, options: TypeReportOptions = {}): Promise<TypeReport> {
161
+ const {located, printed} = await locate(sourceFile);
162
+ const starts = lineStarts(printed);
163
+
164
+ const built = located.map((item, i): TypeEntry => {
165
+ const content = item.content ?? item.start;
166
+ const [line, column] = lineColumn(starts, content);
167
+ const body = descendant(located, i, bodyOf(item.node));
168
+ const stop = body === undefined ? item.end : located[body].start;
169
+ return {
170
+ line,
171
+ column,
172
+ kind: nodeKind(item.node.kind),
173
+ source: excerpt(printed.substring(content, stop)),
174
+ ...describe(item.node),
175
+ supertypes: options.supertypes ? supertypeChain(item.node) : undefined,
176
+ };
177
+ });
178
+
179
+ // A call's name carries no type of its own. Its method type is the call's row.
180
+ const callNames = new Set<J>(located
181
+ .filter(item => item.node.kind === J.Kind.MethodInvocation)
182
+ .map(item => (item.node as J.MethodInvocation).name));
183
+
184
+ const entries: TypeEntry[] = [];
185
+ let nodeCount = 0;
186
+ located.forEach((item, i) => {
187
+ if (options.all ? !hasTypeSlot(item.node) || callNames.has(item.node) : !DEFAULT_KINDS.has(item.node.kind)) {
188
+ return;
189
+ }
190
+ nodeCount++;
191
+ let entry = built[i];
192
+ const args = argumentsOf(item.node)
193
+ .map(arg => descendant(located, i, arg))
194
+ .filter((index): index is number => index !== undefined)
195
+ .map(index => built[index]);
196
+ if (args.length > 0) {
197
+ entry = {...entry, arguments: args};
198
+ }
199
+ if (entry.missing) {
200
+ const cause = descendant(located, i, receiverOf(item.node));
201
+ if (cause !== undefined) {
202
+ entry = {...entry, cause: built[cause]};
203
+ }
204
+ }
205
+ if (!options.onlyMissing || entry.missing) {
206
+ entries.push(entry);
207
+ }
208
+ });
209
+ return {sourcePath: sourceFile.sourcePath, nodeCount, entries};
210
+ }
211
+
212
+ /** Format a report as the table {@link printTypes} writes. */
213
+ export function formatTypeReport(report: TypeReport): string {
214
+ const rows: string[][] = [];
215
+ for (const entry of report.entries) {
216
+ let type = entry.missing ? `⚠ ${entry.type}` : entry.type;
217
+ if (entry.note) {
218
+ type += ` [⚠ ${entry.note}]`;
219
+ }
220
+ rows.push([`${entry.line}:${entry.column}`, entry.kind, entry.source, type]);
221
+ if (entry.supertypes) {
222
+ rows.push([`${entry.line}:${entry.column}`, " └ supertypes", "", entry.supertypes]);
223
+ }
224
+ if (entry.cause) {
225
+ const cause = entry.cause;
226
+ rows.push([`${cause.line}:${cause.column}`, ` └ select:${cause.kind}`, cause.source, cause.type]);
227
+ }
228
+ entry.arguments?.forEach((arg, n) => rows.push([
229
+ `${arg.line}:${arg.column}`, ` └ arg${n}:${arg.kind}`, arg.source, `${arg.type} (${arg.typeKind})`
230
+ ]));
231
+ }
232
+ return table([["line:col", "kind", "source", "type"], ...rows]);
233
+ }
234
+
235
+ /** The report as JSON, for a test or CI check that asserts a fixture gained attribution. */
236
+ export function typeReportToJson(report: TypeReport): string {
237
+ return JSON.stringify({
238
+ sourcePath: report.sourcePath,
239
+ nodeCount: report.nodeCount,
240
+ missingCount: report.entries.filter(e => e.missing).length,
241
+ entries: report.entries,
242
+ }, null, 2) + "\n";
243
+ }
244
+
245
+ /** Print a source-ordered listing of the LST's type attribution. */
246
+ export async function printTypes(
247
+ sourceFile: SourceFile,
248
+ options: TypeReportOptions & { json?: boolean, out?: (text: string) => void } = {}
249
+ ): Promise<TypeReport> {
250
+ const out = options.out ?? (text => process.stdout.write(text));
251
+ const report = await buildTypeReport(sourceFile, options);
252
+ out(options.json ? typeReportToJson(report) : formatTypeReport(report));
253
+ return report;
254
+ }
255
+
256
+ /**
257
+ * Print the report when {@link DUMP_TYPES_ENV} asks for it. Runs on every test parse, so it
258
+ * stays silent unless asked and reports its own failure rather than failing the test.
259
+ */
260
+ export async function dumpTypesIfRequested(sourceFile: SourceFile): Promise<boolean> {
261
+ if (sourceFile.kind !== JS.Kind.CompilationUnit) {
262
+ return false;
263
+ }
264
+ const flags = new Set((process.env[DUMP_TYPES_ENV] ?? "")
265
+ .split(",").map(f => f.trim().toLowerCase()).filter(f => f));
266
+ if (flags.size === 0 || [...flags].every(f => ["0", "false", "off", "no"].includes(f))) {
267
+ return false;
268
+ }
269
+ const header = `\n--- type attribution: ${sourceFile.sourcePath} ---\n`;
270
+ try {
271
+ const report = await buildTypeReport(sourceFile, {
272
+ onlyMissing: flags.has("missing"),
273
+ all: flags.has("all"),
274
+ supertypes: flags.has("supertypes"),
275
+ });
276
+ console.log(header + formatTypeReport(report));
277
+ } catch (e) {
278
+ console.log(`${header}(could not report type attribution: ${e})`);
279
+ }
280
+ return true;
281
+ }
282
+
283
+ /** The unattributed rows, for appending to a failing assertion. */
284
+ export async function attributionHint(sourceFile: SourceFile, limit = 12): Promise<string> {
285
+ if (sourceFile.kind !== JS.Kind.CompilationUnit) {
286
+ return "";
287
+ }
288
+ let missing: TypeEntry[];
289
+ try {
290
+ missing = (await buildTypeReport(sourceFile, {onlyMissing: true})).entries;
291
+ } catch {
292
+ return "";
293
+ }
294
+ if (missing.length === 0) {
295
+ return "";
296
+ }
297
+ const shown = missing.slice(0, limit).map(e => ({...e, arguments: undefined}));
298
+ const lines = formatTypeReport({sourcePath: sourceFile.sourcePath, nodeCount: shown.length, entries: shown})
299
+ .trimEnd().split("\n").map(line => ` ${line}`).join("\n");
300
+ const more = missing.length > limit ? `\n ... and ${missing.length - limit} more` : "";
301
+ return `\n\nNodes with no type attribution (a recipe gated on one of these cannot fire):\n${lines}${more}`;
302
+ }
303
+
304
+ interface Located {
305
+ node: J;
306
+ depth: number;
307
+ /** Offset of the node's prefix. */
308
+ start: number;
309
+ /** Offset of its first source character. */
310
+ content?: number;
311
+ end: number;
312
+ }
313
+
314
+ /**
315
+ * Prints the tree, recording each node's source offsets, since LST nodes carry no positions.
316
+ * A node's text starts where the `beforeSyntax` printing its prefix ends, or after its first
317
+ * `visitSpace` when it has none. A node printed inline without a visit, such as a
318
+ * `NamedVariable`, opens on `beforeSyntax` and closes on `afterSyntax`.
319
+ */
320
+ class LocatingPrinter extends JavaScriptPrinter {
321
+ readonly located: Located[] = [];
322
+ private readonly open: { entry: Located, inline: boolean }[] = [];
323
+ private pending?: Located;
324
+
325
+ override async visit<R extends J>(tree: Tree, p: PrintOutputCapture, parent?: Cursor): Promise<R | undefined> {
326
+ if (!isJava(tree)) {
327
+ return super.visit(tree, p, parent);
328
+ }
329
+ const depth = this.open.length;
330
+ this.enter(tree, p, false);
331
+ try {
332
+ return await super.visit(tree, p, parent);
333
+ } finally {
334
+ while (this.open.length > depth) {
335
+ this.exit(p);
336
+ }
337
+ }
338
+ }
339
+
340
+ protected override async beforeSyntax(j: J, p: PrintOutputCapture): Promise<void> {
341
+ if (this.open[this.open.length - 1]?.entry.node !== j) {
342
+ this.enter(j, p, true);
343
+ }
344
+ const {entry} = this.open[this.open.length - 1];
345
+ await super.beforeSyntax(j, p);
346
+ entry.content = p.out.length;
347
+ if (this.pending === entry) {
348
+ this.pending = undefined;
349
+ }
350
+ }
351
+
352
+ protected override async afterSyntax(j: J, p: PrintOutputCapture): Promise<void> {
353
+ await super.afterSyntax(j, p);
354
+ const top = this.open[this.open.length - 1];
355
+ if (top?.inline && top.entry.node === j) {
356
+ this.exit(p);
357
+ }
358
+ }
359
+
360
+ override async visitSpace(space: J.Space | undefined, p: PrintOutputCapture): Promise<J.Space> {
361
+ const result = await super.visitSpace(space, p);
362
+ this.prefixPrinted(p);
363
+ return result;
364
+ }
365
+
366
+ private enter(node: J, p: PrintOutputCapture, inline: boolean): void {
367
+ this.prefixPrinted(p, true);
368
+ const entry: Located = {node, depth: this.open.length, start: p.out.length, end: 0};
369
+ this.located.push(entry);
370
+ this.open.push({entry, inline});
371
+ this.pending = entry;
372
+ }
373
+
374
+ private exit(p: PrintOutputCapture): void {
375
+ this.prefixPrinted(p, true);
376
+ this.open.pop()!.entry.end = p.out.length;
377
+ }
378
+
379
+ private prefixPrinted(p: PrintOutputCapture, fallback = false): void {
380
+ if (this.pending) {
381
+ this.pending.content = fallback ? this.pending.start : p.out.length;
382
+ this.pending = undefined;
383
+ }
384
+ }
385
+ }
386
+
387
+ async function locate(sourceFile: SourceFile): Promise<{ located: Located[], printed: string }> {
388
+ const printer = new LocatingPrinter();
389
+ const capture = new PrintOutputCapture();
390
+ await printer.visit(sourceFile, capture);
391
+ return {located: printer.located, printed: capture.out};
392
+ }
393
+
394
+ // `type` also names fields that hold no Type, so a slot counts only when it is empty or typed.
395
+ function hasTypeSlot(node: J): boolean {
396
+ return TYPE_SLOTS.some(slot => slot in node &&
397
+ ((node as any)[slot] === undefined || Type.isType((node as any)[slot])));
398
+ }
399
+
400
+ function methodTypeOf(node: J): Type.Method | undefined {
401
+ return node.kind === J.Kind.NewClass ? (node as J.NewClass).constructorType : (node as any).methodType;
402
+ }
403
+
404
+ function describe(node: J): { type: string, typeKind: string, missing: boolean, note?: string } {
405
+ if ("methodType" in node || "constructorType" in node) {
406
+ const method = methodTypeOf(node);
407
+ if (method === undefined) {
408
+ return {type: NONE, typeKind: NONE, missing: true};
409
+ }
410
+ // The declaring type is what MethodMatcher gates on, so it alone decides whether the
411
+ // row is missing. An unresolved return type shows in the text.
412
+ const declaring = renderType(method.declaringType);
413
+ const missing = isMissing(declaring);
414
+ const {pattern, note} = missing ? {pattern: `${declaring} ${method.name}(..)`, note: undefined} :
415
+ matcherPattern(method, declaring);
416
+ return {type: `${pattern} -> ${renderType(method.returnType)}`, typeKind: typeKindOf(method), missing, note};
417
+ }
418
+ let type: Type | undefined;
419
+ if (node.kind === J.Kind.NamedVariable) {
420
+ type = (node as J.VariableDeclarations.NamedVariable).variableType?.type;
421
+ } else if (node.kind === J.Kind.Identifier && (node as J.Identifier).fieldType) {
422
+ type = (node as J.Identifier).fieldType!.type;
423
+ } else if (Type.isType((node as any).type)) {
424
+ type = (node as any).type;
425
+ }
426
+ const rendered = renderType(type);
427
+ return {type: rendered, typeKind: typeKindOf(type), missing: isMissing(rendered)};
428
+ }
429
+
430
+ function isMissing(rendered: string): boolean {
431
+ return rendered === NONE || rendered === UNKNOWN;
432
+ }
433
+
434
+ function typeKindOf(type: Type | undefined): string {
435
+ if (type === undefined || type === null) {
436
+ return NONE;
437
+ }
438
+ return type.kind === Type.Kind.Union ? "Union" : shortKind(type.kind);
439
+ }
440
+
441
+ /**
442
+ * The most specific {@link MethodMatcher} pattern that matches `method`. It names the parameter
443
+ * types as the matcher does, or uses `(..)` when one has no such name. Each candidate is checked
444
+ * against the matcher, and a note says when neither matches.
445
+ */
446
+ function matcherPattern(method: Type.Method, declaring: string): { pattern: string, note?: string } {
447
+ const params = method.parameterTypes.map(MethodMatcher.typeName).map(name => TS_SPELLING[name] ?? name);
448
+ const candidates = [`${declaring} ${method.name}(..)`];
449
+ if (!params.includes("unknown")) {
450
+ candidates.unshift(`${declaring} ${method.name}(${params.join(", ")})`);
451
+ }
452
+ let note = "pattern does not match this call";
453
+ for (const candidate of candidates) {
454
+ try {
455
+ if (new MethodMatcher(candidate).matches(method)) {
456
+ return {pattern: candidate};
457
+ }
458
+ } catch (e) {
459
+ note = `pattern unparseable: ${e instanceof Error ? e.message : e}`;
460
+ }
461
+ }
462
+ return {pattern: candidates[candidates.length - 1], note};
463
+ }
464
+
465
+ /**
466
+ * The declaring type's superclass chain. With `matchOverrides` a pattern may name any type in it,
467
+ * or an interface one of them records, which the chain does not show.
468
+ */
469
+ function supertypeChain(node: J): string | undefined {
470
+ let current: Type | undefined = methodTypeOf(node)?.declaringType;
471
+ if (current === undefined || current.kind === Type.Kind.Unknown) {
472
+ return undefined;
473
+ }
474
+ const chain: string[] = [];
475
+ const seen = new Set<Type>();
476
+ while (current && !seen.has(current)) {
477
+ seen.add(current);
478
+ chain.push(renderType(current));
479
+ const cls: Type | undefined = Type.isParameterized(current) ? current.type : current;
480
+ current = (cls as Type.Class | undefined)?.supertype;
481
+ }
482
+ if (chain.length === 1) {
483
+ chain.push(NO_SUPERTYPE);
484
+ }
485
+ return chain.join(" <: ");
486
+ }
487
+
488
+ function argumentsOf(node: J): J[] {
489
+ const args = (node as J.MethodInvocation | J.NewClass | JS.FunctionCall).arguments;
490
+ return CALL_KINDS.has(node.kind) && args ?
491
+ args.elements.map(arg => arg.element).filter(arg => arg.kind !== J.Kind.Empty) : [];
492
+ }
493
+
494
+ function receiverOf(node: J): J | undefined {
495
+ if (node.kind === J.Kind.MethodInvocation) {
496
+ return (node as J.MethodInvocation).select?.element;
497
+ }
498
+ if (node.kind === JS.Kind.FunctionCall) {
499
+ return (node as JS.FunctionCall).function?.element;
500
+ }
501
+ return undefined;
502
+ }
503
+
504
+ /** The block a declaration owns, so its excerpt can stop at the signature. */
505
+ function bodyOf(node: J): J | undefined {
506
+ if (node.kind === J.Kind.MethodDeclaration) {
507
+ return (node as J.MethodDeclaration).body;
508
+ }
509
+ if (node.kind === J.Kind.ClassDeclaration) {
510
+ return (node as J.ClassDeclaration).body;
511
+ }
512
+ return undefined;
513
+ }
514
+
515
+ /**
516
+ * Index of `child` within the node at `parent`. A recipe can leave the same node object in two
517
+ * slots, so a child is identified by where this traversal reached it.
518
+ */
519
+ function descendant(located: Located[], parent: number, child: J | undefined): number | undefined {
520
+ if (child === undefined) {
521
+ return undefined;
522
+ }
523
+ for (let i = parent + 1; i < located.length && located[i].start < located[parent].end; i++) {
524
+ if (located[i].node === child) {
525
+ return i;
526
+ }
527
+ }
528
+ return undefined;
529
+ }
530
+
531
+ function lineStarts(text: string): number[] {
532
+ const starts = [0];
533
+ for (let i = 0; i < text.length; i++) {
534
+ if (text[i] === "\n") {
535
+ starts.push(i + 1);
536
+ }
537
+ }
538
+ return starts;
539
+ }
540
+
541
+ function lineColumn(starts: number[], offset: number): [number, number] {
542
+ let lo = 0, hi = starts.length - 1;
543
+ while (lo < hi) {
544
+ const mid = (lo + hi + 1) >> 1;
545
+ if (starts[mid] <= offset) {
546
+ lo = mid;
547
+ } else {
548
+ hi = mid - 1;
549
+ }
550
+ }
551
+ return [lo + 1, offset - starts[lo] + 1];
552
+ }
553
+
554
+ function excerpt(text: string, width = EXCERPT_WIDTH): string {
555
+ const collapsed = text.split(/\s+/).filter(s => s).join(" ");
556
+ return collapsed.length <= width ? collapsed : collapsed.substring(0, width - 3) + "...";
557
+ }
558
+
559
+ // `JS.Binary` and `J.Binary` are visited by different methods, so a JavaScript-only kind keeps its namespace.
560
+ function nodeKind(kind: string): string {
561
+ const short = shortKind(kind);
562
+ return kind.startsWith("org.openrewrite.javascript.") ?
563
+ `${kind.substring(kind.lastIndexOf(".") + 1, kind.indexOf("$"))}.${short}` : short;
564
+ }
565
+
566
+ function shortKind(kind: string): string {
567
+ return kind.substring(kind.lastIndexOf("$") + 1);
568
+ }
569
+
570
+ function table(rows: string[][]): string {
571
+ const widths = [0, 1, 2].map(i => Math.max(...rows.map(r => r[i].length)));
572
+ return rows.map(r =>
573
+ (r.slice(0, 3).map((cell, i) => cell.padEnd(widths[i])).join(" ") + " " + r[3]).trimEnd()
574
+ ).join("\n") + "\n";
575
+ }
package/src/markers.ts CHANGED
@@ -128,6 +128,14 @@ export function replaceMarkerByKind(markers: Markers, newMarker: Marker): Marker
128
128
  };
129
129
  }
130
130
 
131
+ /**
132
+ * Removes every marker of the given kind, returning `markers` itself when none has that kind.
133
+ */
134
+ export function removeMarkerByKind(markers: Markers, kind: Marker["kind"]): Markers {
135
+ const kept = markers.markers.filter(m => m.kind !== kind);
136
+ return kept.length === markers.markers.length ? markers : {...markers, markers: kept};
137
+ }
138
+
131
139
  export const emptyMarkers: Markers = asRef({
132
140
  kind: MarkersKind.Markers,
133
141
  id: randomId(),