feat: add react/button-has-type lint rule
MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -156,6 +156,7 @@ type LintDiagnosticCategory =
| "lint/jsx-a11y/noTargetBlank"
| "lint/jsx-a11y/scope"
| "lint/jsx-a11y/tabindexNoPositive"
+ | "lint/react/buttonHasType"
| "lint/react/jsxKey"
| "lint/react/jsxNoCommentText"
| "lint/react/noChildrenProp"

MODIFY
packages/@romejs/diagnostics/descriptions.ts
packages/@romejs/diagnostics/descriptions.ts
@@ -292,6 +292,12 @@ export const descriptions = createMessages({
},
// @romejs/js-compiler
LINT: {
+ REACT_BUTTON_HAS_TYPE: {
+ category: "lint/react/buttonHasType",
+ message: `Use an explicit <emphasis>type</emphasis> prop on <emphasis>${escapeMarkup(
+ "<button>",
+ )}</emphasis> elements.`,
+ },
JSX_A11Y_TABINDEX_NO_POSITIVE: {
category: "lint/jsx-a11y/tabindexNoPositive",
message: "Avoid positive integer values for <emphasis>tabIndex</emphasis>.",

MODIFY
packages/@romejs/js-compiler/lint/rules/index.ts
packages/@romejs/js-compiler/lint/rules/index.ts
@@ -70,6 +70,7 @@ import noDistractingElements from "./jsx-a11y/noDistractingElements";
import noTargetBlank from "./jsx-a11y/noTargetBlank";
import scope from "./jsx-a11y/scope";
import tabindexNoPositive from "./jsx-a11y/tabindexNoPositive";
+import buttonHasType from "./react/buttonHasType";
import jsxKey from "./react/jsxKey";
import jsxNoCommentText from "./react/jsxNoCommentText";
import noChildrenProp from "./react/noChildrenProp";
@@ -148,6 +149,7 @@ export const lintTransforms = [
noTargetBlank,
scope,
tabindexNoPositive,
+ buttonHasType,
jsxKey,
jsxNoCommentText,
noChildrenProp,

ADD
packages/@romejs/js-compiler/lint/rules/react/buttonHasType.test.ts
+import {test} from "rome";
+import {testLintMultiple} from "../testHelpers";
+
+test(
+ "button has type",
+ async (t) => {
+ await testLintMultiple(
+ t,
+ [
+ // INVALID
+ "<button>child</button>",
+ '<button type="foo">child</button>',
+ "React.createElement('button', {type: 'foo'}, ['child'])",
+ "createElement('button', {type: 'foo'}, ['child'])",
+ // VALID
+ "<div>child</div>",
+ '<div type="type">child</div>',
+ '<button type="button">child</button>',
+ '<button type="submit">child</button>',
+ '<button type="reset">child</button>',
+ "React.createElement('div', ['child'])",
+ "React.createElement('div', {type: 'type'}, ['child'])",
+ "React.createElement('button', {type: 'button'}, ['child'])",
+ "React.createElement('button', {type: 'submit'}, ['child'])",
+ "React.createElement('button', {type: 'reset'}, ['child'])",
+ "createElement('div', ['child'])",
+ "createElement('div', {type: 'type'}, ['child'])",
+ "createElement('button', {type: 'button'}, ['child'])",
+ "createElement('button', {type: 'submit'}, ['child'])",
+ "createElement('button', {type: 'reset'}, ['child'])",
+ ],
+ {category: "lint/react/buttonHasType"},
+ );
+ },
+);

ADD
packages/@romejs/js-compiler/lint/rules/react/buttonHasType.ts
+import {Path, TransformExitResult} from "@romejs/js-compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {AnyNode} from "@romejs/ast";
+import {
+ doesNodeMatchPattern,
+ getJSXAttribute,
+ hasJSXAttribute,
+ isJSXElement,
+} from "@romejs/js-ast-utils";
+
+const BUTTON_TYPE_REGEX = /^(reset)|(submit)|(button)$/;
+
+function createElementMissingType(node: AnyNode) {
+ if (node.type !== "JSCallExpression") {
+ return false;
+ }
+ if (
+ (doesNodeMatchPattern(node.callee, "React.createElement") ||
+ doesNodeMatchPattern(node.callee, "createElement")) &&
+ node.arguments[0].type === "JSStringLiteral" &&
+ node.arguments[0].value === "button" &&
+ node.arguments[1].type === "JSObjectExpression" &&
+ !node.arguments[1].properties.find((prop) =>
+ prop.type === "JSObjectProperty" &&
+ prop.key.value.type === "JSIdentifier" &&
+ prop.key.value.name === "type" &&
+ prop.value.type === "JSStringLiteral" &&
+ BUTTON_TYPE_REGEX.test(prop.value.value)
+ )
+ ) {
+ return true;
+ }
+ return false;
+}
+
+function jsxMissingType(node: AnyNode) {
+ if (!isJSXElement(node, "button")) {
+ return false;
+ }
+ if (!hasJSXAttribute(node, "type")) {
+ return true;
+ }
+ const valueNode = getJSXAttribute(node, "type")?.value;
+ if (
+ valueNode?.type === "JSStringLiteral" &&
+ !BUTTON_TYPE_REGEX.test(valueNode.value)
+ ) {
+ return true;
+ }
+ return false;
+}
+
+export default {
+ name: "buttonHasType",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (createElementMissingType(node) || jsxMissingType(node)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.REACT_BUTTON_HAS_TYPE,
+ );
+ }
+
+ return node;
+ },
+};

feat: add react/button-has-type lint rule (#486)