feat(lint): rule from IntelliJ, js/useSimplifiedBooleanExpression
MODIFY
internal/compiler/lint/rules/index.ts
internal/compiler/lint/rules/index.ts
-/* GENERATED:START(hash:036ee9bf2f9cb7e3d500cfd8a09b87829de06088,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:975209047ce41adf41a399951c4d0e439e9f6633,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
import useClosingNonVoid from "./html/useClosingNonVoid";
import noArguments from "./js/noArguments";
import noAsyncPromiseExecutor from "./js/noAsyncPromiseExecutor";
@@ -41,6 +41,7 @@ import useCamelCase from "./js/useCamelCase";
import useDefaultExportBasename from "./js/useDefaultExportBasename";
import useDefaultImportBasename from "./js/useDefaultImportBasename";
import useFunctionDeclarations from "./js/useFunctionDeclarations";
+import useSimplifiedBooleanExpression from "./js/useSimplifiedBooleanExpression";
import useSingleCaseStatement from "./js/useSingleCaseStatement";
import useSingleVarDeclarator from "./js/useSingleVarDeclarator";
import useSortedSpecifiers from "./js/useSortedSpecifiers";
@@ -154,6 +155,10 @@ lintTransforms.set("js/useCamelCase", useCamelCase);
lintTransforms.set("js/useDefaultExportBasename", useDefaultExportBasename);
lintTransforms.set("js/useDefaultImportBasename", useDefaultImportBasename);
lintTransforms.set("js/useFunctionDeclarations", useFunctionDeclarations);
+lintTransforms.set(
+ "js/useSimplifiedBooleanExpression",
+ useSimplifiedBooleanExpression,
+);
lintTransforms.set("js/useSingleCaseStatement", useSingleCaseStatement);
lintTransforms.set("js/useSingleVarDeclarator", useSingleVarDeclarator);
lintTransforms.set("js/useSortedSpecifiers", useSortedSpecifiers);
@@ -293,6 +298,7 @@ export const lintRuleNames: Array<LintRuleName> = [
"js/useDefaultExportBasename",
"js/useDefaultImportBasename",
"js/useFunctionDeclarations",
+ "js/useSimplifiedBooleanExpression",
"js/useSingleCaseStatement",
"js/useSingleVarDeclarator",
"js/useSortedSpecifiers",
@@ -406,6 +412,7 @@ export type LintRuleName =
| "js/useDefaultExportBasename"
| "js/useDefaultImportBasename"
| "js/useFunctionDeclarations"
+ | "js/useSimplifiedBooleanExpression"
| "js/useSingleCaseStatement"
| "js/useSingleVarDeclarator"
| "js/useSortedSpecifiers"

ADD
internal/compiler/lint/rules/js/useSimplifiedBooleanExpression.test.rjson
+filename: "file.ts"
+invalid: [
+ "
+ const boolExp = true;
+ const r = boolExp === true;
+ "
+ "
+ const boolExp = true;
+ const r = true !== boolExp;
+ "
+ "
+ const boolExp = true;
+ const r = !!boolExp;
+ "
+ "
+ const boolExp = true;
+ const r = true && boolExp;
+ "
+ "
+ const boolExp = true;
+ const r = boolExp || true;
+ "
+ "
+ const nonNullExp = 123;
+ const r = null ?? nonNullExp;
+ "
+ "
+ const boolExpr1 = true;
+ const boolExpr2 = false;
+ const r = (!boolExpr1) || (!boolExpr2);
+ "
+]
+valid: [
+
+]
\ No newline at end of file

ADD
internal/compiler/lint/rules/js/useSimplifiedBooleanExpression.ts
+import {
+ AnyJSExpression,
+ JSBooleanLiteral,
+ JSLogicalExpression,
+ JSUnaryExpression,
+} from "@internal/ast";
+import { Path, signals } from "@internal/compiler";
+import { createVisitor } from "@internal/compiler/utils";
+import { descriptions } from "@internal/diagnostics";
+
+export default createVisitor({
+ name: "js/useSimplifiedBooleanExpression",
+ enter(path) {
+ const { node } = path;
+
+ if (node.type === "JSBinaryExpression") {
+ if (
+ node.operator === "===" ||
+ node.operator === "==" ||
+ node.operator === "!==" ||
+ node.operator === "!="
+ ) {
+ // if the operator is "not equal" (exclusive) or one of the operands is "false"
+ // we consider the expression to be "negated"
+ let negated = node.operator === "!==" || node.operator === "!=";
+
+ if (node.left.type === "JSBooleanLiteral") {
+ return simplifyBinaryExpressionDiagnostic(
+ path,
+ node.left,
+ node.right,
+ negated
+ );
+ }
+ if (node.right.type === "JSBooleanLiteral") {
+ return simplifyBinaryExpressionDiagnostic(
+ path,
+ node.right,
+ node.left,
+ negated
+ );
+ }
+ }
+ } else if (
+ node.type === "JSUnaryExpression" &&
+ node.operator === "!" &&
+ node.argument.type === "JSUnaryExpression" &&
+ node.argument.operator === "!"
+ ) {
+ return path.addFixableDiagnostic(
+ {
+ fixed: signals.replace({ ...node.argument.argument }),
+ },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+ } else if (node.type === "JSLogicalExpression") {
+ if (node.operator === "&&") {
+ if (node.left.type === "JSBooleanLiteral") {
+ return simplifyAndExpression(path, node.left, node.right);
+ }
+ if (node.right.type === "JSBooleanLiteral") {
+ return simplifyAndExpression(path, node.right, node.left);
+ }
+
+ if (couldApplyDeMorgan(node)) {
+ return simplifyDeMorgan(path, node);
+ }
+ } else if (node.operator === "||") {
+ if (node.left.type === "JSBooleanLiteral") {
+ return simplifyOrExpression(path, node.left, node.right);
+ }
+ if (node.right.type === "JSBooleanLiteral") {
+ return simplifyOrExpression(path, node.right, node.left);
+ }
+
+ if (couldApplyDeMorgan(node)) {
+ return simplifyDeMorgan(path, node);
+ }
+ } else if (node.operator === "??" && node.left.type === "JSNullLiteral") {
+ return path.addFixableDiagnostic(
+ {
+ fixed: signals.replace({
+ ...node.right,
+ }),
+ },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+ }
+ }
+ /* TODO: simplify the following case when the AST will support it
+ if (boolExp) {
+ return true;
+ }
+ return false;
+ */
+
+ return signals.retain;
+ },
+});
+
+function simplifyBinaryExpressionDiagnostic(
+ path: Path,
+ toRemove: JSBooleanLiteral,
+ expression: AnyJSExpression,
+ negated: boolean
+) {
+ const shouldNegate = xor(negated, !toRemove.value);
+ return path.addFixableDiagnostic(
+ {
+ fixed: signals.replace(createSimpleExpression(expression, shouldNegate)),
+ },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+}
+
+function createSimpleExpression(
+ expression: AnyJSExpression,
+ shouldNegate: boolean
+): AnyJSExpression {
+ return shouldNegate
+ ? {
+ type: "JSUnaryExpression",
+ operator: "!",
+ argument: expression,
+ }
+ : {
+ ...expression,
+ };
+}
+
+function simplifyAndExpression(
+ path: Path,
+ literal: JSBooleanLiteral,
+ expression: AnyJSExpression
+) {
+ return keepExpressionIfLiteral(path, expression, literal, true);
+}
+
+function simplifyOrExpression(
+ path: Path,
+ literal: JSBooleanLiteral,
+ expression: AnyJSExpression
+) {
+ return keepExpressionIfLiteral(path, expression, literal, false);
+}
+
+function keepExpressionIfLiteral(
+ path: Path,
+ expression: AnyJSExpression,
+ literal: JSBooleanLiteral,
+ expectedValue: boolean
+) {
+ if (literal.value === expectedValue) {
+ return path.addFixableDiagnostic(
+ { fixed: signals.replace({ ...expression }) },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+ }
+ return path.addFixableDiagnostic(
+ { fixed: signals.replace({ ...literal }) },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+}
+
+// https://en.wikipedia.org/wiki/De_Morgan%27s_laws
+function couldApplyDeMorgan(
+ node: JSLogicalExpression
+): node is DeMorganExpression {
+ return (
+ node.left.type === "JSUnaryExpression" &&
+ node.right.type === "JSUnaryExpression" &&
+ node.left.operator === "!" &&
+ node.right.operator === "!"
+ );
+}
+
+function simplifyDeMorgan(path: Path, node: DeMorganExpression) {
+ return path.addFixableDiagnostic(
+ {
+ fixed: signals.replace({
+ type: "JSUnaryExpression",
+ operator: "!",
+ argument: {
+ ...node,
+ left: node.left.argument,
+ right: node.right.argument,
+ operator: node.operator === "||" ? "&&" : "||",
+ },
+ }),
+ },
+ descriptions.LINT.JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION
+ );
+}
+
+interface NegatedExpression extends JSUnaryExpression {
+ operator: "!";
+}
+
+interface DeMorganExpression extends JSLogicalExpression {
+ left: NegatedExpression;
+ right: NegatedExpression;
+}
+
+function xor(x: boolean, y: boolean) {
+ return x ? !y : y;
+}

MODIFY
internal/compiler/lint/rules/tests.ts
internal/compiler/lint/rules/tests.ts
@@ -8,7 +8,7 @@ type Test = {
type Tests = Dict<Test | Array<Test>>;
-/* GENERATED:START(hash:9079da4baf626c79c2d70dd6c3408e2d73320bd4,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:1f918a6fdde78d80ad8778847105378ec2cb90d6,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
// @ts-ignore
import useClosingNonVoid from "./html/useClosingNonVoid.test.rjson";
// @ts-ignore
@@ -92,6 +92,8 @@ import useDefaultImportBasename from "./js/useDefaultImportBasename.test.rjson";
// @ts-ignore
import useFunctionDeclarations from "./js/useFunctionDeclarations.test.rjson";
// @ts-ignore
+import useSimplifiedBooleanExpression from "./js/useSimplifiedBooleanExpression.test.rjson";
+// @ts-ignore
import useSingleCaseStatement from "./js/useSingleCaseStatement.test.rjson";
// @ts-ignore
import useSingleVarDeclarator from "./js/useSingleVarDeclarator.test.rjson";
@@ -264,6 +266,7 @@ export const tests: Tests = {
"js/useDefaultExportBasename": useDefaultExportBasename,
"js/useDefaultImportBasename": useDefaultImportBasename,
"js/useFunctionDeclarations": useFunctionDeclarations,
+ "js/useSimplifiedBooleanExpression": useSimplifiedBooleanExpression,
"js/useSingleCaseStatement": useSingleCaseStatement,
"js/useSingleVarDeclarator": useSingleVarDeclarator,
"js/useSortedSpecifiers": useSortedSpecifiers,

MODIFY
internal/diagnostics/categories.ts
internal/diagnostics/categories.ts
@@ -109,7 +109,7 @@ export type DiagnosticCategoryPrefix =
| "typeCheck"
| "v8";
-/* GENERATED:START(hash:4fc28e881ad64941ef6ccfeb3edd1e9fa8b79578,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
+/* GENERATED:START(hash:ad06cb0919fbdc43e38e4a4a4079e71e0701b17a,id:main) Everything below is automatically generated. DO NOT MODIFY. Run `./rome run scripts/generated-files/lint-rules` to update. */
export type DiagnosticLintCategory =
| "lint/html/useClosingNonVoid"
| "lint/js/noArguments"
@@ -153,6 +153,7 @@ export type DiagnosticLintCategory =
| "lint/js/useDefaultExportBasename"
| "lint/js/useDefaultImportBasename"
| "lint/js/useFunctionDeclarations"
+ | "lint/js/useSimplifiedBooleanExpression"
| "lint/js/useSingleCaseStatement"
| "lint/js/useSingleVarDeclarator"
| "lint/js/useSortedSpecifiers"

MODIFY
internal/diagnostics/descriptions/lint.ts
internal/diagnostics/descriptions/lint.ts
@@ -111,20 +111,19 @@ export const lint = createDiagnosticsCategory({
},
JSX_A11Y_ARIA_PROPTYPES: (
attributeName: string,
- values?: Array<string | boolean>,
+ values?: Array<string | boolean>
) => {
let advice: DiagnosticAdvice = [];
if (values) {
advice.push({
type: "log",
category: "info",
- text: markup`The supported values for the <emphasis>${attributeName}</emphasis> attribute are: ${values.reduce(
- (str, value) => {
+ text: markup`The supported values for the <emphasis>${attributeName}</emphasis> attribute are: ${values
+ .reduce((str, value) => {
str.push(typeof value === "boolean" ? String(value) : `"${value}"`);
return str;
- },
- ([] as Array<string>),
- ).join(", ")}`,
+ }, [] as Array<string>)
+ .join(", ")}`,
});
}
return {
@@ -134,7 +133,9 @@ export const lint = createDiagnosticsCategory({
};
},
- JSX_A11Y_NO_NONINTERACTIVE_ELEMENT_TO_INTERACTIVE_ROLE: (element: string) => ({
+ JSX_A11Y_NO_NONINTERACTIVE_ELEMENT_TO_INTERACTIVE_ROLE: (
+ element: string
+ ) => ({
category: "lint/jsx-a11y/noNoninteractiveElementToInteractiveRole",
message: markup`The HTML element <emphasis>${element}</emphasis> is non-interactive and should not have an interactive role.`,
advice: [
@@ -302,7 +303,7 @@ export const lint = createDiagnosticsCategory({
},
JSX_A11Y_MOUSE_EVENTS_HAVE_KEY_EVENTS: (
mouseEvent: string,
- keyboardEvent: string,
+ keyboardEvent: string
) => ({
category: "lint/jsx-a11y/useKeyWithMouseEvents",
message: markup`Pair the <emphasis>${mouseEvent}</emphasis> mouse event with the <emphasis>${keyboardEvent}</emphasis> keyboard event.`,
@@ -486,7 +487,7 @@ export const lint = createDiagnosticsCategory({
},
JSX_A11Y_ROLE_HAS_REQUIRED_ARIA_PROPS: (
roleName: string,
- missingAttributes: Array<string>,
+ missingAttributes: Array<string>
) => ({
category: "lint/jsx-a11y/useAriaPropsForRole",
message: markup`The element with the <emphasis>${roleName}</emphasis> ARIA role does not have the required ARIA attributes.`,
@@ -589,7 +590,7 @@ export const lint = createDiagnosticsCategory({
REACT_USE_SORT_COMP: (
right: string,
wrong: string,
- position: "before" | "after",
+ position: "before" | "after"
) => ({
category: "lint/react/useSortComp",
message: markup`<emphasis>${wrong}</emphasis> should be placed ${position} <emphasis>${right}</emphasis>.`,
@@ -622,17 +623,17 @@ export const lint = createDiagnosticsCategory({
},
REACT_NO_VOID_ELEMENTS_WITH_CHILDREN: (
element: string,
- properties: Array<string>,
+ properties: Array<string>
) => ({
category: "lint/react/noVoidElementsWithChildren",
message: markup`<emphasis>${element}</emphasis> is a void element tag and must not have <emphasis>${orJoin(
- properties.map((name) => markup`${name}`),
+ properties.map((name) => markup`${name}`)
)}</emphasis>.`,
}),
JS_USE_DEFAULT_IMPORT_BASENAME: (prev: string, basenames: Array<string>) => ({
category: "lint/js/useDefaultImportBasename",
message: markup`Use the basename ${orJoin(
- addEmphasis(basenames.map((basename) => markup`${basename}`)),
+ addEmphasis(basenames.map((basename) => markup`${basename}`))
)} when importing the default.`,
advice: [
{
@@ -704,7 +705,10 @@ export const lint = createDiagnosticsCategory({
},
],
}),
- JS_NO_UNDECLARED_VARIABLES: (name: string, bindingsInScope: Array<string>) => ({
+ JS_NO_UNDECLARED_VARIABLES: (
+ name: string,
+ bindingsInScope: Array<string>
+ ) => ({
category: "lint/js/noUndeclaredVariables",
message: markup`The <emphasis>${name}</emphasis> variable is undeclared`,
advice: buildSuggestionAdvice(name, bindingsInScope),
@@ -852,7 +856,7 @@ export const lint = createDiagnosticsCategory({
type: "log",
category: "info",
text: markup`It's hard to visually count the amount of spaces, it's clearer if you use a quantifier instead. eg / {${String(
- count,
+ count
)}}/`,
},
],
@@ -1040,8 +1044,7 @@ export const lint = createDiagnosticsCategory({
category: "lint/regex/noReferenceToNonExistingGroup",
message: markup`Avoid nonexistent group names. Check the <emphasis>${name}</emphasis> group.`,
}),
- JS_USE_DEFAULT_EXPORT_BASENAME: (
- {
+ JS_USE_DEFAULT_EXPORT_BASENAME: ({
defaultName,
defaultType,
actualFilename,
@@ -1051,8 +1054,7 @@ export const lint = createDiagnosticsCategory({
defaultType: string;
actualFilename: string;
correctFilename: string;
- },
- ) => {
+ }) => {
let adviceMessage;
if (defaultName === "*default*") {
@@ -1098,7 +1100,7 @@ export const lint = createDiagnosticsCategory({
relativeFilename: string,
language: DiagnosticLanguage,
original: string,
- formatted: string,
+ formatted: string
) => ({
category: "lint/pendingFixes",
message: markup`Pending formatting and safe fixes`,
@@ -1108,7 +1110,7 @@ export const lint = createDiagnosticsCategory({
language,
diff: stringDiff(original, formatted),
},
- ({
+ {
type: "action",
command: "check",
shortcut: "f",
@@ -1118,8 +1120,8 @@ export const lint = createDiagnosticsCategory({
commandFlags: {
apply: true,
},
- } as DiagnosticAdviceAction),
- ({
+ } as DiagnosticAdviceAction,
+ {
type: "action",
hidden: true,
command: "check",
@@ -1130,7 +1132,7 @@ export const lint = createDiagnosticsCategory({
commandFlags: {
format: true,
},
- } as DiagnosticAdviceAction),
+ } as DiagnosticAdviceAction,
],
}),
TS_NO_EXPLICIT_ANY: {
@@ -1144,4 +1146,8 @@ export const lint = createDiagnosticsCategory({
},
],
},
+ JS_USE_SIMPLIFIED_BOOLEAN_EXPRESSION: {
+ category: "lint/js/useSimplifiedBooleanExpression",
+ message: markup`Boolean expression contains unnecessary complexity.`,
+ },
});

feat(lint): rule from IntelliJ, js/useSimplifiedBooleanExpression