@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,63 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.SimplifyRedundantLogicalExpression = void 0;
13
+ const rewrite_1 = require("@openrewrite/rewrite");
14
+ const javascript_1 = require("@openrewrite/rewrite/javascript");
15
+ class SimplifyRedundantLogicalExpression extends rewrite_1.Recipe {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.name = "org.openrewrite.javascript.cleanup.SimplifyRedundantLogicalExpression";
19
+ this.displayName = "Simplify redundant logical expressions";
20
+ this.description = "Replace `x && x` with `x` and `x || x` with `x`. " +
21
+ "Identical operands in a logical expression are redundant " +
22
+ "and often indicate a copy-paste mistake.";
23
+ this.tags = ["RSPEC-S1764"];
24
+ this.estimatedEffortPerOccurrence = 2;
25
+ }
26
+ editor() {
27
+ return __awaiter(this, void 0, void 0, function* () {
28
+ return new class extends javascript_1.JavaScriptVisitor {
29
+ visitBinary(binary, ctx) {
30
+ const _super = Object.create(null, {
31
+ visitBinary: { get: () => super.visitBinary }
32
+ });
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ binary = (yield _super.visitBinary.call(this, binary, ctx));
35
+ const op = binary.operator.element;
36
+ if (op !== "And" && op !== "Or") {
37
+ return binary;
38
+ }
39
+ if (yield expressionsEqual(binary.left, binary.right)) {
40
+ return Object.assign(Object.assign({}, binary.left), { prefix: binary.prefix });
41
+ }
42
+ return binary;
43
+ });
44
+ }
45
+ };
46
+ });
47
+ }
48
+ }
49
+ exports.SimplifyRedundantLogicalExpression = SimplifyRedundantLogicalExpression;
50
+ function printNode(node) {
51
+ return __awaiter(this, void 0, void 0, function* () {
52
+ const p = (0, rewrite_1.printer)("org.openrewrite.javascript.tree.JS$CompilationUnit");
53
+ return yield p.print(node);
54
+ });
55
+ }
56
+ function expressionsEqual(a, b) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ const aText = (yield printNode(a)).trim();
59
+ const bText = (yield printNode(b)).trim();
60
+ return aText === bText;
61
+ });
62
+ }
63
+ //# sourceMappingURL=simplify-redundant-logical-expression.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simplify-redundant-logical-expression.js","sourceRoot":"","sources":["../src/simplify-redundant-logical-expression.ts"],"names":[],"mappings":";;;;;;;;;;;;AAeA,kDAAoF;AACpF,gEAAkE;AAalE,MAAa,kCAAmC,SAAQ,gBAAM;IAA9D;;QACI,SAAI,GAAG,uEAAuE,CAAC;QAC/E,gBAAW,GAAG,wCAAwC,CAAC;QACvD,gBAAW,GAAG,mDAAmD;YAC7D,2DAA2D;YAC3D,0CAA0C,CAAC;QAC/C,SAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QACvB,iCAA4B,GAAG,CAAC,CAAC;IAyBrC,CAAC;IAvBS,MAAM;;YACR,OAAO,IAAI,KAAM,SAAQ,8BAAmC;gBACzC,WAAW,CACtB,MAAgB,EAChB,GAAqB;;;;;wBAErB,MAAM,IAAG,MAAM,OAAM,WAAW,YAAC,MAAM,EAAE,GAAG,CAAa,CAAA,CAAC;wBAE1D,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;wBACnC,IAAI,EAAE,UAAsB,IAAI,EAAE,SAAqB,EAAE,CAAC;4BACtD,OAAO,MAAM,CAAC;wBAClB,CAAC;wBAED,IAAI,MAAM,gBAAgB,CAAC,MAAM,CAAC,IAAsB,EAAE,MAAM,CAAC,KAAuB,CAAC,EAAE,CAAC;4BAGxF,uCAAY,MAAM,CAAC,IAAY,KAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAE;wBAC5D,CAAC;wBAED,OAAO,MAAM,CAAC;oBAClB,CAAC;iBAAA;aACJ,CAAC;QACN,CAAC;KAAA;CACJ;AAhCD,gFAgCC;AAED,SAAe,SAAS,CAAC,IAAO;;QAC5B,MAAM,CAAC,GAAG,IAAA,iBAAO,EAAC,oDAAoD,CAAC,CAAC;QACxE,OAAO,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;CAAA;AAED,SAAe,gBAAgB,CAAC,CAAiB,EAAE,CAAiB;;QAGhE,MAAM,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC1C,OAAO,KAAK,KAAK,KAAK,CAAC;IAC3B,CAAC;CAAA"}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@openrewrite/recipes-code-quality",
3
+ "version": "0.1.0-20260409-154017",
4
+ "license": "Moderne Source Available License",
5
+ "description": "OpenRewrite recipes for JavaScript/TypeScript code quality.",
6
+ "homepage": "https://github.com/moderneinc/recipes-javascript",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist/**",
11
+ "src/**"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/index.js"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "rm -rf ./dist tsconfig.build.tsbuildinfo && tsc --build tsconfig.build.json",
21
+ "dev": "tsc --watch -p tsconfig.json",
22
+ "test": "npm run build && jest",
23
+ "ci:test": "jest"
24
+ },
25
+ "dependencies": {
26
+ "@openrewrite/rewrite": "next",
27
+ "immer": "^10.1.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/jest": "^29.5.13",
31
+ "@types/node": "^22.5.4",
32
+ "jest": "^29.7.0",
33
+ "jest-junit": "^16.0.0",
34
+ "tmp-promise": "^3.0.3",
35
+ "ts-jest": "^29.2.5",
36
+ "ts-node": "^10.9.2",
37
+ "typescript": "^5.6.2"
38
+ }
39
+ }
@@ -0,0 +1,133 @@
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 {J, Statement} from "@openrewrite/rewrite/java";
19
+
20
+ /**
21
+ * Replaces if/else-if/else chains where every branch has the same body
22
+ * with just the body.
23
+ *
24
+ * When all branches of a conditional execute identical code the condition
25
+ * is meaningless. The entire chain can be replaced with a single copy of
26
+ * the body, removing unnecessary complexity.
27
+ *
28
+ * Only applies when there is an explicit `else` so that all paths are
29
+ * covered.
30
+ */
31
+ export class AllBranchesIdentical extends Recipe {
32
+ name = "org.openrewrite.javascript.cleanup.AllBranchesIdentical";
33
+ displayName = "Remove conditional with identical branches";
34
+ description = "Replace `if`/`else if`/`else` chains where every branch has " +
35
+ "the same body with just the body, since the condition has " +
36
+ "no effect on what code executes.";
37
+ tags = ["RSPEC-S3923"];
38
+ estimatedEffortPerOccurrence = 5;
39
+
40
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
41
+ return new class extends JavaScriptVisitor<ExecutionContext> {
42
+ override async visitIf(
43
+ ifStmt: J.If,
44
+ ctx: ExecutionContext
45
+ ): Promise<J | undefined> {
46
+ ifStmt = await super.visitIf(ifStmt, ctx) as J.If;
47
+
48
+ // Only process top-level if (not else-if branches).
49
+ // Start from parent cursor to avoid matching self.
50
+ const parentCursor = this.cursor.parent;
51
+ if (parentCursor) {
52
+ const parentIf = parentCursor.firstEnclosing(
53
+ (n): n is J.If => n.kind === J.Kind.If
54
+ );
55
+ if (parentIf) {
56
+ return ifStmt;
57
+ }
58
+ }
59
+
60
+ // Must have an explicit else to cover all cases
61
+ if (!hasExplicitElse(ifStmt)) {
62
+ return ifStmt;
63
+ }
64
+
65
+ const bodies = collectBodies(ifStmt);
66
+ if (!await allBodiesEqual(bodies)) {
67
+ return ifStmt;
68
+ }
69
+
70
+ // Replace with just the body, preserving the if prefix
71
+ const thenBody = ifStmt.thenPart.element;
72
+ return {...thenBody, prefix: ifStmt.prefix} as Statement & J;
73
+ }
74
+ };
75
+ }
76
+ }
77
+
78
+ function hasExplicitElse(ifStmt: J.If): boolean {
79
+ let current: J.If = ifStmt;
80
+ while (true) {
81
+ const elsePart = current.elsePart;
82
+ if (!elsePart) {
83
+ return false;
84
+ }
85
+ const body = elsePart.body.element;
86
+ if (body.kind === J.Kind.If) {
87
+ current = body as J.If;
88
+ } else {
89
+ // Reached a plain else block
90
+ return true;
91
+ }
92
+ }
93
+ }
94
+
95
+ function collectBodies(ifStmt: J.If): (Statement & J)[] {
96
+ const bodies: (Statement & J)[] = [ifStmt.thenPart.element as Statement & J];
97
+ let current: J.If = ifStmt;
98
+ while (true) {
99
+ const elsePart = current.elsePart;
100
+ if (!elsePart) {
101
+ break;
102
+ }
103
+ const body = elsePart.body.element;
104
+ if (body.kind === J.Kind.If) {
105
+ const elseIf = body as J.If;
106
+ bodies.push(elseIf.thenPart.element as Statement & J);
107
+ current = elseIf;
108
+ } else {
109
+ // Plain else body
110
+ bodies.push(body as Statement & J);
111
+ break;
112
+ }
113
+ }
114
+ return bodies;
115
+ }
116
+
117
+ async function printNode(node: J): Promise<string> {
118
+ const p = printer("org.openrewrite.javascript.tree.JS$CompilationUnit");
119
+ return (await p.print(node)).trim();
120
+ }
121
+
122
+ async function allBodiesEqual(bodies: (Statement & J)[]): Promise<boolean> {
123
+ if (bodies.length < 2) {
124
+ return false;
125
+ }
126
+ const firstText = await printNode(bodies[0]);
127
+ for (let i = 1; i < bodies.length; i++) {
128
+ if (await printNode(bodies[i]) !== firstText) {
129
+ return false;
130
+ }
131
+ }
132
+ return true;
133
+ }
@@ -0,0 +1,144 @@
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, Recipe, TreeVisitor} from "@openrewrite/rewrite";
17
+ import {capture, JavaScriptVisitor, pattern, rewrite, template} from "@openrewrite/rewrite/javascript";
18
+ import {J} from "@openrewrite/rewrite/java";
19
+
20
+ /**
21
+ * Replaces inverted boolean checks with their simpler equivalents.
22
+ *
23
+ * Negating a comparison with `!` when an opposite operator exists
24
+ * adds unnecessary mental overhead. Flipping the operator directly
25
+ * is shorter and reads more naturally.
26
+ *
27
+ * Also removes double negation (`!!x` is left alone as it is a common
28
+ * boolean-coercion idiom, but `!(!x)` with parentheses is simplified).
29
+ */
30
+ export class BooleanChecksNotInverted extends Recipe {
31
+ name = "org.openrewrite.javascript.cleanup.BooleanChecksNotInverted";
32
+ displayName = "Boolean checks should not be inverted";
33
+ description = "Replaces inverted boolean comparisons with their simpler equivalents. " +
34
+ "For example, `!(a === b)` becomes `a !== b` and `!(a < b)` becomes `a >= b`. " +
35
+ "Also simplifies double negation like `!(!x)` to `x`.";
36
+ tags = ["RSPEC-S1940"];
37
+ estimatedEffortPerOccurrence = 2;
38
+
39
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
40
+ const a = () => capture();
41
+ const b = () => capture();
42
+
43
+ // !(a === b) -> a !== b
44
+ const notStrictEquals = rewrite(() => {
45
+ const [x, y] = [a(), b()];
46
+ return {
47
+ before: pattern`!(${x} === ${y})`,
48
+ after: template`${x} !== ${y}`
49
+ };
50
+ });
51
+
52
+ // !(a !== b) -> a === b
53
+ const notStrictNotEquals = rewrite(() => {
54
+ const [x, y] = [a(), b()];
55
+ return {
56
+ before: pattern`!(${x} !== ${y})`,
57
+ after: template`${x} === ${y}`
58
+ };
59
+ });
60
+
61
+ // !(a == b) -> a != b
62
+ const notLooseEquals = rewrite(() => {
63
+ const [x, y] = [a(), b()];
64
+ return {
65
+ before: pattern`!(${x} == ${y})`,
66
+ after: template`${x} != ${y}`
67
+ };
68
+ });
69
+
70
+ // !(a != b) -> a == b
71
+ const notLooseNotEquals = rewrite(() => {
72
+ const [x, y] = [a(), b()];
73
+ return {
74
+ before: pattern`!(${x} != ${y})`,
75
+ after: template`${x} == ${y}`
76
+ };
77
+ });
78
+
79
+ // !(a < b) -> a >= b
80
+ const notLessThan = rewrite(() => {
81
+ const [x, y] = [a(), b()];
82
+ return {
83
+ before: pattern`!(${x} < ${y})`,
84
+ after: template`${x} >= ${y}`
85
+ };
86
+ });
87
+
88
+ // !(a > b) -> a <= b
89
+ const notGreaterThan = rewrite(() => {
90
+ const [x, y] = [a(), b()];
91
+ return {
92
+ before: pattern`!(${x} > ${y})`,
93
+ after: template`${x} <= ${y}`
94
+ };
95
+ });
96
+
97
+ // !(a <= b) -> a > b
98
+ const notLessOrEqual = rewrite(() => {
99
+ const [x, y] = [a(), b()];
100
+ return {
101
+ before: pattern`!(${x} <= ${y})`,
102
+ after: template`${x} > ${y}`
103
+ };
104
+ });
105
+
106
+ // !(a >= b) -> a < b
107
+ const notGreaterOrEqual = rewrite(() => {
108
+ const [x, y] = [a(), b()];
109
+ return {
110
+ before: pattern`!(${x} >= ${y})`,
111
+ after: template`${x} < ${y}`
112
+ };
113
+ });
114
+
115
+ // !(!x) -> x (double negation with parentheses)
116
+ const doubleNegation = rewrite(() => {
117
+ const x = capture();
118
+ return {
119
+ before: pattern`!(!${x})`,
120
+ after: template`${x}`
121
+ };
122
+ });
123
+
124
+ const rules = notStrictEquals
125
+ .orElse(notStrictNotEquals)
126
+ .orElse(notLooseEquals)
127
+ .orElse(notLooseNotEquals)
128
+ .orElse(notLessThan)
129
+ .orElse(notGreaterThan)
130
+ .orElse(notLessOrEqual)
131
+ .orElse(notGreaterOrEqual)
132
+ .orElse(doubleNegation);
133
+
134
+ return new class extends JavaScriptVisitor<ExecutionContext> {
135
+ override async visitUnary(
136
+ unary: J.Unary,
137
+ ctx: ExecutionContext
138
+ ): Promise<J | undefined> {
139
+ unary = await super.visitUnary(unary, ctx) as J.Unary;
140
+ return await rules.tryOn(this.cursor, unary) || unary;
141
+ }
142
+ };
143
+ }
144
+ }
@@ -0,0 +1,162 @@
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, Recipe, TreeVisitor} from "@openrewrite/rewrite";
17
+ import {JavaScriptVisitor, JS, template} from "@openrewrite/rewrite/javascript";
18
+ import {emptySpace, Expression, J} from "@openrewrite/rewrite/java";
19
+
20
+ /**
21
+ * Merges collapsible if statements into a single condition joined by `&&`.
22
+ *
23
+ * When an `if` block contains nothing but another `if` and neither has
24
+ * an `else` branch, the two conditions can be combined. This reduces
25
+ * nesting depth and makes the guard logic easier to follow at a glance.
26
+ *
27
+ * If the inner condition is a logical-or expression it is wrapped in
28
+ * parentheses to preserve evaluation order.
29
+ */
30
+ export class CollapsibleIfStatements extends Recipe {
31
+ name = "org.openrewrite.javascript.cleanup.CollapsibleIfStatements";
32
+ displayName = "Collapsible if statements should be merged";
33
+ description = "Merges nested if statements that can be combined with `&&`. " +
34
+ "For example, `if (a) { if (b) { ... } }` becomes `if (a && b) { ... }`. " +
35
+ "Only applies when neither if has an else clause and the outer body contains " +
36
+ "only the inner if.";
37
+ tags = ["RSPEC-S1066"];
38
+ estimatedEffortPerOccurrence = 5;
39
+
40
+ async editor(): Promise<TreeVisitor<any, ExecutionContext>> {
41
+ return new class extends JavaScriptVisitor<ExecutionContext> {
42
+ override async visitIf(
43
+ ifStmt: J.If,
44
+ ctx: ExecutionContext
45
+ ): Promise<J | undefined> {
46
+ ifStmt = await super.visitIf(ifStmt, ctx) as J.If;
47
+
48
+ // Must not have an else branch
49
+ if (ifStmt.elsePart) {
50
+ return ifStmt;
51
+ }
52
+
53
+ // The body must be a block containing exactly one statement
54
+ const body = ifStmt.thenPart.element;
55
+ if (body.kind !== J.Kind.Block) {
56
+ return ifStmt;
57
+ }
58
+
59
+ const block = body as J.Block;
60
+ if (block.statements.length !== 1) {
61
+ return ifStmt;
62
+ }
63
+
64
+ // That single statement must be another if with no else
65
+ const inner = block.statements[0].element;
66
+ if (inner.kind !== J.Kind.If) {
67
+ return ifStmt;
68
+ }
69
+
70
+ const innerIf = inner as J.If;
71
+ if (innerIf.elsePart) {
72
+ return ifStmt;
73
+ }
74
+
75
+ // Build the combined condition: outerCond && innerCond
76
+ // Strip any existing prefix (whitespace/newlines) from the
77
+ // conditions so the template controls spacing
78
+ const outerCond = {...(ifStmt.ifCondition.tree.element as Expression & J), prefix: emptySpace} as Expression;
79
+ const innerCond = {...(innerIf.ifCondition.tree.element as Expression & J), prefix: emptySpace} as Expression;
80
+
81
+ // If inner condition is || expression, wrap in parens
82
+ const needsParens = innerCond.kind === J.Kind.Binary &&
83
+ (innerCond as J.Binary).operator.element === J.Binary.Type.Or;
84
+
85
+ let combined: J | undefined;
86
+ if (needsParens) {
87
+ combined = await template`${outerCond} && (${innerCond})`.apply(
88
+ ifStmt.ifCondition.tree.element, this.cursor
89
+ );
90
+ } else {
91
+ combined = await template`${outerCond} && ${innerCond}`.apply(
92
+ ifStmt.ifCondition.tree.element, this.cursor
93
+ );
94
+ }
95
+
96
+ if (!combined) {
97
+ return ifStmt;
98
+ }
99
+
100
+ // The template may wrap the result in an ExpressionStatement;
101
+ // unwrap it to get the actual expression.
102
+ if (combined.kind === JS.Kind.ExpressionStatement) {
103
+ combined = (combined as JS.ExpressionStatement).expression as J;
104
+ }
105
+
106
+ // Strip any leading newline from the combined expression.
107
+ combined = stripLeadingNewline(combined);
108
+
109
+ // Fix the inner then-body indentation: it was nested one level
110
+ // deeper, so we need to dedent by one level
111
+ const innerBody = innerIf.thenPart.element as J;
112
+ const outerIndent = getIndent(ifStmt.prefix);
113
+ const dedented = dedentBlock(innerBody as J.Block, outerIndent);
114
+
115
+ return await this.produceJava(ifStmt, ctx, draft => {
116
+ (draft.ifCondition.tree as any).element = combined;
117
+ (draft.thenPart as any).element = dedented;
118
+ });
119
+ }
120
+ };
121
+ }
122
+ }
123
+
124
+ function stripLeadingNewline(node: J): J {
125
+ // The prefix of the outermost node may not have the newline;
126
+ // it might be on the leftmost child. Walk down the left spine.
127
+ if (node.prefix.whitespace.includes('\n')) {
128
+ return {...node, prefix: emptySpace};
129
+ }
130
+ // For binary expressions, the newline might be on the left operand
131
+ if (node.kind === J.Kind.Binary) {
132
+ const bin = node as J.Binary;
133
+ const newLeft = stripLeadingNewline(bin.left as J);
134
+ if (newLeft !== bin.left) {
135
+ return {...bin, left: newLeft as Expression} as unknown as J;
136
+ }
137
+ }
138
+ return node;
139
+ }
140
+
141
+ function getIndent(sp: J.Space): string {
142
+ const ws = sp.whitespace;
143
+ const lastNewline = ws.lastIndexOf('\n');
144
+ if (lastNewline === -1) {
145
+ return "";
146
+ }
147
+ return ws.substring(lastNewline + 1);
148
+ }
149
+
150
+ function dedentBlock(block: J.Block, outerIndent: string): J.Block {
151
+ const bodyIndent = outerIndent + " ";
152
+
153
+ const newStmts = block.statements.map(stmt => {
154
+ const el = stmt.element as J;
155
+ const newPrefix = {...el.prefix, whitespace: "\n" + bodyIndent};
156
+ return {...stmt, element: {...el, prefix: newPrefix}};
157
+ });
158
+
159
+ const newEnd = {...block.end, whitespace: "\n" + outerIndent};
160
+
161
+ return {...block, statements: newStmts, end: newEnd} as J.Block;
162
+ }
package/src/index.ts ADDED
@@ -0,0 +1,34 @@
1
+ import {CategoryDescriptor, RecipeMarketplace} from "@openrewrite/rewrite";
2
+ import {AllBranchesIdentical} from "./all-branches-identical";
3
+ import {SimplifyBooleanLiteral} from "./simplify-boolean-literal";
4
+ import {BooleanChecksNotInverted} from "./boolean-checks-not-inverted";
5
+ import {CollapsibleIfStatements} from "./collapsible-if-statements";
6
+ import {MergeIdenticalBranches} from "./merge-identical-branches";
7
+ import {RemoveDuplicateConditions} from "./remove-duplicate-conditions";
8
+ import {RemoveSelfAssignment} from "./remove-self-assignment";
9
+ import {RemoveUnconditionalValueOverwrite} from "./remove-unconditional-value-overwrite";
10
+ import {SimplifyRedundantLogicalExpression} from "./simplify-redundant-logical-expression";
11
+
12
+ export const CodeQuality: CategoryDescriptor[] = [{displayName: "Code quality"}];
13
+
14
+ export async function activate(marketplace: RecipeMarketplace): Promise<void> {
15
+ await marketplace.install(AllBranchesIdentical, CodeQuality);
16
+ await marketplace.install(SimplifyBooleanLiteral, CodeQuality);
17
+ await marketplace.install(BooleanChecksNotInverted, CodeQuality);
18
+ await marketplace.install(CollapsibleIfStatements, CodeQuality);
19
+ await marketplace.install(MergeIdenticalBranches, CodeQuality);
20
+ await marketplace.install(RemoveDuplicateConditions, CodeQuality);
21
+ await marketplace.install(RemoveSelfAssignment, CodeQuality);
22
+ await marketplace.install(RemoveUnconditionalValueOverwrite, CodeQuality);
23
+ await marketplace.install(SimplifyRedundantLogicalExpression, CodeQuality);
24
+ }
25
+
26
+ export {AllBranchesIdentical} from "./all-branches-identical";
27
+ export {SimplifyBooleanLiteral} from "./simplify-boolean-literal";
28
+ export {BooleanChecksNotInverted} from "./boolean-checks-not-inverted";
29
+ export {CollapsibleIfStatements} from "./collapsible-if-statements";
30
+ export {MergeIdenticalBranches} from "./merge-identical-branches";
31
+ export {RemoveDuplicateConditions} from "./remove-duplicate-conditions";
32
+ export {RemoveSelfAssignment} from "./remove-self-assignment";
33
+ export {RemoveUnconditionalValueOverwrite} from "./remove-unconditional-value-overwrite";
34
+ export {SimplifyRedundantLogicalExpression} from "./simplify-redundant-logical-expression";