@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.
Files changed (51) hide show
  1. package/dist/all-branches-identical.d.ts +10 -0
  2. package/dist/all-branches-identical.d.ts.map +1 -0
  3. package/dist/all-branches-identical.js +116 -0
  4. package/dist/all-branches-identical.js.map +1 -0
  5. package/dist/boolean-checks-not-inverted.d.ts +10 -0
  6. package/dist/boolean-checks-not-inverted.d.ts.map +1 -0
  7. package/dist/boolean-checks-not-inverted.js +117 -0
  8. package/dist/boolean-checks-not-inverted.js.map +1 -0
  9. package/dist/collapsible-if-statements.d.ts +10 -0
  10. package/dist/collapsible-if-statements.d.ts.map +1 -0
  11. package/dist/collapsible-if-statements.js +119 -0
  12. package/dist/collapsible-if-statements.js.map +1 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +55 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/merge-identical-branches.d.ts +10 -0
  18. package/dist/merge-identical-branches.d.ts.map +1 -0
  19. package/dist/merge-identical-branches.js +118 -0
  20. package/dist/merge-identical-branches.js.map +1 -0
  21. package/dist/remove-duplicate-conditions.d.ts +10 -0
  22. package/dist/remove-duplicate-conditions.d.ts.map +1 -0
  23. package/dist/remove-duplicate-conditions.js +141 -0
  24. package/dist/remove-duplicate-conditions.js.map +1 -0
  25. package/dist/remove-self-assignment.d.ts +10 -0
  26. package/dist/remove-self-assignment.d.ts.map +1 -0
  27. package/dist/remove-self-assignment.js +86 -0
  28. package/dist/remove-self-assignment.js.map +1 -0
  29. package/dist/remove-unconditional-value-overwrite.d.ts +10 -0
  30. package/dist/remove-unconditional-value-overwrite.d.ts.map +1 -0
  31. package/dist/remove-unconditional-value-overwrite.js +112 -0
  32. package/dist/remove-unconditional-value-overwrite.js.map +1 -0
  33. package/dist/simplify-boolean-literal.d.ts +9 -0
  34. package/dist/simplify-boolean-literal.d.ts.map +1 -0
  35. package/dist/simplify-boolean-literal.js +179 -0
  36. package/dist/simplify-boolean-literal.js.map +1 -0
  37. package/dist/simplify-redundant-logical-expression.d.ts +10 -0
  38. package/dist/simplify-redundant-logical-expression.d.ts.map +1 -0
  39. package/dist/simplify-redundant-logical-expression.js +63 -0
  40. package/dist/simplify-redundant-logical-expression.js.map +1 -0
  41. package/package.json +39 -0
  42. package/src/all-branches-identical.ts +133 -0
  43. package/src/boolean-checks-not-inverted.ts +144 -0
  44. package/src/collapsible-if-statements.ts +162 -0
  45. package/src/index.ts +34 -0
  46. package/src/merge-identical-branches.ts +149 -0
  47. package/src/remove-duplicate-conditions.ts +165 -0
  48. package/src/remove-self-assignment.ts +98 -0
  49. package/src/remove-unconditional-value-overwrite.ts +128 -0
  50. package/src/simplify-boolean-literal.ts +220 -0
  51. 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
+ }