@openrewrite/recipes-code-quality 0.1.0-20260409-154017
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/all-branches-identical.d.ts +10 -0
- package/dist/all-branches-identical.d.ts.map +1 -0
- package/dist/all-branches-identical.js +116 -0
- package/dist/all-branches-identical.js.map +1 -0
- package/dist/boolean-checks-not-inverted.d.ts +10 -0
- package/dist/boolean-checks-not-inverted.d.ts.map +1 -0
- package/dist/boolean-checks-not-inverted.js +117 -0
- package/dist/boolean-checks-not-inverted.js.map +1 -0
- package/dist/collapsible-if-statements.d.ts +10 -0
- package/dist/collapsible-if-statements.d.ts.map +1 -0
- package/dist/collapsible-if-statements.js +119 -0
- package/dist/collapsible-if-statements.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/merge-identical-branches.d.ts +10 -0
- package/dist/merge-identical-branches.d.ts.map +1 -0
- package/dist/merge-identical-branches.js +118 -0
- package/dist/merge-identical-branches.js.map +1 -0
- package/dist/remove-duplicate-conditions.d.ts +10 -0
- package/dist/remove-duplicate-conditions.d.ts.map +1 -0
- package/dist/remove-duplicate-conditions.js +141 -0
- package/dist/remove-duplicate-conditions.js.map +1 -0
- package/dist/remove-self-assignment.d.ts +10 -0
- package/dist/remove-self-assignment.d.ts.map +1 -0
- package/dist/remove-self-assignment.js +86 -0
- package/dist/remove-self-assignment.js.map +1 -0
- package/dist/remove-unconditional-value-overwrite.d.ts +10 -0
- package/dist/remove-unconditional-value-overwrite.d.ts.map +1 -0
- package/dist/remove-unconditional-value-overwrite.js +112 -0
- package/dist/remove-unconditional-value-overwrite.js.map +1 -0
- package/dist/simplify-boolean-literal.d.ts +9 -0
- package/dist/simplify-boolean-literal.d.ts.map +1 -0
- package/dist/simplify-boolean-literal.js +179 -0
- package/dist/simplify-boolean-literal.js.map +1 -0
- package/dist/simplify-redundant-logical-expression.d.ts +10 -0
- package/dist/simplify-redundant-logical-expression.d.ts.map +1 -0
- package/dist/simplify-redundant-logical-expression.js +63 -0
- package/dist/simplify-redundant-logical-expression.js.map +1 -0
- package/package.json +39 -0
- package/src/all-branches-identical.ts +133 -0
- package/src/boolean-checks-not-inverted.ts +144 -0
- package/src/collapsible-if-statements.ts +162 -0
- package/src/index.ts +34 -0
- package/src/merge-identical-branches.ts +149 -0
- package/src/remove-duplicate-conditions.ts +165 -0
- package/src/remove-self-assignment.ts +98 -0
- package/src/remove-unconditional-value-overwrite.ts +128 -0
- package/src/simplify-boolean-literal.ts +220 -0
- package/src/simplify-redundant-logical-expression.ts +75 -0
|
@@ -0,0 +1,149 @@
|
|
|
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 {ExecutionContext, printer, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
17
|
+
import {JavaScriptVisitor, JS, template} from "@openrewrite/rewrite/javascript";
|
|
18
|
+
import {emptySpace, Expression, J, Statement} from "@openrewrite/rewrite/java";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Merges consecutive if/else-if branches that have identical bodies.
|
|
22
|
+
*
|
|
23
|
+
* When two adjacent branches execute the same code, their conditions
|
|
24
|
+
* can be joined with `||` and the duplicate body removed. This eliminates
|
|
25
|
+
* copy-paste redundancy and makes the relationship between the conditions
|
|
26
|
+
* explicit.
|
|
27
|
+
*/
|
|
28
|
+
export class MergeIdenticalBranches extends Recipe {
|
|
29
|
+
name = "org.openrewrite.javascript.cleanup.MergeIdenticalBranches";
|
|
30
|
+
displayName = "Merge consecutive branches with identical bodies";
|
|
31
|
+
description = "Combine consecutive `if`/`else if` branches that have the same body " +
|
|
32
|
+
"into a single branch with conditions joined by `||`.";
|
|
33
|
+
tags = ["RSPEC-S1871"];
|
|
34
|
+
estimatedEffortPerOccurrence = 10;
|
|
35
|
+
|
|
36
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
37
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
38
|
+
override async visitIf(
|
|
39
|
+
ifStmt: J.If,
|
|
40
|
+
ctx: ExecutionContext
|
|
41
|
+
): Promise<J | undefined> {
|
|
42
|
+
ifStmt = await super.visitIf(ifStmt, ctx) as J.If;
|
|
43
|
+
|
|
44
|
+
// Only process top-level if (not else-if branches)
|
|
45
|
+
const parentCursor = this.cursor.parent;
|
|
46
|
+
if (parentCursor) {
|
|
47
|
+
const parentIf = parentCursor.firstEnclosing(
|
|
48
|
+
(n): n is J.If => n.kind === J.Kind.If
|
|
49
|
+
);
|
|
50
|
+
if (parentIf) {
|
|
51
|
+
return ifStmt;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if the if has an else-if with the same body
|
|
56
|
+
if (!ifStmt.elsePart) {
|
|
57
|
+
return ifStmt;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const elseBody = ifStmt.elsePart.body.element;
|
|
61
|
+
if (elseBody.kind !== J.Kind.If) {
|
|
62
|
+
return ifStmt;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const elseIf = elseBody as J.If;
|
|
66
|
+
const ifBody = ifStmt.thenPart.element as Statement & J;
|
|
67
|
+
const elseIfBody = elseIf.thenPart.element as Statement & J;
|
|
68
|
+
|
|
69
|
+
if (!await bodiesEqual(ifBody, elseIfBody)) {
|
|
70
|
+
return ifStmt;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build combined condition: ifCond || elseIfCond
|
|
74
|
+
const ifCond = ifStmt.ifCondition.tree.element as Expression & J;
|
|
75
|
+
const elseIfCond = elseIf.ifCondition.tree.element as Expression & J;
|
|
76
|
+
|
|
77
|
+
// If either condition is && expression, wrap in parens
|
|
78
|
+
const leftNeedsParens = ifCond.kind === J.Kind.Binary &&
|
|
79
|
+
(ifCond as J.Binary).operator.element === J.Binary.Type.And;
|
|
80
|
+
const rightNeedsParens = elseIfCond.kind === J.Kind.Binary &&
|
|
81
|
+
(elseIfCond as J.Binary).operator.element === J.Binary.Type.And;
|
|
82
|
+
|
|
83
|
+
const cleanLeft = {...ifCond, prefix: emptySpace} as Expression;
|
|
84
|
+
const cleanRight = {...elseIfCond, prefix: emptySpace} as Expression;
|
|
85
|
+
|
|
86
|
+
let combined: J | undefined;
|
|
87
|
+
if (leftNeedsParens && rightNeedsParens) {
|
|
88
|
+
combined = await template`(${cleanLeft}) || (${cleanRight})`.apply(
|
|
89
|
+
ifStmt.ifCondition.tree.element, this.cursor
|
|
90
|
+
);
|
|
91
|
+
} else if (leftNeedsParens) {
|
|
92
|
+
combined = await template`(${cleanLeft}) || ${cleanRight}`.apply(
|
|
93
|
+
ifStmt.ifCondition.tree.element, this.cursor
|
|
94
|
+
);
|
|
95
|
+
} else if (rightNeedsParens) {
|
|
96
|
+
combined = await template`${cleanLeft} || (${cleanRight})`.apply(
|
|
97
|
+
ifStmt.ifCondition.tree.element, this.cursor
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
combined = await template`${cleanLeft} || ${cleanRight}`.apply(
|
|
101
|
+
ifStmt.ifCondition.tree.element, this.cursor
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!combined) {
|
|
106
|
+
return ifStmt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Unwrap ExpressionStatement if the template wrapped it
|
|
110
|
+
if (combined.kind === JS.Kind.ExpressionStatement) {
|
|
111
|
+
combined = (combined as JS.ExpressionStatement).expression as J;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Strip any leading newline from the combined expression
|
|
115
|
+
combined = stripLeadingNewline(combined);
|
|
116
|
+
|
|
117
|
+
// Build the merged if: combined condition, keep the if-body,
|
|
118
|
+
// skip the else-if and connect to whatever follows it
|
|
119
|
+
return await this.produceJava(ifStmt, ctx, draft => {
|
|
120
|
+
(draft.ifCondition.tree as any).element = combined;
|
|
121
|
+
draft.elsePart = elseIf.elsePart as any;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function printNode(node: J): Promise<string> {
|
|
129
|
+
const p = printer("org.openrewrite.javascript.tree.JS$CompilationUnit");
|
|
130
|
+
return (await p.print(node)).trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function bodiesEqual(a: Statement & J, b: Statement & J): Promise<boolean> {
|
|
134
|
+
return await printNode(a) === await printNode(b);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripLeadingNewline(node: J): J {
|
|
138
|
+
if (node.prefix.whitespace.includes('\n')) {
|
|
139
|
+
return {...node, prefix: emptySpace};
|
|
140
|
+
}
|
|
141
|
+
if (node.kind === J.Kind.Binary) {
|
|
142
|
+
const bin = node as J.Binary;
|
|
143
|
+
const newLeft = stripLeadingNewline(bin.left as J);
|
|
144
|
+
if (newLeft !== bin.left) {
|
|
145
|
+
return {...bin, left: newLeft as Expression} as unknown as J;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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 {ExecutionContext, printer, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
17
|
+
import {JavaScriptVisitor} from "@openrewrite/rewrite/javascript";
|
|
18
|
+
import {Expression, J} from "@openrewrite/rewrite/java";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes dead duplicate-condition branches in if/else-if chains.
|
|
22
|
+
*
|
|
23
|
+
* When the same condition appears more than once in an if/else-if chain,
|
|
24
|
+
* the later branch is dead code because the earlier branch already
|
|
25
|
+
* handles that case. Removing the unreachable branch eliminates confusion
|
|
26
|
+
* and prevents maintenance hazards.
|
|
27
|
+
*/
|
|
28
|
+
export class RemoveDuplicateConditions extends Recipe {
|
|
29
|
+
name = "org.openrewrite.javascript.cleanup.RemoveDuplicateConditions";
|
|
30
|
+
displayName = "Remove duplicate conditions in if/else-if chains";
|
|
31
|
+
description = "Remove `else if` branches whose condition is identical to an earlier " +
|
|
32
|
+
"branch in the same `if`/`else if` chain, since the duplicate branch " +
|
|
33
|
+
"is dead code that can never execute.";
|
|
34
|
+
tags = ["RSPEC-S1862"];
|
|
35
|
+
estimatedEffortPerOccurrence = 10;
|
|
36
|
+
|
|
37
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
38
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
39
|
+
override async visitIf(
|
|
40
|
+
ifStmt: J.If,
|
|
41
|
+
ctx: ExecutionContext
|
|
42
|
+
): Promise<J | undefined> {
|
|
43
|
+
ifStmt = await super.visitIf(ifStmt, ctx) as J.If;
|
|
44
|
+
|
|
45
|
+
// Only process top-level if (not else-if branches).
|
|
46
|
+
// Start from parent cursor to avoid matching self.
|
|
47
|
+
const parentCursor = this.cursor.parent;
|
|
48
|
+
if (parentCursor) {
|
|
49
|
+
const parentIf = parentCursor.firstEnclosing(
|
|
50
|
+
(n): n is J.If => n.kind === J.Kind.If
|
|
51
|
+
);
|
|
52
|
+
if (parentIf) {
|
|
53
|
+
return ifStmt;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const result = await removeDuplicates(ifStmt);
|
|
58
|
+
if (result === ifStmt) {
|
|
59
|
+
return ifStmt;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return await this.produceJava(ifStmt, ctx, draft => {
|
|
63
|
+
Object.assign(draft, result);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function printExpr(node: J): Promise<string> {
|
|
71
|
+
const p = printer("org.openrewrite.javascript.tree.JS$CompilationUnit");
|
|
72
|
+
return (await p.print(node)).trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function expressionsEqual(a: Expression & J, b: Expression & J): Promise<boolean> {
|
|
76
|
+
return await printExpr(a) === await printExpr(b);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function removeDuplicates(ifStmt: J.If): Promise<J.If> {
|
|
80
|
+
const seen: (Expression & J)[] = [ifStmt.ifCondition.tree.element as Expression & J];
|
|
81
|
+
let result = ifStmt;
|
|
82
|
+
let changed = false;
|
|
83
|
+
let current: J.If = ifStmt;
|
|
84
|
+
|
|
85
|
+
while (current.elsePart) {
|
|
86
|
+
const body = current.elsePart.body.element;
|
|
87
|
+
if (body.kind !== J.Kind.If) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const elseIf = body as J.If;
|
|
92
|
+
const cond = elseIf.ifCondition.tree.element as Expression & J;
|
|
93
|
+
let isDuplicate = false;
|
|
94
|
+
for (const prev of seen) {
|
|
95
|
+
if (await expressionsEqual(prev, cond)) {
|
|
96
|
+
isDuplicate = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isDuplicate) {
|
|
102
|
+
// Skip this else-if: connect current to the duplicate's else
|
|
103
|
+
const newElse = elseIf.elsePart;
|
|
104
|
+
result = replaceElse(result, current, newElse);
|
|
105
|
+
changed = true;
|
|
106
|
+
// Find the updated current node in the rebuilt tree
|
|
107
|
+
const updated = findById(result, current.id);
|
|
108
|
+
if (updated) {
|
|
109
|
+
current = updated;
|
|
110
|
+
} else {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
seen.push(cond);
|
|
115
|
+
current = elseIf;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return changed ? result : ifStmt;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function replaceElse(root: J.If, target: J.If, newElse: J.If.Else | undefined): J.If {
|
|
123
|
+
if (root.id === target.id) {
|
|
124
|
+
return {...root, elsePart: newElse};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const elsePart = root.elsePart;
|
|
128
|
+
if (!elsePart) {
|
|
129
|
+
return root;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const body = elsePart.body.element;
|
|
133
|
+
if (body.kind !== J.Kind.If) {
|
|
134
|
+
return root;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const branch = body as J.If;
|
|
138
|
+
const updated = replaceElse(branch, target, newElse);
|
|
139
|
+
if (updated === branch) {
|
|
140
|
+
return root;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
...root,
|
|
145
|
+
elsePart: {
|
|
146
|
+
...elsePart,
|
|
147
|
+
body: {...elsePart.body, element: updated}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findById(root: J.If, id: string): J.If | undefined {
|
|
153
|
+
if (root.id === id) {
|
|
154
|
+
return root;
|
|
155
|
+
}
|
|
156
|
+
const elsePart = root.elsePart;
|
|
157
|
+
if (!elsePart) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const body = elsePart.body.element;
|
|
161
|
+
if (body.kind === J.Kind.If) {
|
|
162
|
+
return findById(body as J.If, id);
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 {ExecutionContext, printer, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
17
|
+
import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
|
|
18
|
+
import {Expression, J} from "@openrewrite/rewrite/java";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes statements that assign a variable to itself.
|
|
22
|
+
*
|
|
23
|
+
* A self-assignment like `x = x` has no effect and is almost certainly
|
|
24
|
+
* a mistake -- the programmer likely intended to assign a different
|
|
25
|
+
* value or assign to a different target. Removing the statement keeps
|
|
26
|
+
* the code honest about what it actually does.
|
|
27
|
+
*
|
|
28
|
+
* Handles simple identifiers (`x = x`) and member access on `this`
|
|
29
|
+
* (`this.x = this.x`). Does not flag `this.x = x` because those
|
|
30
|
+
* deliberately copy a parameter into an instance field.
|
|
31
|
+
*/
|
|
32
|
+
export class RemoveSelfAssignment extends Recipe {
|
|
33
|
+
name = "org.openrewrite.javascript.cleanup.RemoveSelfAssignment";
|
|
34
|
+
displayName = "Remove self-assignments";
|
|
35
|
+
description = "Removes assignment statements where a variable is assigned to itself, " +
|
|
36
|
+
"such as `x = x` or `this.x = this.x`. These statements have no effect and " +
|
|
37
|
+
"typically indicate a programming error.";
|
|
38
|
+
tags = ["RSPEC-S1656"];
|
|
39
|
+
estimatedEffortPerOccurrence = 3;
|
|
40
|
+
|
|
41
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
42
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
43
|
+
override async visitBlock(
|
|
44
|
+
block: J.Block,
|
|
45
|
+
ctx: ExecutionContext
|
|
46
|
+
): Promise<J | undefined> {
|
|
47
|
+
block = await super.visitBlock(block, ctx) as J.Block;
|
|
48
|
+
|
|
49
|
+
const stmts = block.statements;
|
|
50
|
+
if (stmts.length === 0) {
|
|
51
|
+
return block;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const toRemove = new Set<number>();
|
|
55
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
56
|
+
const assign = getAssignment(stmts[i].element);
|
|
57
|
+
if (assign && await isSelfAssignment(assign)) {
|
|
58
|
+
toRemove.add(i);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (toRemove.size === 0) {
|
|
63
|
+
return block;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const newStmts = stmts.filter((_, idx) => !toRemove.has(idx));
|
|
67
|
+
return this.produceJava(block, ctx, draft => {
|
|
68
|
+
draft.statements = newStmts as any;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getAssignment(stmt: J): J.Assignment | undefined {
|
|
76
|
+
if (stmt.kind === JS.Kind.ExpressionStatement) {
|
|
77
|
+
const expr = (stmt as JS.ExpressionStatement).expression;
|
|
78
|
+
if (expr.kind === J.Kind.Assignment) {
|
|
79
|
+
return expr as J.Assignment;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (stmt.kind === J.Kind.Assignment) {
|
|
83
|
+
return stmt as J.Assignment;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function printNode(node: J): Promise<string> {
|
|
89
|
+
const p = printer("org.openrewrite.javascript.tree.JS$CompilationUnit");
|
|
90
|
+
return (await p.print(node)).trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function isSelfAssignment(assign: J.Assignment): Promise<boolean> {
|
|
94
|
+
const variable = assign.variable;
|
|
95
|
+
const value = assign.assignment.element as Expression & J;
|
|
96
|
+
|
|
97
|
+
return await printNode(variable as J) === await printNode(value as J);
|
|
98
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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 {ExecutionContext, printer, Recipe, TreeVisitor} from "@openrewrite/rewrite";
|
|
17
|
+
import {JavaScriptVisitor, JS} from "@openrewrite/rewrite/javascript";
|
|
18
|
+
import {J, Expression} from "@openrewrite/rewrite/java";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Removes consecutive assignments that write to the same object property
|
|
22
|
+
* or bracket-accessed key.
|
|
23
|
+
*
|
|
24
|
+
* When a value written to a property or bracket key is immediately
|
|
25
|
+
* overwritten in the very next statement, the first write is dead code.
|
|
26
|
+
* Removing it keeps the code focused on the value that actually persists.
|
|
27
|
+
*
|
|
28
|
+
* Handles both bracket notation (`obj["key"] = ...`) and dot notation
|
|
29
|
+
* (`obj.key = ...`).
|
|
30
|
+
*/
|
|
31
|
+
export class RemoveUnconditionalValueOverwrite extends Recipe {
|
|
32
|
+
name = "org.openrewrite.javascript.cleanup.RemoveUnconditionalValueOverwrite";
|
|
33
|
+
displayName = "Remove unconditional value overwrites";
|
|
34
|
+
description = "Remove consecutive assignments that write to the same object property " +
|
|
35
|
+
"or bracket-accessed key, since the first value is immediately " +
|
|
36
|
+
"overwritten and never used.";
|
|
37
|
+
tags = ["RSPEC-S4143"];
|
|
38
|
+
estimatedEffortPerOccurrence = 5;
|
|
39
|
+
|
|
40
|
+
async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
|
|
41
|
+
return new class extends JavaScriptVisitor<ExecutionContext> {
|
|
42
|
+
override async visitBlock(
|
|
43
|
+
block: J.Block,
|
|
44
|
+
ctx: ExecutionContext
|
|
45
|
+
): Promise<J | undefined> {
|
|
46
|
+
block = await super.visitBlock(block, ctx) as J.Block;
|
|
47
|
+
|
|
48
|
+
const stmts = block.statements;
|
|
49
|
+
if (stmts.length < 2) {
|
|
50
|
+
return block;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const toRemove = new Set<number>();
|
|
54
|
+
for (let i = 0; i < stmts.length - 1; i++) {
|
|
55
|
+
if (toRemove.has(i)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const aAssign = getAssignment(stmts[i].element);
|
|
59
|
+
if (!aAssign || !isPropertyAssign(aAssign)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const bAssign = getAssignment(stmts[i + 1].element);
|
|
63
|
+
if (!bAssign) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (await isDuplicateWrite(aAssign, bAssign)) {
|
|
67
|
+
toRemove.add(i);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (toRemove.size === 0) {
|
|
72
|
+
return block;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newStmts = stmts.filter((_, idx) => !toRemove.has(idx));
|
|
76
|
+
return this.produceJava(block, ctx, draft => {
|
|
77
|
+
draft.statements = newStmts as any;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getAssignment(stmt: J): J.Assignment | undefined {
|
|
85
|
+
if (stmt.kind === JS.Kind.ExpressionStatement) {
|
|
86
|
+
const expr = (stmt as JS.ExpressionStatement).expression;
|
|
87
|
+
if (expr.kind === J.Kind.Assignment) {
|
|
88
|
+
return expr as J.Assignment;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (stmt.kind === J.Kind.Assignment) {
|
|
92
|
+
return stmt as J.Assignment;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isPropertyAssign(assign: J.Assignment): boolean {
|
|
98
|
+
const v = assign.variable;
|
|
99
|
+
return v.kind === J.Kind.ArrayAccess || v.kind === J.Kind.FieldAccess;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function printNode(node: J): Promise<string> {
|
|
103
|
+
const p = printer("org.openrewrite.javascript.tree.JS$CompilationUnit");
|
|
104
|
+
return await p.print(node);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function isDuplicateWrite(a: J.Assignment, b: J.Assignment): Promise<boolean> {
|
|
108
|
+
const va = a.variable;
|
|
109
|
+
const vb = b.variable;
|
|
110
|
+
|
|
111
|
+
// Both bracket notation: obj["key"] = ...
|
|
112
|
+
if (va.kind === J.Kind.ArrayAccess && vb.kind === J.Kind.ArrayAccess) {
|
|
113
|
+
const aa = va as J.ArrayAccess;
|
|
114
|
+
const ab = vb as J.ArrayAccess;
|
|
115
|
+
return await printNode(aa.indexed) === await printNode(ab.indexed) &&
|
|
116
|
+
await printNode(aa.dimension.index.element as Expression & J) === await printNode(ab.dimension.index.element as Expression & J);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Both dot notation: obj.key = ...
|
|
120
|
+
if (va.kind === J.Kind.FieldAccess && vb.kind === J.Kind.FieldAccess) {
|
|
121
|
+
const fa = va as J.FieldAccess;
|
|
122
|
+
const fb = vb as J.FieldAccess;
|
|
123
|
+
return fa.name.element.simpleName === fb.name.element.simpleName &&
|
|
124
|
+
await printNode(fa.target) === await printNode(fb.target);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return false;
|
|
128
|
+
}
|