mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules (#34341)
* chore(ui): add filename, size, JSX-handler, prefer-const, and antd lint rules Wires up five error-level ESLint rules on the dashboard, grandfathering every current offender into eslint-suppressions.json so the gate only bites new code and ratchets down as files are fixed - local/filename-pascal-case: new local rule requiring PascalCase .tsx names, exempting Next.js reserved files (page, layout, route, ...) and test/spec files (239 grandfathered) - max-lines: 800 lines over src/**, excluding tests, src/data, and generated schema.d.ts (20 grandfathered) - local/no-complex-jsx-arrow: new local rule flagging inline JSX arrow handlers with block bodies over two statements; each failure is a small extract-to-named -handler refactor (65 grandfathered) - prefer-const: flipped from off to error (103 grandfathered) - no-restricted-imports: added antd to the phase-out ban alongside tremor, and pointed both messages at shadcn/ui primitives (405 antd import sites grandfathered) Both new local rules ship with RuleTester coverage * fix(ui): preserve secondary extensions in filename-pascal-case suggestion The suggestion text built the rename from only the head segment, so a multi-dot file like my-component.utils.tsx was told to become MyComponent.tsx instead of MyComponent.utils.tsx. Rebuild it from the PascalCased head plus the untouched remaining segments, and add tests covering multi-dot filenames and the hyphenated Next.js reserved names (global-error, apple-icon, opengraph-image, twitter-image)
This commit is contained in:
parent
0c2b86e167
commit
67fce87b16
7 changed files with 2766 additions and 147 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -19,12 +19,13 @@ const eslintConfig = [
|
|||
"unused-imports/no-unused-imports": "error",
|
||||
"local/no-large-inline-object-arg": "warn",
|
||||
"local/no-long-condition-chain": "warn",
|
||||
"local/no-complex-jsx-arrow": ["error", { maxStatements: 2 }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"prefer-const": "off",
|
||||
"prefer-const": "error",
|
||||
"no-empty": "off",
|
||||
"no-prototype-builtins": "off",
|
||||
"no-useless-catch": "off",
|
||||
|
|
@ -51,13 +52,32 @@ const eslintConfig = [
|
|||
patterns: [
|
||||
{
|
||||
group: ["@tremor/react", "@tremor/react/*"],
|
||||
message: "@tremor/react is being phased out; build new UI with antd instead of adding tremor imports.",
|
||||
message:
|
||||
"@tremor/react is being phased out; build new UI with shadcn/ui primitives instead of adding tremor imports.",
|
||||
},
|
||||
{
|
||||
group: ["antd", "antd/*"],
|
||||
message:
|
||||
"antd is being phased out; build new UI with shadcn/ui primitives instead of adding antd imports.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.tsx"],
|
||||
rules: {
|
||||
"local/filename-pascal-case": "error",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.{ts,tsx}"],
|
||||
ignores: ["src/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}", "src/data/**"],
|
||||
rules: {
|
||||
"max-lines": ["error", { max: 800, skipBlankLines: true, skipComments: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/lib/http/**"],
|
||||
rules: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { basename } from "path";
|
||||
|
||||
const NEXT_RESERVED = new Set([
|
||||
"page",
|
||||
"layout",
|
||||
"route",
|
||||
"template",
|
||||
"default",
|
||||
"loading",
|
||||
"error",
|
||||
"global-error",
|
||||
"not-found",
|
||||
"middleware",
|
||||
"instrumentation",
|
||||
"sitemap",
|
||||
"robots",
|
||||
"manifest",
|
||||
"icon",
|
||||
"apple-icon",
|
||||
"favicon",
|
||||
"opengraph-image",
|
||||
"twitter-image",
|
||||
]);
|
||||
|
||||
const PASCAL_CASE = /^[A-Z][A-Za-z0-9]*$/;
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description: "Require PascalCase filenames for .tsx modules; exempt Next.js reserved files and test/spec files.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
notPascalCase: "Filename '{{name}}' should be PascalCase (e.g. '{{suggestion}}.tsx').",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.filename;
|
||||
const stem = basename(filename).replace(/\.tsx$/, "");
|
||||
const [head, ...rest] = stem.split(".");
|
||||
if (rest.includes("test") || rest.includes("spec")) return {};
|
||||
if (NEXT_RESERVED.has(head)) return {};
|
||||
if (PASCAL_CASE.test(head)) return {};
|
||||
const pascalHead = head
|
||||
.split(/[-_]/)
|
||||
.filter(Boolean)
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join("");
|
||||
const suggestion = [pascalHead, ...rest].join(".");
|
||||
return {
|
||||
Program(node) {
|
||||
context.report({ node, messageId: "notPascalCase", data: { name: `${stem}.tsx`, suggestion } });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
import noLargeInlineObjectArg from "./no-large-inline-object-arg.mjs";
|
||||
import noLongConditionChain from "./no-long-condition-chain.mjs";
|
||||
import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
|
||||
import filenamePascalCase from "./filename-pascal-case.mjs";
|
||||
|
||||
const plugin = {
|
||||
rules: {
|
||||
"no-large-inline-object-arg": noLargeInlineObjectArg,
|
||||
"no-long-condition-chain": noLongConditionChain,
|
||||
"no-complex-jsx-arrow": noComplexJsxArrow,
|
||||
"filename-pascal-case": filenamePascalCase,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
const DEFAULT_MAX_STATEMENTS = 2;
|
||||
|
||||
const isJsxAttributeValue = (node) => {
|
||||
const parent = node.parent;
|
||||
if (parent == null) return false;
|
||||
return parent.type === "JSXExpressionContainer" && parent.parent?.type === "JSXAttribute";
|
||||
};
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow arrow functions with block bodies over a few statements passed inline as JSX attributes; extract them into a named handler.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { maxStatements: { type: "integer", minimum: 1 } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
tooComplex: "Inline JSX arrow handler has {{count}} statements; extract it into a named function (max {{max}}).",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const maxStatements = context.options[0]?.maxStatements ?? DEFAULT_MAX_STATEMENTS;
|
||||
return {
|
||||
ArrowFunctionExpression(node) {
|
||||
if (node.body.type !== "BlockStatement") return;
|
||||
if (!isJsxAttributeValue(node)) return;
|
||||
const count = node.body.body.length;
|
||||
if (count <= maxStatements) return;
|
||||
context.report({ node, messageId: "tooComplex", data: { count, max: maxStatements } });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { RuleTester } from "eslint";
|
||||
import rule from "../../scripts/eslint-rules/filename-pascal-case.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: "latest", sourceType: "module", parserOptions: { ecmaFeatures: { jsx: true } } },
|
||||
});
|
||||
|
||||
ruleTester.run("filename-pascal-case", rule as never, {
|
||||
valid: [
|
||||
{ code: "export const x = 1;", filename: "src/components/TeamInfo.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/components/Button.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/teams/page.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/teams/layout.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/teams/not-found.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/global-error.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/apple-icon.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/opengraph-image.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/app/twitter-image.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/components/TeamInfo.test.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/components/view_users.test.tsx" },
|
||||
{ code: "export const x = 1;", filename: "src/components/TeamInfo.spec.tsx" },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "export const x = 1;",
|
||||
filename: "src/components/user_info_view.tsx",
|
||||
errors: [{ messageId: "notPascalCase", data: { name: "user_info_view.tsx", suggestion: "UserInfoView" } }],
|
||||
},
|
||||
{
|
||||
code: "export const x = 1;",
|
||||
filename: "src/components/team-info.tsx",
|
||||
errors: [{ messageId: "notPascalCase", data: { name: "team-info.tsx", suggestion: "TeamInfo" } }],
|
||||
},
|
||||
{
|
||||
code: "export const x = 1;",
|
||||
filename: "src/components/teamInfo.tsx",
|
||||
errors: [{ messageId: "notPascalCase", data: { name: "teamInfo.tsx", suggestion: "TeamInfo" } }],
|
||||
},
|
||||
{
|
||||
code: "export const x = 1;",
|
||||
filename: "src/components/my-component.utils.tsx",
|
||||
errors: [
|
||||
{ messageId: "notPascalCase", data: { name: "my-component.utils.tsx", suggestion: "MyComponent.utils" } },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { RuleTester } from "eslint";
|
||||
import rule from "../../scripts/eslint-rules/no-complex-jsx-arrow.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: "latest", sourceType: "module", parserOptions: { ecmaFeatures: { jsx: true } } },
|
||||
});
|
||||
|
||||
ruleTester.run("no-complex-jsx-arrow", rule as never, {
|
||||
valid: [
|
||||
"const x = <button onClick={() => doThing()} />;",
|
||||
"const x = <button onClick={() => { a(); }} />;",
|
||||
"const x = <button onClick={() => { a(); b(); }} />;",
|
||||
"const handler = () => { a(); b(); c(); }; const x = <button onClick={handler} />;",
|
||||
"const run = () => { a(); b(); c(); };",
|
||||
"foo(() => { a(); b(); c(); });",
|
||||
"const x = <List renderItem={(i) => i.name} />;",
|
||||
{ code: "const x = <button onClick={() => { a(); b(); c(); }} />;", options: [{ maxStatements: 3 }] },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const x = <button onClick={() => { a(); b(); c(); }} />;",
|
||||
errors: [{ messageId: "tooComplex", data: { count: 3, max: 2 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = <form onSubmit={() => { a(); b(); c(); d(); }} />;",
|
||||
errors: [{ messageId: "tooComplex", data: { count: 4, max: 2 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = <button onClick={() => { a(); b(); }} />;",
|
||||
options: [{ maxStatements: 1 }],
|
||||
errors: [{ messageId: "tooComplex", data: { count: 2, max: 1 } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue