@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,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";
|