feat: add jsx/file-extension lint rule
MODIFY
packages/@romejs/compiler/lint/rules/index.ts
packages/@romejs/compiler/lint/rules/index.ts
@@ -80,6 +80,7 @@ import noTargetBlank from "./jsx-a11y/noTargetBlank";
import roleHasRequiredAriaProps from "./jsx-a11y/roleHasRequiredAriaProps";
import scope from "./jsx-a11y/scope";
import tabindexNoPositive from "./jsx-a11y/tabindexNoPositive";
+import fileExtension from "./jsx/fileExtension";
import noCommentText from "./jsx/noCommentText";
import noDuplicateProps from "./jsx/noDuplicateProps";
import pascalCase from "./jsx/pascalCase";
@@ -185,6 +186,7 @@ export const lintTransforms = [
roleHasRequiredAriaProps,
scope,
tabindexNoPositive,
+ fileExtension,
noCommentText,
noDuplicateProps,
pascalCase,

ADD
packages/@romejs/compiler/lint/rules/jsx/fileExtension.test.ts
+import {test} from "rome";
+import {testLint} from "../../utils/testing";
+
+const JSX_CASES = [
+ "<div></div>",
+ "<></>",
+ "<Fragment></Fragment>",
+ "<React.Fragment></React.Fragment>",
+];
+
+const NON_JSX_CASES = ["'<div></div>'"];
+
+test(
+ "jsx file extension",
+ async (t) => {
+ await testLint(
+ t,
+ {
+ invalid: JSX_CASES,
+ valid: NON_JSX_CASES,
+ },
+ {category: "lint/jsx/fileExtension", path: "test.js"},
+ );
+
+ await testLint(
+ t,
+ {
+ valid: JSX_CASES,
+ },
+ {category: "lint/jsx/fileExtension", path: "test.jsx"},
+ );
+
+ await testLint(
+ t,
+ {
+ valid: JSX_CASES,
+ },
+ {category: "lint/jsx/fileExtension", path: "test.tsx"},
+ );
+ },
+);

ADD
packages/@romejs/compiler/lint/rules/jsx/fileExtension.ts
+import {CompilerContext, Path, TransformExitResult} from "@romejs/compiler";
+import {descriptions} from "@romejs/diagnostics";
+import {AnyNode} from "@romejs/ast";
+
+const JSX_FILE_EXTENSIONS = [".jsx", ".tsx"];
+
+function isJSXNode(node: AnyNode): boolean {
+ return node.type === "JSXFragment" || node.type === "JSXElement";
+}
+
+function isJSXFile(context: CompilerContext): boolean {
+ return JSX_FILE_EXTENSIONS.includes(context.path.getExtensions());
+}
+
+export default {
+ name: "jsxFileExtension",
+ enter(path: Path): TransformExitResult {
+ const {node, context} = path;
+
+ if (isJSXNode(node) && !isJSXFile(context)) {
+ path.context.addNodeDiagnostic(
+ node,
+ descriptions.LINT.JSX_FILE_EXTENSION(
+ context.path.getExtensions(),
+ context.path.getExtensionlessBasename(),
+ ),
+ );
+ }
+
+ return node;
+ },
+};

MODIFY
packages/@romejs/diagnostics/categories.ts
packages/@romejs/diagnostics/categories.ts
@@ -169,6 +169,7 @@ type LintDiagnosticCategory =
| "lint/jsx-a11y/roleHasRequiredAriaProps"
| "lint/jsx-a11y/scope"
| "lint/jsx-a11y/tabindexNoPositive"
+ | "lint/jsx/fileExtension"
| "lint/jsx/noCommentText"
| "lint/jsx/noDuplicateProps"
| "lint/jsx/pascalCase"

MODIFY
packages/@romejs/diagnostics/descriptions/lint.ts
packages/@romejs/diagnostics/descriptions/lint.ts
@@ -16,6 +16,17 @@ import {buildSuggestionAdvice} from "../helpers";
import {createDiagnosticsCategory, orJoin} from "./index";
export const lint = createDiagnosticsCategory({
+ JSX_FILE_EXTENSION: (ext: string, basename: string) => ({
+ category: "lint/jsx/fileExtension",
+ message: `Files with the <emphasis>${ext}</emphasis> extension cannot contain JSX elements.`,
+ advice: [
+ {
+ type: "log",
+ category: "info",
+ text: `Change the <emphasis>${basename}${ext}</emphasis> file extension to <emphasis>.jsx</emphasis> or <emphasis>.tsx</emphasis>.`,
+ },
+ ],
+ }),
TS_PREFER_INTERFACES: {
category: "lint/ts/preferInterfaces",
message: "Use an interface instead of an object type alias",

MODIFY
website/src/docs/check/rules/index.md
website/src/docs/check/rules/index.md
@@ -95,6 +95,7 @@ showHero: false
| role-has-required-aria-props | |
| scope | |
| tabindex-no-positive | |
+| file-extension | |
| no-comment-text | |
| no-duplicate-props | |
| pascal-case | |

feat: add jsx/file-extension lint rule (#680)