@sarj/eslint-plugin 5.1.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.cjs +473 -192
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -111
- package/dist/index.d.ts +152 -111
- package/dist/index.js +473 -191
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -8645,6 +8645,261 @@ var no_hand_rolled_sleep_default = ESLintUtils50.RuleCreator(
|
|
|
8645
8645
|
}
|
|
8646
8646
|
});
|
|
8647
8647
|
|
|
8648
|
+
// src/rules/prefer-module-level-schema.ts
|
|
8649
|
+
import {
|
|
8650
|
+
AST_NODE_TYPES as AST_NODE_TYPES41,
|
|
8651
|
+
ESLintUtils as ESLintUtils51
|
|
8652
|
+
} from "@typescript-eslint/utils";
|
|
8653
|
+
var DEFAULT_FACTORIES = [
|
|
8654
|
+
"discriminatedUnion",
|
|
8655
|
+
"intersection",
|
|
8656
|
+
"looseObject",
|
|
8657
|
+
"object",
|
|
8658
|
+
"record",
|
|
8659
|
+
"strictObject",
|
|
8660
|
+
"tuple",
|
|
8661
|
+
"union"
|
|
8662
|
+
];
|
|
8663
|
+
var DEFAULT_MIN_PROPERTIES = 1;
|
|
8664
|
+
var MEMO_CALLEES = /* @__PURE__ */ new Set([
|
|
8665
|
+
"lazy",
|
|
8666
|
+
"memo",
|
|
8667
|
+
"once",
|
|
8668
|
+
"useMemo"
|
|
8669
|
+
]);
|
|
8670
|
+
var TERMINAL_METHODS = /* @__PURE__ */ new Set([
|
|
8671
|
+
"isNullable",
|
|
8672
|
+
"isOptional",
|
|
8673
|
+
"parse",
|
|
8674
|
+
"parseAsync",
|
|
8675
|
+
"safeParse",
|
|
8676
|
+
"safeParseAsync",
|
|
8677
|
+
"spa"
|
|
8678
|
+
]);
|
|
8679
|
+
var FUNCTION_TYPES6 = /* @__PURE__ */ new Set([
|
|
8680
|
+
AST_NODE_TYPES41.ArrowFunctionExpression,
|
|
8681
|
+
AST_NODE_TYPES41.FunctionDeclaration,
|
|
8682
|
+
AST_NODE_TYPES41.FunctionExpression
|
|
8683
|
+
]);
|
|
8684
|
+
function schemaExpression(node) {
|
|
8685
|
+
let current = node;
|
|
8686
|
+
for (; ; ) {
|
|
8687
|
+
const parent = current.parent ?? void 0;
|
|
8688
|
+
if (parent === void 0) {
|
|
8689
|
+
return current;
|
|
8690
|
+
}
|
|
8691
|
+
if (parent.type === AST_NODE_TYPES41.MemberExpression && parent.object === current && !parent.computed && parent.property.type === AST_NODE_TYPES41.Identifier && TERMINAL_METHODS.has(parent.property.name)) {
|
|
8692
|
+
return current;
|
|
8693
|
+
}
|
|
8694
|
+
if (parent.type === AST_NODE_TYPES41.MemberExpression && parent.object === current || parent.type === AST_NODE_TYPES41.CallExpression && parent.callee === current || parent.type === AST_NODE_TYPES41.TSAsExpression && parent.expression === current || parent.type === AST_NODE_TYPES41.TSNonNullExpression && parent.expression === current) {
|
|
8695
|
+
current = parent;
|
|
8696
|
+
continue;
|
|
8697
|
+
}
|
|
8698
|
+
return current;
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
function outermostEnclosingFunction(node) {
|
|
8702
|
+
let outermost;
|
|
8703
|
+
let current = node.parent ?? void 0;
|
|
8704
|
+
while (current !== void 0) {
|
|
8705
|
+
if (FUNCTION_TYPES6.has(current.type)) {
|
|
8706
|
+
outermost = current;
|
|
8707
|
+
}
|
|
8708
|
+
current = current.parent ?? void 0;
|
|
8709
|
+
}
|
|
8710
|
+
return outermost;
|
|
8711
|
+
}
|
|
8712
|
+
function readsReceiver(node) {
|
|
8713
|
+
let found = false;
|
|
8714
|
+
const visit = (value) => {
|
|
8715
|
+
if (found || value === null || typeof value !== "object") {
|
|
8716
|
+
return;
|
|
8717
|
+
}
|
|
8718
|
+
if (Array.isArray(value)) {
|
|
8719
|
+
for (const item of value) {
|
|
8720
|
+
visit(item);
|
|
8721
|
+
}
|
|
8722
|
+
return;
|
|
8723
|
+
}
|
|
8724
|
+
const candidate = value;
|
|
8725
|
+
if (typeof candidate.type !== "string") {
|
|
8726
|
+
return;
|
|
8727
|
+
}
|
|
8728
|
+
if (candidate.type === AST_NODE_TYPES41.ThisExpression || candidate.type === AST_NODE_TYPES41.Super || candidate.type === AST_NODE_TYPES41.Identifier && candidate.name === "arguments") {
|
|
8729
|
+
found = true;
|
|
8730
|
+
return;
|
|
8731
|
+
}
|
|
8732
|
+
for (const key of Object.keys(candidate)) {
|
|
8733
|
+
if (key === "parent" || key === "loc" || key === "range") {
|
|
8734
|
+
continue;
|
|
8735
|
+
}
|
|
8736
|
+
visit(candidate[key]);
|
|
8737
|
+
}
|
|
8738
|
+
};
|
|
8739
|
+
visit(node);
|
|
8740
|
+
return found;
|
|
8741
|
+
}
|
|
8742
|
+
function collectReferences(scope, out) {
|
|
8743
|
+
out.push(...scope.references);
|
|
8744
|
+
for (const child of scope.childScopes) {
|
|
8745
|
+
collectReferences(child, out);
|
|
8746
|
+
}
|
|
8747
|
+
}
|
|
8748
|
+
var prefer_module_level_schema_default = ESLintUtils51.RuleCreator(
|
|
8749
|
+
(name) => `https://github.com/sarj-ai/standards/tree/main/packages/typescript#${name}`
|
|
8750
|
+
)({
|
|
8751
|
+
name: "prefer-module-level-schema",
|
|
8752
|
+
meta: {
|
|
8753
|
+
type: "problem",
|
|
8754
|
+
docs: {
|
|
8755
|
+
description: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function"
|
|
8756
|
+
},
|
|
8757
|
+
schema: [
|
|
8758
|
+
{
|
|
8759
|
+
type: "object",
|
|
8760
|
+
properties: {
|
|
8761
|
+
factories: {
|
|
8762
|
+
type: "array",
|
|
8763
|
+
items: { type: "string" },
|
|
8764
|
+
description: "Zod factory names to check. Defaults to the object-like composites; add `array` / `enum` to widen."
|
|
8765
|
+
},
|
|
8766
|
+
ignoreTestFiles: {
|
|
8767
|
+
type: "boolean",
|
|
8768
|
+
description: "Skip test files, where a fixture schema belongs next to its assertion."
|
|
8769
|
+
},
|
|
8770
|
+
minProperties: {
|
|
8771
|
+
type: "number",
|
|
8772
|
+
minimum: 0,
|
|
8773
|
+
description: "Minimum key count before an object-like schema is reported."
|
|
8774
|
+
}
|
|
8775
|
+
},
|
|
8776
|
+
additionalProperties: false
|
|
8777
|
+
}
|
|
8778
|
+
],
|
|
8779
|
+
messages: {
|
|
8780
|
+
hoistSchema: "Move this `{{factory}}` schema to module scope. It uses nothing from `{{owner}}`, so it is rebuilt on every call for no benefit and cannot be exported, reused, or `z.infer`-ed from where it is."
|
|
8781
|
+
}
|
|
8782
|
+
},
|
|
8783
|
+
defaultOptions: [{}],
|
|
8784
|
+
create(context, [options]) {
|
|
8785
|
+
const factories = new Set(options?.factories ?? DEFAULT_FACTORIES);
|
|
8786
|
+
const ignoreTestFiles = options?.ignoreTestFiles ?? true;
|
|
8787
|
+
const minProperties = options?.minProperties ?? DEFAULT_MIN_PROPERTIES;
|
|
8788
|
+
const sourceCode = context.sourceCode;
|
|
8789
|
+
const filename = context.filename;
|
|
8790
|
+
if (isGeneratedFile(filename, sourceCode.getText())) {
|
|
8791
|
+
return {};
|
|
8792
|
+
}
|
|
8793
|
+
if (ignoreTestFiles && isTestFile(filename)) {
|
|
8794
|
+
return {};
|
|
8795
|
+
}
|
|
8796
|
+
const zodNamespaces = /* @__PURE__ */ new Set();
|
|
8797
|
+
function isZodCall(node) {
|
|
8798
|
+
return node.type === AST_NODE_TYPES41.CallExpression && node.callee.type === AST_NODE_TYPES41.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES41.Identifier && zodNamespaces.has(node.callee.object.name);
|
|
8799
|
+
}
|
|
8800
|
+
function isCovered(node) {
|
|
8801
|
+
let current = node.parent ?? void 0;
|
|
8802
|
+
while (current !== void 0) {
|
|
8803
|
+
if (current !== node && isZodCall(current) && current.callee.type === AST_NODE_TYPES41.MemberExpression && current.callee.property.type === AST_NODE_TYPES41.Identifier && factories.has(current.callee.property.name)) {
|
|
8804
|
+
return true;
|
|
8805
|
+
}
|
|
8806
|
+
if (current.type === AST_NODE_TYPES41.CallExpression && (current.callee.type === AST_NODE_TYPES41.Identifier && MEMO_CALLEES.has(current.callee.name) || current.callee.type === AST_NODE_TYPES41.MemberExpression && !current.callee.computed && current.callee.property.type === AST_NODE_TYPES41.Identifier && MEMO_CALLEES.has(current.callee.property.name))) {
|
|
8807
|
+
return true;
|
|
8808
|
+
}
|
|
8809
|
+
current = current.parent ?? void 0;
|
|
8810
|
+
}
|
|
8811
|
+
return false;
|
|
8812
|
+
}
|
|
8813
|
+
function closesOverNothing(node, enclosing) {
|
|
8814
|
+
const references = [];
|
|
8815
|
+
collectReferences(sourceCode.getScope(node), references);
|
|
8816
|
+
const [schemaStart, schemaEnd] = node.range;
|
|
8817
|
+
const [functionStart, functionEnd] = enclosing.range;
|
|
8818
|
+
for (const reference of references) {
|
|
8819
|
+
const [identifierStart] = reference.identifier.range;
|
|
8820
|
+
if (identifierStart < schemaStart || identifierStart >= schemaEnd) {
|
|
8821
|
+
continue;
|
|
8822
|
+
}
|
|
8823
|
+
const resolved = reference.resolved;
|
|
8824
|
+
if (resolved === null) {
|
|
8825
|
+
continue;
|
|
8826
|
+
}
|
|
8827
|
+
for (const definition of resolved.defs) {
|
|
8828
|
+
if (definition.type === "ImportBinding") {
|
|
8829
|
+
continue;
|
|
8830
|
+
}
|
|
8831
|
+
const [defStart, defEnd] = definition.node.range;
|
|
8832
|
+
if (defStart >= functionStart && defEnd <= functionEnd) {
|
|
8833
|
+
return false;
|
|
8834
|
+
}
|
|
8835
|
+
}
|
|
8836
|
+
}
|
|
8837
|
+
return true;
|
|
8838
|
+
}
|
|
8839
|
+
function ownerName(enclosing) {
|
|
8840
|
+
const parent = enclosing.parent ?? void 0;
|
|
8841
|
+
if (enclosing.type === AST_NODE_TYPES41.FunctionDeclaration && enclosing.id !== null) {
|
|
8842
|
+
return enclosing.id.name;
|
|
8843
|
+
}
|
|
8844
|
+
if (parent !== void 0 && parent.type === AST_NODE_TYPES41.VariableDeclarator && parent.id.type === AST_NODE_TYPES41.Identifier) {
|
|
8845
|
+
return parent.id.name;
|
|
8846
|
+
}
|
|
8847
|
+
if (parent !== void 0 && (parent.type === AST_NODE_TYPES41.MethodDefinition || parent.type === AST_NODE_TYPES41.Property) && parent.key.type === AST_NODE_TYPES41.Identifier) {
|
|
8848
|
+
return parent.key.name;
|
|
8849
|
+
}
|
|
8850
|
+
return "this function";
|
|
8851
|
+
}
|
|
8852
|
+
return {
|
|
8853
|
+
ImportDeclaration(node) {
|
|
8854
|
+
if (!isZodModule(node.source.value)) {
|
|
8855
|
+
return;
|
|
8856
|
+
}
|
|
8857
|
+
for (const specifier of node.specifiers) {
|
|
8858
|
+
if (specifier.type === AST_NODE_TYPES41.ImportNamespaceSpecifier || specifier.type === AST_NODE_TYPES41.ImportDefaultSpecifier || specifier.type === AST_NODE_TYPES41.ImportSpecifier && specifier.imported.type === AST_NODE_TYPES41.Identifier && specifier.imported.name === "z") {
|
|
8859
|
+
zodNamespaces.add(specifier.local.name);
|
|
8860
|
+
}
|
|
8861
|
+
}
|
|
8862
|
+
},
|
|
8863
|
+
CallExpression(node) {
|
|
8864
|
+
if (zodNamespaces.size === 0 || !isZodCall(node)) {
|
|
8865
|
+
return;
|
|
8866
|
+
}
|
|
8867
|
+
const callee = node.callee;
|
|
8868
|
+
if (callee.property.type !== AST_NODE_TYPES41.Identifier) {
|
|
8869
|
+
return;
|
|
8870
|
+
}
|
|
8871
|
+
const factory = callee.property.name;
|
|
8872
|
+
if (!factories.has(factory)) {
|
|
8873
|
+
return;
|
|
8874
|
+
}
|
|
8875
|
+
const enclosing = outermostEnclosingFunction(node);
|
|
8876
|
+
if (enclosing === void 0) {
|
|
8877
|
+
return;
|
|
8878
|
+
}
|
|
8879
|
+
if (isCovered(node)) {
|
|
8880
|
+
return;
|
|
8881
|
+
}
|
|
8882
|
+
const shape = node.arguments[0];
|
|
8883
|
+
if (shape !== void 0 && shape.type === AST_NODE_TYPES41.ObjectExpression && shape.properties.length < minProperties) {
|
|
8884
|
+
return;
|
|
8885
|
+
}
|
|
8886
|
+
const expression = schemaExpression(node);
|
|
8887
|
+
if (readsReceiver(expression)) {
|
|
8888
|
+
return;
|
|
8889
|
+
}
|
|
8890
|
+
if (!closesOverNothing(expression, enclosing)) {
|
|
8891
|
+
return;
|
|
8892
|
+
}
|
|
8893
|
+
context.report({
|
|
8894
|
+
node,
|
|
8895
|
+
messageId: "hoistSchema",
|
|
8896
|
+
data: { factory: `z.${factory}`, owner: ownerName(enclosing) }
|
|
8897
|
+
});
|
|
8898
|
+
}
|
|
8899
|
+
};
|
|
8900
|
+
}
|
|
8901
|
+
});
|
|
8902
|
+
|
|
8648
8903
|
// src/index.ts
|
|
8649
8904
|
var rules = {
|
|
8650
8905
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -8696,205 +8951,232 @@ var rules = {
|
|
|
8696
8951
|
"require-interface-for-injected-service": require_interface_for_injected_service_default,
|
|
8697
8952
|
"strict-test-assertions": strict_test_assertions_default,
|
|
8698
8953
|
"prefer-non-nullable-collection": prefer_non_nullable_collection_default,
|
|
8699
|
-
"no-async-callback-in-waitfor": no_async_callback_in_waitfor_default
|
|
8954
|
+
"no-async-callback-in-waitfor": no_async_callback_in_waitfor_default,
|
|
8955
|
+
"prefer-module-level-schema": prefer_module_level_schema_default
|
|
8956
|
+
};
|
|
8957
|
+
var meta = {
|
|
8958
|
+
name: "@sarj/eslint-plugin",
|
|
8959
|
+
version: "6.1.0"
|
|
8960
|
+
};
|
|
8961
|
+
var recommendedRules = {
|
|
8962
|
+
"@sarj/zod-naming-convention": "warn",
|
|
8963
|
+
"@sarj/require-assert-never": "error",
|
|
8964
|
+
"@sarj/require-zod-form-validation": "error",
|
|
8965
|
+
"@sarj/enforce-file-structure": "warn",
|
|
8966
|
+
"@sarj/no-client-side-data-fetching": "warn",
|
|
8967
|
+
"@sarj/prefer-server-actions": "warn",
|
|
8968
|
+
"@sarj/no-unnecessary-use-client": "warn",
|
|
8969
|
+
"@sarj/prefer-schema-for-api-payload": "warn",
|
|
8970
|
+
// Distilled from sarj-audit skills — warn in recommended, error in strict.
|
|
8971
|
+
"@sarj/no-sentinel-return-on-catch": "warn",
|
|
8972
|
+
"@sarj/no-log-only-catch": "warn",
|
|
8973
|
+
"@sarj/no-insecure-random-id": "warn",
|
|
8974
|
+
"@sarj/no-json-stringify-error": "warn",
|
|
8975
|
+
"@sarj/no-string-concat-in-loop": "warn",
|
|
8976
|
+
"@sarj/prefer-discriminated-union": "warn",
|
|
8977
|
+
"@sarj/no-comment-cruft": "warn",
|
|
8978
|
+
// Frontend / styling — distilled from frontend PR-review mining.
|
|
8979
|
+
"@sarj/prefer-semantic-colors": [
|
|
8980
|
+
"warn",
|
|
8981
|
+
{ requireSemanticTokens: true }
|
|
8982
|
+
],
|
|
8983
|
+
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
8984
|
+
"@sarj/no-fat-try-blocks": "warn",
|
|
8985
|
+
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
8986
|
+
"@sarj/no-secret-in-log": "warn",
|
|
8987
|
+
"@sarj/no-unsafe-mock-casting": "warn",
|
|
8988
|
+
"@sarj/prefer-string-literal-union": "warn",
|
|
8989
|
+
"@sarj/prefer-zod-enum": "warn",
|
|
8990
|
+
// A type hand-written beside the Zod schema it restates drifts the
|
|
8991
|
+
// moment the schema gains a field. 30,759-file, 17-repo sweep
|
|
8992
|
+
// (2026-07): 5 reports, 5 true positives, all in public repos.
|
|
8993
|
+
// `requireIdenticalShape: false` widens it to 8 reports, 1 of them noise.
|
|
8994
|
+
"@sarj/prefer-zod-infer": "warn",
|
|
8995
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
8996
|
+
"@sarj/require-fetch-timeout": "warn",
|
|
8997
|
+
"@sarj/no-silent-promise-catch": "warn",
|
|
8998
|
+
// Second SARJ port wave — the TS/Python parity gap. Each targets a
|
|
8999
|
+
// defect class seen in production Workers code: timing-leaky secret
|
|
9000
|
+
// compares, non-idempotent store writes under queue redelivery,
|
|
9001
|
+
// O(N) pagination, implicit row contracts, flaky timed tests.
|
|
9002
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
9003
|
+
"@sarj/store-insert-requires-on-conflict": "warn",
|
|
9004
|
+
"@sarj/no-offset-pagination": "warn",
|
|
9005
|
+
"@sarj/no-select-star": "warn",
|
|
9006
|
+
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
9007
|
+
// strict config (205 enabled rules) that nothing already reports this
|
|
9008
|
+
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
9009
|
+
"@sarj/no-hand-rolled-sleep": "warn",
|
|
9010
|
+
"@sarj/no-sleep-in-test-body": "warn",
|
|
9011
|
+
"@sarj/no-conditional-in-test": "warn",
|
|
9012
|
+
"@sarj/no-repeated-string-literal": "warn",
|
|
9013
|
+
"@sarj/no-positional-tuple-return": "warn",
|
|
9014
|
+
// Injection guard — low FP, applies to any repo touching SQL.
|
|
9015
|
+
"@sarj/no-dynamic-sql": "warn",
|
|
9016
|
+
// Mined from two years of PR review (SARJ-928). Schema-layer sibling of
|
|
9017
|
+
// `no-enum`; autofixable for inline string-literal objects.
|
|
9018
|
+
"@sarj/no-zod-native-enum": "warn",
|
|
9019
|
+
// Mined from two years of PR review — the single most frequent uncovered
|
|
9020
|
+
// theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
|
|
9021
|
+
// positives, so it is safe to run everywhere.
|
|
9022
|
+
"@sarj/prefer-module-level-constant": "warn",
|
|
9023
|
+
// Anti-comment-verbosity family (2026-07), from a 37,918-comment,
|
|
9024
|
+
// nine-repo measurement study. Each is a deletion-class finding, so each
|
|
9025
|
+
// was validated against pydantic / trio / attrs as well as the maintained
|
|
9026
|
+
// repos: `no-restated-comment` 0 hits in the flagship first-party
|
|
9027
|
+
// repo and 4 in the three famous
|
|
9028
|
+
// corpora combined; `trailing-value-narration` 18 hits, 18 true
|
|
9029
|
+
// positives; `jsdoc-restates-signature` 36 hits, 0 measured false
|
|
9030
|
+
// positives, and it offers a suggestion rather than a `--fix` because a
|
|
9031
|
+
// wrong deletion is silent information loss.
|
|
9032
|
+
"@sarj/no-restated-comment": "warn",
|
|
9033
|
+
"@sarj/jsdoc-restates-signature": "warn",
|
|
9034
|
+
"@sarj/trailing-value-narration": "warn",
|
|
9035
|
+
// The VOLUME arm of the same family (2026-07). Its siblings judge one
|
|
9036
|
+
// comment at a time and can only condemn one that adds nothing; this
|
|
9037
|
+
// one judges a TYPE, so it can report ten rows that each add a word.
|
|
9038
|
+
// 33 OSS TS repos / 46,861 files: 22 findings, all read, 0 false; zero
|
|
9039
|
+
// across ten first-party repos, where the generated-file sniff alone
|
|
9040
|
+
// removed 321 of the 407 raw hits. Measurements and the six false
|
|
9041
|
+
// positives that shaped the guards: docs/rules/no-type-member-comment-wall.md
|
|
9042
|
+
"@sarj/no-type-member-comment-wall": "warn",
|
|
9043
|
+
// The TS half of SARJ057 (2026-07). Python has caught the
|
|
9044
|
+
// assertion-FREE test since 0.15.0 (SARJ043) and had no TS
|
|
9045
|
+
// counterpart, which is how `expect(true).toBe(true); // placeholder`
|
|
9046
|
+
// survived in a first-party repo: the file HAS an assertion.
|
|
9047
|
+
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
9048
|
+
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
9049
|
+
// positives, 0 false positives.
|
|
9050
|
+
"@sarj/no-tautological-expect": "warn",
|
|
9051
|
+
// Substitutability: an exported service class with injected
|
|
9052
|
+
// collaborators and no interface above it can only be tested by
|
|
9053
|
+
// mocking. 11-repo sweep: 229 exported classes, 82% already carry a
|
|
9054
|
+
// port, 29 fire, 28 of them true positives.
|
|
9055
|
+
"@sarj/require-interface-for-injected-service": "warn",
|
|
9056
|
+
"@sarj/prefer-non-nullable-collection": "warn",
|
|
9057
|
+
"@sarj/no-async-callback-in-waitfor": "warn",
|
|
9058
|
+
// A Zod schema built inside a function is rebuilt on every call, every
|
|
9059
|
+
// request, every render. Zod sibling of `prefer-module-level-constant`,
|
|
9060
|
+
// which cannot reach it: that rule's hoist gate requires literal leaves and
|
|
9061
|
+
// `z.object({...})` is a call. 378 reports over 30,546 .ts/.tsx files in 17
|
|
9062
|
+
// repos; the free-variable, chain and `this` gates are what keep the schema
|
|
9063
|
+
// FACTORY case (closes over a parameter, a type parameter, or a local type)
|
|
9064
|
+
// silent.
|
|
9065
|
+
"@sarj/prefer-module-level-schema": "warn",
|
|
9066
|
+
// `strict-test-assertions` shipped in `rules` but in NEITHER preset, so a
|
|
9067
|
+
// consumer using `configs.recommended`/`configs.strict` instead of the shared
|
|
9068
|
+
// `eslint.strict.mjs` had silently never run it. `flat-presets.test.ts`
|
|
9069
|
+
// asserts strict wires every rule the plugin ships, so it cannot recur.
|
|
9070
|
+
"@sarj/strict-test-assertions": "warn"
|
|
9071
|
+
};
|
|
9072
|
+
var strictRules = {
|
|
9073
|
+
"@sarj/zod-naming-convention": "error",
|
|
9074
|
+
"@sarj/require-assert-never": "error",
|
|
9075
|
+
"@sarj/require-zod-form-validation": "error",
|
|
9076
|
+
"@sarj/enforce-file-structure": "error",
|
|
9077
|
+
"@sarj/no-raw-env": "error",
|
|
9078
|
+
"@sarj/no-enum": "error",
|
|
9079
|
+
"@sarj/no-client-side-data-fetching": "error",
|
|
9080
|
+
"@sarj/prefer-server-actions": "error",
|
|
9081
|
+
"@sarj/no-unnecessary-use-client": "error",
|
|
9082
|
+
"@sarj/prefer-schema-for-api-payload": "error",
|
|
9083
|
+
// Distilled from sarj-audit skills.
|
|
9084
|
+
"@sarj/no-sentinel-return-on-catch": "error",
|
|
9085
|
+
"@sarj/no-log-only-catch": "error",
|
|
9086
|
+
"@sarj/no-insecure-random-id": "error",
|
|
9087
|
+
"@sarj/no-json-stringify-error": "error",
|
|
9088
|
+
"@sarj/no-string-concat-in-loop": "error",
|
|
9089
|
+
"@sarj/prefer-discriminated-union": "error",
|
|
9090
|
+
"@sarj/no-comment-cruft": "error",
|
|
9091
|
+
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
9092
|
+
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
9093
|
+
"@sarj/prefer-semantic-colors": [
|
|
9094
|
+
"error",
|
|
9095
|
+
{ requireSemanticTokens: true }
|
|
9096
|
+
],
|
|
9097
|
+
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
9098
|
+
"@sarj/no-fat-try-blocks": "error",
|
|
9099
|
+
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
9100
|
+
"@sarj/no-secret-in-log": "error",
|
|
9101
|
+
"@sarj/no-unsafe-mock-casting": "error",
|
|
9102
|
+
// Promoted to error 2026-07-25 — strict means strict (user directive).
|
|
9103
|
+
"@sarj/prefer-string-literal-union": "error",
|
|
9104
|
+
"@sarj/prefer-zod-enum": "error",
|
|
9105
|
+
// See the `recommended` block for the measured counts.
|
|
9106
|
+
"@sarj/prefer-zod-infer": "error",
|
|
9107
|
+
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
9108
|
+
"@sarj/require-fetch-timeout": "error",
|
|
9109
|
+
"@sarj/no-silent-promise-catch": "error",
|
|
9110
|
+
// Second SARJ port wave — the TS/Python parity gap.
|
|
9111
|
+
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
9112
|
+
"@sarj/store-insert-requires-on-conflict": "error",
|
|
9113
|
+
"@sarj/no-offset-pagination": "error",
|
|
9114
|
+
"@sarj/no-select-star": "error",
|
|
9115
|
+
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
9116
|
+
// strict config (205 enabled rules) that nothing already reports this
|
|
9117
|
+
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
9118
|
+
"@sarj/no-hand-rolled-sleep": "error",
|
|
9119
|
+
"@sarj/no-sleep-in-test-body": "error",
|
|
9120
|
+
"@sarj/no-conditional-in-test": "error",
|
|
9121
|
+
"@sarj/no-repeated-string-literal": "error",
|
|
9122
|
+
// API-shape advice rather than a runtime defect — a corpus sweep found its
|
|
9123
|
+
// only hits are parser `[value, cursor]` returns, which are conventional.
|
|
9124
|
+
// Warn even in strict until a rollout justifies more.
|
|
9125
|
+
"@sarj/no-positional-tuple-return": "error",
|
|
9126
|
+
"@sarj/no-dynamic-sql": "error",
|
|
9127
|
+
// Architectural: both need per-repo config to be meaningful, so they
|
|
9128
|
+
// are strict-only. `no-storage-in-stateless-modules` is a no-op until
|
|
9129
|
+
// its `modules` option names the directories a team declared stateless;
|
|
9130
|
+
// `no-raw-fetch-outside-clients` defaults to the `clients/` convention
|
|
9131
|
+
// and takes an `allow` list for repos that lay their client layer out
|
|
9132
|
+
// differently.
|
|
9133
|
+
"@sarj/no-raw-fetch-outside-clients": "error",
|
|
9134
|
+
"@sarj/no-storage-in-stateless-modules": "error",
|
|
9135
|
+
// Mined from two years of PR review (SARJ-928).
|
|
9136
|
+
"@sarj/no-zod-native-enum": "error",
|
|
9137
|
+
"@sarj/prefer-module-level-constant": "error",
|
|
9138
|
+
// Anti-comment-verbosity family (2026-07) — see the `recommended` block
|
|
9139
|
+
// for the measured hit counts and false-positive rates.
|
|
9140
|
+
"@sarj/no-restated-comment": "error",
|
|
9141
|
+
"@sarj/jsdoc-restates-signature": "error",
|
|
9142
|
+
"@sarj/trailing-value-narration": "error",
|
|
9143
|
+
"@sarj/no-type-member-comment-wall": "error",
|
|
9144
|
+
// TS half of SARJ057 — see the `recommended` block for the measurement.
|
|
9145
|
+
"@sarj/no-tautological-expect": "error",
|
|
9146
|
+
// Substitutability: the TS sibling of the Python `prefer-real-store-in-tests`
|
|
9147
|
+
// / `prefer-library-fake` wave. The convention already exists in the
|
|
9148
|
+
// corpus (175 `implements` clauses vs 29 hits), so strict enforces it.
|
|
9149
|
+
"@sarj/require-interface-for-injected-service": "error",
|
|
9150
|
+
"@sarj/prefer-non-nullable-collection": "error",
|
|
9151
|
+
"@sarj/no-async-callback-in-waitfor": "error",
|
|
9152
|
+
// Zod sibling of `prefer-module-level-constant` -- see `recommended`.
|
|
9153
|
+
"@sarj/prefer-module-level-schema": "error",
|
|
9154
|
+
// See the `recommended` block.
|
|
9155
|
+
"@sarj/strict-test-assertions": "error"
|
|
8700
9156
|
};
|
|
8701
9157
|
var plugin = {
|
|
8702
|
-
meta
|
|
8703
|
-
name: "@sarj/eslint-plugin",
|
|
8704
|
-
version: "5.1.0"
|
|
8705
|
-
},
|
|
9158
|
+
meta,
|
|
8706
9159
|
rules,
|
|
8707
9160
|
configs: {
|
|
8708
|
-
recommended: {
|
|
8709
|
-
|
|
8710
|
-
rules: {
|
|
8711
|
-
"@sarj/zod-naming-convention": "warn",
|
|
8712
|
-
"@sarj/require-assert-never": "error",
|
|
8713
|
-
"@sarj/require-zod-form-validation": "error",
|
|
8714
|
-
"@sarj/enforce-file-structure": "warn",
|
|
8715
|
-
"@sarj/no-client-side-data-fetching": "warn",
|
|
8716
|
-
"@sarj/prefer-server-actions": "warn",
|
|
8717
|
-
"@sarj/no-unnecessary-use-client": "warn",
|
|
8718
|
-
"@sarj/prefer-schema-for-api-payload": "warn",
|
|
8719
|
-
// Distilled from sarj-audit skills — warn in recommended, error in strict.
|
|
8720
|
-
"@sarj/no-sentinel-return-on-catch": "warn",
|
|
8721
|
-
"@sarj/no-log-only-catch": "warn",
|
|
8722
|
-
"@sarj/no-insecure-random-id": "warn",
|
|
8723
|
-
"@sarj/no-json-stringify-error": "warn",
|
|
8724
|
-
"@sarj/no-string-concat-in-loop": "warn",
|
|
8725
|
-
"@sarj/prefer-discriminated-union": "warn",
|
|
8726
|
-
"@sarj/no-comment-cruft": "warn",
|
|
8727
|
-
// Frontend / styling — distilled from frontend PR-review mining.
|
|
8728
|
-
"@sarj/prefer-semantic-colors": [
|
|
8729
|
-
"warn",
|
|
8730
|
-
{ requireSemanticTokens: true }
|
|
8731
|
-
],
|
|
8732
|
-
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
8733
|
-
"@sarj/no-fat-try-blocks": "warn",
|
|
8734
|
-
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
8735
|
-
"@sarj/no-secret-in-log": "warn",
|
|
8736
|
-
"@sarj/no-unsafe-mock-casting": "warn",
|
|
8737
|
-
"@sarj/prefer-string-literal-union": "warn",
|
|
8738
|
-
"@sarj/prefer-zod-enum": "warn",
|
|
8739
|
-
// A type hand-written beside the Zod schema it restates drifts the
|
|
8740
|
-
// moment the schema gains a field. 30,759-file, 17-repo sweep
|
|
8741
|
-
// (2026-07): 5 reports, 5 true positives, all in public repos.
|
|
8742
|
-
// `requireIdenticalShape: false` widens it to 8 reports, 1 of them noise.
|
|
8743
|
-
"@sarj/prefer-zod-infer": "warn",
|
|
8744
|
-
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
8745
|
-
"@sarj/require-fetch-timeout": "warn",
|
|
8746
|
-
"@sarj/no-silent-promise-catch": "warn",
|
|
8747
|
-
// Second SARJ port wave — the TS/Python parity gap. Each targets a
|
|
8748
|
-
// defect class seen in production Workers code: timing-leaky secret
|
|
8749
|
-
// compares, non-idempotent store writes under queue redelivery,
|
|
8750
|
-
// O(N) pagination, implicit row contracts, flaky timed tests.
|
|
8751
|
-
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
8752
|
-
"@sarj/store-insert-requires-on-conflict": "warn",
|
|
8753
|
-
"@sarj/no-offset-pagination": "warn",
|
|
8754
|
-
"@sarj/no-select-star": "warn",
|
|
8755
|
-
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
8756
|
-
// strict config (205 enabled rules) that nothing already reports this
|
|
8757
|
-
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
8758
|
-
"@sarj/no-hand-rolled-sleep": "warn",
|
|
8759
|
-
"@sarj/no-sleep-in-test-body": "warn",
|
|
8760
|
-
"@sarj/no-conditional-in-test": "warn",
|
|
8761
|
-
"@sarj/no-repeated-string-literal": "warn",
|
|
8762
|
-
"@sarj/no-positional-tuple-return": "warn",
|
|
8763
|
-
// Injection guard — low FP, applies to any repo touching SQL.
|
|
8764
|
-
"@sarj/no-dynamic-sql": "warn",
|
|
8765
|
-
// Mined from two years of PR review (SARJ-928). Schema-layer sibling of
|
|
8766
|
-
// `no-enum`; autofixable for inline string-literal objects.
|
|
8767
|
-
"@sarj/no-zod-native-enum": "warn",
|
|
8768
|
-
// Mined from two years of PR review — the single most frequent uncovered
|
|
8769
|
-
// theme (~37 PRs). Measured 17 hits / 1085 real TS files, all true
|
|
8770
|
-
// positives, so it is safe to run everywhere.
|
|
8771
|
-
"@sarj/prefer-module-level-constant": "warn",
|
|
8772
|
-
// Anti-comment-verbosity family (2026-07), from a 37,918-comment,
|
|
8773
|
-
// nine-repo measurement study. Each is a deletion-class finding, so each
|
|
8774
|
-
// was validated against pydantic / trio / attrs as well as the maintained
|
|
8775
|
-
// repos: `no-restated-comment` 0 hits in the flagship first-party
|
|
8776
|
-
// repo and 4 in the three famous
|
|
8777
|
-
// corpora combined; `trailing-value-narration` 18 hits, 18 true
|
|
8778
|
-
// positives; `jsdoc-restates-signature` 36 hits, 0 measured false
|
|
8779
|
-
// positives, and it offers a suggestion rather than a `--fix` because a
|
|
8780
|
-
// wrong deletion is silent information loss.
|
|
8781
|
-
"@sarj/no-restated-comment": "warn",
|
|
8782
|
-
"@sarj/jsdoc-restates-signature": "warn",
|
|
8783
|
-
"@sarj/trailing-value-narration": "warn",
|
|
8784
|
-
// The VOLUME arm of the same family (2026-07). Its siblings judge one
|
|
8785
|
-
// comment at a time and can only condemn one that adds nothing; this
|
|
8786
|
-
// one judges a TYPE, so it can report ten rows that each add a word.
|
|
8787
|
-
// 33 OSS TS repos / 46,861 files: 22 findings, all read, 0 false; zero
|
|
8788
|
-
// across ten first-party repos, where the generated-file sniff alone
|
|
8789
|
-
// removed 321 of the 407 raw hits. Measurements and the six false
|
|
8790
|
-
// positives that shaped the guards: docs/rules/no-type-member-comment-wall.md
|
|
8791
|
-
"@sarj/no-type-member-comment-wall": "warn",
|
|
8792
|
-
// The TS half of SARJ057 (2026-07). Python has caught the
|
|
8793
|
-
// assertion-FREE test since 0.15.0 (SARJ043) and had no TS
|
|
8794
|
-
// counterpart, which is how `expect(true).toBe(true); // placeholder`
|
|
8795
|
-
// survived in a first-party repo: the file HAS an assertion.
|
|
8796
|
-
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
8797
|
-
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
8798
|
-
// positives, 0 false positives.
|
|
8799
|
-
"@sarj/no-tautological-expect": "warn",
|
|
8800
|
-
// Substitutability: an exported service class with injected
|
|
8801
|
-
// collaborators and no interface above it can only be tested by
|
|
8802
|
-
// mocking. 11-repo sweep: 229 exported classes, 82% already carry a
|
|
8803
|
-
// port, 29 fire, 28 of them true positives.
|
|
8804
|
-
"@sarj/require-interface-for-injected-service": "warn",
|
|
8805
|
-
"@sarj/prefer-non-nullable-collection": "warn",
|
|
8806
|
-
"@sarj/no-async-callback-in-waitfor": "warn"
|
|
8807
|
-
}
|
|
8808
|
-
},
|
|
8809
|
-
strict: {
|
|
8810
|
-
plugins: ["@sarj"],
|
|
8811
|
-
rules: {
|
|
8812
|
-
"@sarj/zod-naming-convention": "error",
|
|
8813
|
-
"@sarj/require-assert-never": "error",
|
|
8814
|
-
"@sarj/require-zod-form-validation": "error",
|
|
8815
|
-
"@sarj/enforce-file-structure": "error",
|
|
8816
|
-
"@sarj/no-raw-env": "error",
|
|
8817
|
-
"@sarj/no-enum": "error",
|
|
8818
|
-
"@sarj/no-client-side-data-fetching": "error",
|
|
8819
|
-
"@sarj/prefer-server-actions": "error",
|
|
8820
|
-
"@sarj/no-unnecessary-use-client": "error",
|
|
8821
|
-
"@sarj/prefer-schema-for-api-payload": "error",
|
|
8822
|
-
// Distilled from sarj-audit skills.
|
|
8823
|
-
"@sarj/no-sentinel-return-on-catch": "error",
|
|
8824
|
-
"@sarj/no-log-only-catch": "error",
|
|
8825
|
-
"@sarj/no-insecure-random-id": "error",
|
|
8826
|
-
"@sarj/no-json-stringify-error": "error",
|
|
8827
|
-
"@sarj/no-string-concat-in-loop": "error",
|
|
8828
|
-
"@sarj/prefer-discriminated-union": "error",
|
|
8829
|
-
"@sarj/no-comment-cruft": "error",
|
|
8830
|
-
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
8831
|
-
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
8832
|
-
"@sarj/prefer-semantic-colors": [
|
|
8833
|
-
"error",
|
|
8834
|
-
{ requireSemanticTokens: true }
|
|
8835
|
-
],
|
|
8836
|
-
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
8837
|
-
"@sarj/no-fat-try-blocks": "error",
|
|
8838
|
-
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
8839
|
-
"@sarj/no-secret-in-log": "error",
|
|
8840
|
-
"@sarj/no-unsafe-mock-casting": "error",
|
|
8841
|
-
// Promoted to error 2026-07-25 — strict means strict (user directive).
|
|
8842
|
-
"@sarj/prefer-string-literal-union": "error",
|
|
8843
|
-
"@sarj/prefer-zod-enum": "error",
|
|
8844
|
-
// See the `recommended` block for the measured counts.
|
|
8845
|
-
"@sarj/prefer-zod-infer": "error",
|
|
8846
|
-
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
8847
|
-
"@sarj/require-fetch-timeout": "error",
|
|
8848
|
-
"@sarj/no-silent-promise-catch": "error",
|
|
8849
|
-
// Second SARJ port wave — the TS/Python parity gap.
|
|
8850
|
-
"@sarj/prefer-constant-time-secret-compare": "error",
|
|
8851
|
-
"@sarj/store-insert-requires-on-conflict": "error",
|
|
8852
|
-
"@sarj/no-offset-pagination": "error",
|
|
8853
|
-
"@sarj/no-select-star": "error",
|
|
8854
|
-
// Uncancellable hand-rolled timers. Verified against the shipped
|
|
8855
|
-
// strict config (205 enabled rules) that nothing already reports this
|
|
8856
|
-
// position; `unicorn` 72 has no promisified-timer rule at all.
|
|
8857
|
-
"@sarj/no-hand-rolled-sleep": "error",
|
|
8858
|
-
"@sarj/no-sleep-in-test-body": "error",
|
|
8859
|
-
"@sarj/no-conditional-in-test": "error",
|
|
8860
|
-
"@sarj/no-repeated-string-literal": "error",
|
|
8861
|
-
// API-shape advice rather than a runtime defect — a corpus sweep found its
|
|
8862
|
-
// only hits are parser `[value, cursor]` returns, which are conventional.
|
|
8863
|
-
// Warn even in strict until a rollout justifies more.
|
|
8864
|
-
"@sarj/no-positional-tuple-return": "error",
|
|
8865
|
-
"@sarj/no-dynamic-sql": "error",
|
|
8866
|
-
// Architectural: both need per-repo config to be meaningful, so they
|
|
8867
|
-
// are strict-only. `no-storage-in-stateless-modules` is a no-op until
|
|
8868
|
-
// its `modules` option names the directories a team declared stateless;
|
|
8869
|
-
// `no-raw-fetch-outside-clients` defaults to the `clients/` convention
|
|
8870
|
-
// and takes an `allow` list for repos that lay their client layer out
|
|
8871
|
-
// differently.
|
|
8872
|
-
"@sarj/no-raw-fetch-outside-clients": "error",
|
|
8873
|
-
"@sarj/no-storage-in-stateless-modules": "error",
|
|
8874
|
-
// Mined from two years of PR review (SARJ-928).
|
|
8875
|
-
"@sarj/no-zod-native-enum": "error",
|
|
8876
|
-
"@sarj/prefer-module-level-constant": "error",
|
|
8877
|
-
// Anti-comment-verbosity family (2026-07) — see the `recommended` block
|
|
8878
|
-
// for the measured hit counts and false-positive rates.
|
|
8879
|
-
"@sarj/no-restated-comment": "error",
|
|
8880
|
-
"@sarj/jsdoc-restates-signature": "error",
|
|
8881
|
-
"@sarj/trailing-value-narration": "error",
|
|
8882
|
-
"@sarj/no-type-member-comment-wall": "error",
|
|
8883
|
-
// TS half of SARJ057 — see the `recommended` block for the measurement.
|
|
8884
|
-
"@sarj/no-tautological-expect": "error",
|
|
8885
|
-
// Substitutability: the TS sibling of the Python `prefer-real-store-in-tests`
|
|
8886
|
-
// / `prefer-library-fake` wave. The convention already exists in the
|
|
8887
|
-
// corpus (175 `implements` clauses vs 29 hits), so strict enforces it.
|
|
8888
|
-
"@sarj/require-interface-for-injected-service": "error",
|
|
8889
|
-
"@sarj/prefer-non-nullable-collection": "error",
|
|
8890
|
-
"@sarj/no-async-callback-in-waitfor": "error"
|
|
8891
|
-
}
|
|
8892
|
-
}
|
|
9161
|
+
recommended: {},
|
|
9162
|
+
strict: {}
|
|
8893
9163
|
}
|
|
8894
9164
|
};
|
|
9165
|
+
plugin.configs.recommended = {
|
|
9166
|
+
name: "@sarj/recommended",
|
|
9167
|
+
plugins: { "@sarj": plugin },
|
|
9168
|
+
rules: recommendedRules
|
|
9169
|
+
};
|
|
9170
|
+
plugin.configs.strict = {
|
|
9171
|
+
name: "@sarj/strict",
|
|
9172
|
+
plugins: { "@sarj": plugin },
|
|
9173
|
+
rules: strictRules
|
|
9174
|
+
};
|
|
8895
9175
|
var index_default = plugin;
|
|
8896
9176
|
export {
|
|
8897
9177
|
index_default as default,
|
|
8898
|
-
|
|
9178
|
+
recommendedRules,
|
|
9179
|
+
rules,
|
|
9180
|
+
strictRules
|
|
8899
9181
|
};
|
|
8900
9182
|
//# sourceMappingURL=index.js.map
|