@openrewrite/rewrite 8.69.0-20251207-160255 → 8.69.0-20251207-214914
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/dist/cli/cli-utils.d.ts.map +1 -1
- package/dist/cli/cli-utils.js +3 -2
- package/dist/cli/cli-utils.js.map +1 -1
- package/dist/cli/rewrite.js +2 -1
- package/dist/cli/rewrite.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +99 -56
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +48 -8
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/async-callback-in-sync-array-method.d.ts +40 -0
- package/dist/javascript/recipes/async-callback-in-sync-array-method.d.ts.map +1 -0
- package/dist/javascript/recipes/async-callback-in-sync-array-method.js +232 -0
- package/dist/javascript/recipes/async-callback-in-sync-array-method.js.map +1 -0
- package/dist/javascript/recipes/change-import.d.ts +51 -0
- package/dist/javascript/recipes/change-import.d.ts.map +1 -0
- package/dist/javascript/recipes/change-import.js +658 -0
- package/dist/javascript/recipes/change-import.js.map +1 -0
- package/dist/javascript/recipes/index.d.ts +3 -0
- package/dist/javascript/recipes/index.d.ts.map +1 -1
- package/dist/javascript/recipes/index.js +3 -0
- package/dist/javascript/recipes/index.js.map +1 -1
- package/dist/javascript/recipes/order-imports.d.ts +10 -0
- package/dist/javascript/recipes/order-imports.d.ts.map +1 -0
- package/dist/javascript/recipes/order-imports.js +240 -0
- package/dist/javascript/recipes/order-imports.js.map +1 -0
- package/dist/javascript/templating/index.d.ts +1 -1
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js +2 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts +36 -2
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +76 -0
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/json/parser.js +78 -30
- package/dist/json/parser.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +7 -4
- package/dist/run.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/cli/cli-utils.ts +3 -2
- package/src/cli/rewrite.ts +2 -1
- package/src/index.ts +3 -2
- package/src/javascript/add-import.ts +125 -69
- package/src/javascript/parser.ts +49 -8
- package/src/javascript/recipes/async-callback-in-sync-array-method.ts +237 -0
- package/src/javascript/recipes/change-import.ts +700 -0
- package/src/javascript/recipes/index.ts +3 -0
- package/src/javascript/recipes/order-imports.ts +242 -0
- package/src/javascript/templating/index.ts +2 -1
- package/src/javascript/templating/rewrite.ts +89 -4
- package/src/json/parser.ts +69 -24
- package/src/run.ts +5 -4
- package/dist/recipe/index.d.ts +0 -2
- package/dist/recipe/index.d.ts.map +0 -1
- package/dist/recipe/index.js +0 -21
- package/dist/recipe/index.js.map +0 -1
- package/dist/recipe/order-imports.d.ts +0 -10
- package/dist/recipe/order-imports.d.ts.map +0 -1
- package/dist/recipe/order-imports.js +0 -213
- package/dist/recipe/order-imports.js.map +0 -1
- package/src/recipe/index.ts +0 -17
- package/src/recipe/order-imports.ts +0 -195
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
|
|
17
|
+
import {Recipe} from "../../recipe";
|
|
18
|
+
import {ExecutionContext} from "../../execution";
|
|
19
|
+
import {TreeVisitor} from "../../visitor";
|
|
20
|
+
import {JavaScriptVisitor} from "../visitor";
|
|
21
|
+
import {J} from "../../java";
|
|
22
|
+
import {JS} from "../tree";
|
|
23
|
+
import {Type} from "../../java";
|
|
24
|
+
import {markupWarn} from "../../markers";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Array methods that don't await async callbacks.
|
|
28
|
+
* When an async function is passed as a callback to these methods,
|
|
29
|
+
* the returned Promise is not awaited, leading to bugs.
|
|
30
|
+
*/
|
|
31
|
+
const SYNC_ARRAY_METHODS = new Set([
|
|
32
|
+
'some', // Returns first truthy value, but Promise is always truthy
|
|
33
|
+
'every', // Returns first falsy value, but Promise is always truthy
|
|
34
|
+
'find', // Returns first truthy value, but Promise is always truthy
|
|
35
|
+
'findIndex', // Returns first truthy value, but Promise is always truthy
|
|
36
|
+
'filter', // Filters based on truthy values, but Promise is always truthy
|
|
37
|
+
'forEach', // Ignores return values entirely, async callbacks won't be awaited
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a type is a Promise type.
|
|
42
|
+
* Looks for types like Promise<T>, PromiseLike<T>, or the Promise class itself.
|
|
43
|
+
*/
|
|
44
|
+
function isPromiseType(type?: Type): boolean {
|
|
45
|
+
if (!type) return false;
|
|
46
|
+
|
|
47
|
+
// Check for Class type with Promise name
|
|
48
|
+
if (Type.isClass(type)) {
|
|
49
|
+
const fqn = type.fullyQualifiedName;
|
|
50
|
+
return fqn === 'Promise' ||
|
|
51
|
+
fqn === 'PromiseLike' ||
|
|
52
|
+
fqn.endsWith('.Promise') ||
|
|
53
|
+
fqn.endsWith('.PromiseLike');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for Parameterized type (e.g., Promise<boolean>)
|
|
57
|
+
if (Type.isParameterized(type)) {
|
|
58
|
+
return isPromiseType(type.type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for Union type (e.g., Promise<T> | undefined)
|
|
62
|
+
if (Type.isUnion(type)) {
|
|
63
|
+
return type.bounds.some(b => isPromiseType(b));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if an arrow function has an async modifier.
|
|
71
|
+
* In JavaScript, async is represented as a LanguageExtension modifier with keyword="async"
|
|
72
|
+
*/
|
|
73
|
+
function hasAsyncModifier(arrowFunc: JS.ArrowFunction): boolean {
|
|
74
|
+
return arrowFunc.modifiers.some(m =>
|
|
75
|
+
m.type === J.ModifierType.Async ||
|
|
76
|
+
(m.type === J.ModifierType.LanguageExtension && m.keyword === 'async')
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if a function type returns a Promise.
|
|
82
|
+
*/
|
|
83
|
+
function functionTypeReturnsPromise(funcType: Type): boolean {
|
|
84
|
+
if (!Type.isFunctionType(funcType)) return false;
|
|
85
|
+
|
|
86
|
+
const clazz = funcType as Type.Class;
|
|
87
|
+
if (clazz.typeParameters && clazz.typeParameters.length > 0) {
|
|
88
|
+
// First type parameter is typically R (return type) in TypeScript function types
|
|
89
|
+
// It's a GenericTypeVariable with bounds containing the actual type
|
|
90
|
+
const returnTypeParam = clazz.typeParameters[0];
|
|
91
|
+
|
|
92
|
+
// Check if it's directly a Promise type
|
|
93
|
+
if (isPromiseType(returnTypeParam)) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if it's a GenericTypeVariable with bounds
|
|
98
|
+
if (Type.isGenericTypeVariable(returnTypeParam)) {
|
|
99
|
+
const bounds = returnTypeParam.bounds;
|
|
100
|
+
if (bounds && bounds.some(b => isPromiseType(b))) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a callback argument returns a Promise.
|
|
110
|
+
* This checks:
|
|
111
|
+
* 1. Explicit async modifier on arrow functions
|
|
112
|
+
* 2. Type annotation indicating Promise return type
|
|
113
|
+
* 3. Function references whose type indicates Promise return
|
|
114
|
+
*/
|
|
115
|
+
function callbackReturnsPromise(arg: any): boolean {
|
|
116
|
+
// Handle RightPadded wrapper
|
|
117
|
+
const element = arg?.element ?? arg;
|
|
118
|
+
|
|
119
|
+
if (!element) return false;
|
|
120
|
+
|
|
121
|
+
// Check if it's an arrow function
|
|
122
|
+
if (element.kind === JS.Kind.ArrowFunction) {
|
|
123
|
+
const arrowFunc = element as JS.ArrowFunction;
|
|
124
|
+
|
|
125
|
+
// Check for async modifier
|
|
126
|
+
if (hasAsyncModifier(arrowFunc)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check if return type expression indicates Promise
|
|
131
|
+
const returnType = arrowFunc.returnTypeExpression;
|
|
132
|
+
if (returnType) {
|
|
133
|
+
// Check if the return type expression is an identifier named Promise
|
|
134
|
+
if (returnType.kind === J.Kind.Identifier) {
|
|
135
|
+
const id = returnType as J.Identifier;
|
|
136
|
+
if (id.simpleName === 'Promise' || id.simpleName === 'PromiseLike') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check if it's a parameterized type like Promise<boolean>
|
|
141
|
+
if (returnType.kind === J.Kind.ParameterizedType) {
|
|
142
|
+
const pt = returnType as J.ParameterizedType;
|
|
143
|
+
// ParameterizedType has 'clazz' property which is the base type
|
|
144
|
+
const baseType = (pt as any).clazz;
|
|
145
|
+
if (baseType?.kind === J.Kind.Identifier) {
|
|
146
|
+
const clazz = baseType as J.Identifier;
|
|
147
|
+
if (clazz.simpleName === 'Promise' || clazz.simpleName === 'PromiseLike') {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check type attribution if available
|
|
155
|
+
const funcType = (arrowFunc as any).type;
|
|
156
|
+
if (funcType && functionTypeReturnsPromise(funcType)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if it's a function reference (Identifier) with type attribution
|
|
162
|
+
if (element.kind === J.Kind.Identifier) {
|
|
163
|
+
const identifier = element as J.Identifier;
|
|
164
|
+
const funcType = identifier.type;
|
|
165
|
+
if (funcType && functionTypeReturnsPromise(funcType)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Detects async callbacks passed to synchronous array methods.
|
|
175
|
+
*
|
|
176
|
+
* This is a common bug pattern in JavaScript/TypeScript where async functions
|
|
177
|
+
* are passed to array methods like `.some()`, `.every()`, `.find()`, etc.
|
|
178
|
+
* These methods don't await the promises returned by async callbacks, leading
|
|
179
|
+
* to bugs where Promise objects are treated as truthy values.
|
|
180
|
+
*
|
|
181
|
+
* Example of buggy code:
|
|
182
|
+
* ```typescript
|
|
183
|
+
* // BUG: .some() doesn't await, so any Promise is truthy
|
|
184
|
+
* const hasAdmin = users.some(async user => {
|
|
185
|
+
* return await checkPermission(user, 'admin');
|
|
186
|
+
* });
|
|
187
|
+
* // hasAdmin is ALWAYS true because Promise objects are truthy!
|
|
188
|
+
*
|
|
189
|
+
* // CORRECT: Use a for loop with await
|
|
190
|
+
* let hasAdmin = false;
|
|
191
|
+
* for (const user of users) {
|
|
192
|
+
* if (await checkPermission(user, 'admin')) {
|
|
193
|
+
* hasAdmin = true;
|
|
194
|
+
* break;
|
|
195
|
+
* }
|
|
196
|
+
* }
|
|
197
|
+
* ```
|
|
198
|
+
*
|
|
199
|
+
* This recipe reports occurrences but doesn't auto-fix because the correct
|
|
200
|
+
* fix depends on the context (could use for...of loop, Promise.all, etc.).
|
|
201
|
+
*/
|
|
202
|
+
export class AsyncCallbackInSyncArrayMethod extends Recipe {
|
|
203
|
+
readonly name = "org.openrewrite.javascript.cleanup.async-callback-in-sync-array-method";
|
|
204
|
+
readonly displayName: string = "Detect async callbacks in synchronous array methods";
|
|
205
|
+
readonly description: string = "Detects async callbacks passed to array methods like .some(), .every(), .filter() which don't await promises. This is a common bug where Promise objects are always truthy.";
|
|
206
|
+
readonly tags = ["javascript", "typescript", "async", "bug", "cleanup"];
|
|
207
|
+
|
|
208
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
209
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
210
|
+
override async visitMethodInvocation(method: J.MethodInvocation, ctx: ExecutionContext): Promise<J | undefined> {
|
|
211
|
+
let m = await super.visitMethodInvocation(method, ctx) as J.MethodInvocation;
|
|
212
|
+
|
|
213
|
+
const methodName = m.name?.simpleName;
|
|
214
|
+
if (!methodName || !SYNC_ARRAY_METHODS.has(methodName)) {
|
|
215
|
+
return m;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check the arguments for async callbacks
|
|
219
|
+
const args = m.arguments?.elements;
|
|
220
|
+
if (!args || args.length === 0) {
|
|
221
|
+
return m;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const firstArg = args[0];
|
|
225
|
+
if (callbackReturnsPromise(firstArg)) {
|
|
226
|
+
return markupWarn(
|
|
227
|
+
m,
|
|
228
|
+
`Async callback passed to .${methodName}()`,
|
|
229
|
+
`Array methods like .${methodName}() don't await async callbacks, so Promises are treated as truthy values. Consider using a for...of loop with await instead.`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return m;
|
|
234
|
+
}
|
|
235
|
+
}();
|
|
236
|
+
}
|
|
237
|
+
}
|