mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
feat(ui): add eslint rules for nested ternaries, large inline object args, and long condition chains (#32415)
* feat(ui): add eslint rules for nested ternaries, large inline object args, and long condition chains Adds three dashboard lint rules to keep new code readable. Nested ternaries are banned outright via the built-in no-nested-ternary, with the 265 existing occurrences grandfathered in eslint-suppressions.json so only new ones fail. Two custom rules ship as a small local plugin under scripts/eslint-rules: no-large-inline-object-arg flags object literals with 4+ properties passed straight into a call, nudging toward a named variable, and no-long-condition-chain flags boolean expressions that combine 4+ conditions, nudging toward a named boolean. Both are warnings tracked on the existing budget ratchet (eslint-budgets.json + eslint-metrics.json) with headroom above the current counts, so they ratchet down over time rather than freezing a baseline. Both thresholds are configurable rule options and covered by RuleTester unit tests. * fix(ui): scope no-long-condition-chain to boolean operators, not nullish Greptile flagged that the rule counted nullish-coalescing chains the same as &&/|| chains, so a 4-part `a ?? b ?? c ?? d` fallback surfaced "Boolean expression combines 4 conditions", which is inaccurate since a `??` fallback is value defaulting, not a condition. Restrict the visitor to && / || nodes so `??` chains are treated as leaves, while a boolean chain nested inside a `??` is still caught. Drops 6 miscounted occurrences (240 -> 234). * chore(ui): sync lint metrics and suppressions with staging Merge advanced the base branch, adding one no-large-inline-object-arg occurrence (508 -> 509) and making one grandfathered react-hooks suppression stale. Regenerate eslint-metrics.json and prune the suppression so the budget/drift gate passes. * chore(ui): sync lint metrics with staging Merge advanced the base, adding four no-large-inline-object-arg occurrences (509 -> 513). Regenerate eslint-metrics.json so the drift gate passes.
This commit is contained in:
parent
0f1e29b334
commit
5973d9fd2b
9 changed files with 641 additions and 3 deletions
|
|
@ -2,5 +2,7 @@
|
|||
"@typescript-eslint/no-explicit-any": { "max": 2040, "target": 1500 },
|
||||
"no-console": { "max": 484, "target": 0 },
|
||||
"complexity": { "max": 140, "target": 80 },
|
||||
"max-depth": { "max": 70, "target": 30 }
|
||||
"max-depth": { "max": 70, "target": 30 },
|
||||
"local/no-large-inline-object-arg": { "max": 560, "target": 300 },
|
||||
"local/no-long-condition-chain": { "max": 265, "target": 120 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"@typescript-eslint/no-explicit-any": 1988,
|
||||
"complexity": 128,
|
||||
"local/no-large-inline-object-arg": 513,
|
||||
"local/no-long-condition-chain": 233,
|
||||
"max-depth": 59,
|
||||
"no-console": 15
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@ import tseslint from "typescript-eslint";
|
|||
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||
import prettier from "eslint-config-prettier/flat";
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import local from "./scripts/eslint-rules/index.mjs";
|
||||
|
||||
const eslintConfig = [
|
||||
{
|
||||
|
|
@ -13,9 +14,11 @@ const eslintConfig = [
|
|||
...nextCoreWebVitals,
|
||||
prettier,
|
||||
{
|
||||
plugins: { "unused-imports": unusedImports },
|
||||
plugins: { "unused-imports": unusedImports, local },
|
||||
rules: {
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"local/no-large-inline-object-arg": "warn",
|
||||
"local/no-long-condition-chain": "warn",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
|
|
@ -28,6 +31,7 @@ const eslintConfig = [
|
|||
"no-useless-escape": "off",
|
||||
"no-self-assign": "error",
|
||||
"no-var": "error",
|
||||
"no-nested-ternary": "error",
|
||||
"react/no-danger": "error",
|
||||
complexity: ["warn", 20],
|
||||
"max-depth": ["warn", 4],
|
||||
|
|
|
|||
11
ui/litellm-dashboard/scripts/eslint-rules/index.mjs
Normal file
11
ui/litellm-dashboard/scripts/eslint-rules/index.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import noLargeInlineObjectArg from "./no-large-inline-object-arg.mjs";
|
||||
import noLongConditionChain from "./no-long-condition-chain.mjs";
|
||||
|
||||
const plugin = {
|
||||
rules: {
|
||||
"no-large-inline-object-arg": noLargeInlineObjectArg,
|
||||
"no-long-condition-chain": noLongConditionChain,
|
||||
},
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const DEFAULT_MIN_PROPERTIES = 4;
|
||||
|
||||
const isArgumentOf = (node) => {
|
||||
const parent = node.parent;
|
||||
if (parent == null) return false;
|
||||
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") return false;
|
||||
return parent.arguments.includes(node);
|
||||
};
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow passing a large object literal inline as a call argument; assign it to a named variable first.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { minProperties: { type: "integer", minimum: 1 } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
tooLarge:
|
||||
"Object literal with {{count}} properties passed inline as an argument; assign it to a named variable first.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const minProperties = context.options[0]?.minProperties ?? DEFAULT_MIN_PROPERTIES;
|
||||
return {
|
||||
ObjectExpression(node) {
|
||||
if (!isArgumentOf(node)) return;
|
||||
if (node.properties.length < minProperties) return;
|
||||
context.report({ node, messageId: "tooLarge", data: { count: node.properties.length } });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const DEFAULT_MIN_CONDITIONS = 4;
|
||||
|
||||
const isBooleanLogical = (node) =>
|
||||
node?.type === "LogicalExpression" && (node.operator === "&&" || node.operator === "||");
|
||||
|
||||
const countConditions = (node) =>
|
||||
isBooleanLogical(node) ? countConditions(node.left) + countConditions(node.right) : 1;
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "suggestion",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow logical expressions that combine many conditions; extract the condition into a named boolean.",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { minConditions: { type: "integer", minimum: 2 } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
tooMany: "Boolean expression combines {{count}} conditions; extract it into a named variable.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const minConditions = context.options[0]?.minConditions ?? DEFAULT_MIN_CONDITIONS;
|
||||
return {
|
||||
LogicalExpression(node) {
|
||||
if (!isBooleanLogical(node)) return;
|
||||
if (isBooleanLogical(node.parent)) return;
|
||||
const count = countConditions(node);
|
||||
if (count < minConditions) return;
|
||||
context.report({ node, messageId: "tooMany", data: { count } });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { RuleTester } from "eslint";
|
||||
import rule from "../../scripts/eslint-rules/no-large-inline-object-arg.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
});
|
||||
|
||||
ruleTester.run("no-large-inline-object-arg", rule as never, {
|
||||
valid: [
|
||||
"foo({ a: 1, b: 2, c: 3 });",
|
||||
"foo({});",
|
||||
"const opts = { a: 1, b: 2, c: 3, d: 4 }; foo(opts);",
|
||||
"const x = { a: 1, b: 2, c: 3, d: 4 };",
|
||||
"function f() { return { a: 1, b: 2, c: 3, d: 4 }; }",
|
||||
"const arr = [{ a: 1, b: 2, c: 3, d: 4 }];",
|
||||
"foo(1, 2, { a: 1, b: 2 });",
|
||||
{ code: "foo({ a: 1, b: 2, c: 3, d: 4 });", options: [{ minProperties: 5 }] },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "foo({ a: 1, b: 2, c: 3, d: 4 });",
|
||||
errors: [{ messageId: "tooLarge", data: { count: 4 } }],
|
||||
},
|
||||
{
|
||||
code: "new Widget({ a: 1, b: 2, c: 3, d: 4, e: 5 });",
|
||||
errors: [{ messageId: "tooLarge", data: { count: 5 } }],
|
||||
},
|
||||
{
|
||||
code: "foo(1, { a: 1, b: 2, c: 3, d: 4 });",
|
||||
errors: [{ messageId: "tooLarge" }],
|
||||
},
|
||||
{
|
||||
code: "foo({ a: 1, ...rest, c: 3, d: 4 });",
|
||||
errors: [{ messageId: "tooLarge", data: { count: 4 } }],
|
||||
},
|
||||
{
|
||||
code: "foo({ a: 1, b: 2, c: 3 });",
|
||||
options: [{ minProperties: 3 }],
|
||||
errors: [{ messageId: "tooLarge", data: { count: 3 } }],
|
||||
},
|
||||
{
|
||||
code: "outer({ a: 1, b: 2, c: 3, d: 4 }, inner({ e: 5, f: 6, g: 7, h: 8 }));",
|
||||
errors: [{ messageId: "tooLarge" }, { messageId: "tooLarge" }],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { RuleTester } from "eslint";
|
||||
import rule from "../../scripts/eslint-rules/no-long-condition-chain.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
});
|
||||
|
||||
ruleTester.run("no-long-condition-chain", rule as never, {
|
||||
valid: [
|
||||
"const x = a && b && c;",
|
||||
"const x = a || b || c;",
|
||||
"const x = a && (b || c);",
|
||||
"const x = a && b;",
|
||||
"if (a || b || c) {}",
|
||||
"const x = a ?? b ?? c;",
|
||||
"const url = a ?? b ?? c ?? d;",
|
||||
"const x = (a && b) ?? c ?? d;",
|
||||
{ code: "const x = a && b && c && d;", options: [{ minConditions: 5 }] },
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: "const x = a && b && c && d;",
|
||||
errors: [{ messageId: "tooMany", data: { count: 4 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = a || b || c || d || e;",
|
||||
errors: [{ messageId: "tooMany", data: { count: 5 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = a && b || c && d;",
|
||||
errors: [{ messageId: "tooMany", data: { count: 4 } }],
|
||||
},
|
||||
{
|
||||
code: "if (!a && !b && !c && !d) {}",
|
||||
errors: [{ messageId: "tooMany", data: { count: 4 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = a && (b || c);",
|
||||
options: [{ minConditions: 3 }],
|
||||
errors: [{ messageId: "tooMany", data: { count: 3 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = (a && b && c && d) || (e && f && g && h);",
|
||||
errors: [{ messageId: "tooMany", data: { count: 8 } }],
|
||||
},
|
||||
{
|
||||
code: "const x = (a && b && c && d) ?? fallback;",
|
||||
errors: [{ messageId: "tooMany", data: { count: 4 } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue