@sarj/eslint-plugin 2.14.0 → 2.16.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 +2 -1
- package/dist/index.cjs +497 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +497 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var GENERATED_MARKER_RE = /(?:@generated\b|generated (?:with|by)|generated (?:gr
|
|
|
9
9
|
function isTestFile(filename) {
|
|
10
10
|
const normalized = filename.replaceAll("\\", "/");
|
|
11
11
|
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
12
|
-
if (
|
|
12
|
+
if (/[.\-_](test|spec|e2e)\.[cm]?[jt]sx?$/.test(base) || /\.integration\.[cm]?[jt]sx?$/.test(base)) {
|
|
13
13
|
return true;
|
|
14
14
|
}
|
|
15
15
|
return /(^|\/)(tests?|__tests__|__mocks__|fixtures|e2e|integration)\//.test(normalized);
|
|
@@ -433,9 +433,9 @@ function restatesStatementHead(body, statement) {
|
|
|
433
433
|
|
|
434
434
|
// src/rules/no-comment-cruft.ts
|
|
435
435
|
var LEADING_PREAMBLE_MIN = 4;
|
|
436
|
-
var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|after(?:wards| that)?|finally|lastly|now)\s*[,:]\s*\S
|
|
436
|
+
var STEP_NARRATION_RE = /^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|after(?:wards| that)?|finally|lastly|now)\s*[,:]\s*\S/i;
|
|
437
437
|
var META_COMMENTARY_RE = /\b(?:for now|keeping (?:it|this) simple|could be (?:refactored|improved|cleaned up|simplified)|refactor(?:ed|ing)? (?:later|this)|not sure (?:if|whether|why|how)|quick[- ](?:and[- ]dirty|fix)|(?:a |bit of a )?hacky|is a hack|temporary (?:solution|workaround|fix|hack)|revisit (?:this|later|below)|clean (?:this|it) up|not ideal|placeholder for now)\b/i;
|
|
438
|
-
var DIRECTIVE_RE = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|
|
|
438
|
+
var DIRECTIVE_RE = /^(eslint\b|eslint-|sarj-noqa\b|@ts-|prettier-ignore|prettier\b|biome-|c8\b|v8\b|istanbul\b|@type\b|@vite|webpack|<reference|<amd|global\b|noinspection|hack\b|xxx\b)/i;
|
|
439
439
|
var LICENSE_RE = /copyright|licen[cs]ed?|spdx|permission is hereby granted|all rights reserved/i;
|
|
440
440
|
var ENUMERATED_ITEM_RE = /^(?:\d+[.):]|[-*•])\s+\S/;
|
|
441
441
|
var ENUMERATED_ITEM_MIN_WORDS = 3;
|
|
@@ -501,7 +501,8 @@ function isSectionLabel(text) {
|
|
|
501
501
|
}
|
|
502
502
|
var HELPER_OPENER_RE = /^(?:a\s+)?helper\s+(?:function|method|component|hook|class|type|util(?:ity)?)\b/i;
|
|
503
503
|
var LETS_RE = /^let'?s\s+(?:not\s+|just\s+|now\s+|first\s+)?(?:add|append|assign|await|build|calculate|call|check|clear|close|compute|convert|copy|count|create|declare|decrement|define|delete|extract|fetch|filter|find|format|generate|get|handle|increment|init|initialise|initialize|insert|iterate|join|load|log|loop|map|merge|open|parse|print|process|push|read|remove|render|reset|return|save|send|set|setup|sort|split|start|stop|store|update|validate|wrap|write)(?:s|es|ed|ing)?\b/i;
|
|
504
|
-
var ENUMERATION_RE = /^(?:\d+[.)]\s+\S|phase\s+\d+\b)/i;
|
|
504
|
+
var ENUMERATION_RE = /^(?:\d+[.)]\s+\S|(?:phase|step)\s+\d+\b)/i;
|
|
505
|
+
var DUMMY_TRANSLATION_RE = /^(?:increment|return|returns|get|gets|set\b(?! up\b)|sets\b(?! up\b)|function to|method to)\b/i;
|
|
505
506
|
var DIAGRAM_ARROW_RE = /[-=~]{2,}>|<[-=~]{2,}/;
|
|
506
507
|
var CODE_KEYWORD_RE = /^(import |export |const |let |var |function\b|class |interface |type \w|enum |return\b|throw |await |async |if\s*\(|for\s*\(|while\s*\(|switch\s*\(|new |console\.)/;
|
|
507
508
|
var CODE_TAIL_RE = /[;{}()]\s*$|=>\s*$|,\s*$/;
|
|
@@ -549,6 +550,14 @@ function isRedundantNarration(body, statementBelow, standalone, isolatedEnumerat
|
|
|
549
550
|
if (STEP_NARRATION_RE.test(t)) return true;
|
|
550
551
|
if (META_COMMENTARY_RE.test(t) && !JUSTIFICATION_RE.test(t)) return true;
|
|
551
552
|
if (HELPER_OPENER_RE.test(t) || LETS_RE.test(t)) return true;
|
|
553
|
+
const words = t.split(/\s+/);
|
|
554
|
+
if (words.length <= 4 && DUMMY_TRANSLATION_RE.test(t) && !/[():=]/.test(t)) {
|
|
555
|
+
const lowerT = t.toLowerCase();
|
|
556
|
+
const rationaleWords = ["when", "because", "if", "so that", "due to", "for", "instead of", "to prevent", "to avoid", "only"];
|
|
557
|
+
if (!rationaleWords.some((w) => lowerT.includes(w))) {
|
|
558
|
+
if (words.length > 1) return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
552
561
|
if (!nested && isSectionLabel(t)) return true;
|
|
553
562
|
if (isolatedEnumeration && ENUMERATION_RE.test(t)) return true;
|
|
554
563
|
}
|
|
@@ -616,7 +625,8 @@ var no_comment_cruft_default = ESLintUtils3.RuleCreator(
|
|
|
616
625
|
commentedOutCode: "Commented-out code \u2014 delete it; git history remembers.",
|
|
617
626
|
sectionBanner: "Section-banner / region comment \u2014 structure code with functions, not ASCII rules.",
|
|
618
627
|
fileHeaderPreamble: "File-header comment preamble \u2014 use a brief doc comment for the why, not a block of `//` lines.",
|
|
619
|
-
redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting."
|
|
628
|
+
redundantNarration: "Comment narrates the code \u2014 delete it or say *why*, not *what*. Code is self-documenting.",
|
|
629
|
+
untrackedTodo: "Untracked TODO/FIXME marker \u2014 add an issue ticket or context link."
|
|
620
630
|
}
|
|
621
631
|
},
|
|
622
632
|
defaultOptions: [],
|
|
@@ -670,6 +680,16 @@ var no_comment_cruft_default = ESLintUtils3.RuleCreator(
|
|
|
670
680
|
if (isJsDoc(comment) || !isStandalone(comment)) continue;
|
|
671
681
|
if (LICENSE_RE.test(comment.value)) continue;
|
|
672
682
|
const texts = comment.value.split("\n").map(stripCommentMarker).filter((l) => l.length > 0 && !isDirective(l));
|
|
683
|
+
const firstText = texts[0];
|
|
684
|
+
if (firstText !== void 0 && /^(?:todo|fixme)\b/i.test(firstText)) {
|
|
685
|
+
if (!runCitesAReference(comments, i)) {
|
|
686
|
+
context.report({
|
|
687
|
+
node: comment,
|
|
688
|
+
messageId: "untrackedTodo"
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
673
693
|
if (texts.some(isBanner)) {
|
|
674
694
|
context.report({ node: comment, messageId: "sectionBanner" });
|
|
675
695
|
continue;
|
|
@@ -5134,8 +5154,8 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
|
|
|
5134
5154
|
if (base.endsWith(".d.ts")) return {};
|
|
5135
5155
|
if (TEST_FILE_RE.test(base)) return {};
|
|
5136
5156
|
if (isTestFile(context.filename)) return {};
|
|
5137
|
-
const
|
|
5138
|
-
if (!JUNK_DRAWER_STEMS.has(
|
|
5157
|
+
const stem3 = stemOf(base);
|
|
5158
|
+
if (!JUNK_DRAWER_STEMS.has(stem3.toLowerCase())) return {};
|
|
5139
5159
|
return {
|
|
5140
5160
|
Program(node) {
|
|
5141
5161
|
const { names, hasReExport, candidate } = summarizeExports(node.body);
|
|
@@ -5143,11 +5163,11 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
|
|
|
5143
5163
|
if (names !== 1 || candidate === null) return;
|
|
5144
5164
|
if (CONVENTIONAL_BUCKET_EXPORTS.has(candidate.name)) return;
|
|
5145
5165
|
const expected = kebabCase2(candidate.name);
|
|
5146
|
-
if (
|
|
5166
|
+
if (stem3 === expected) return;
|
|
5147
5167
|
context.report({
|
|
5148
5168
|
node: candidate.node,
|
|
5149
5169
|
messageId: "renameJunkDrawer",
|
|
5150
|
-
data: { stem:
|
|
5170
|
+
data: { stem: stem3, name: candidate.name, expected }
|
|
5151
5171
|
});
|
|
5152
5172
|
}
|
|
5153
5173
|
};
|
|
@@ -5301,7 +5321,7 @@ function createSqlListener(handler) {
|
|
|
5301
5321
|
}
|
|
5302
5322
|
|
|
5303
5323
|
// src/rules/no-offset-pagination.ts
|
|
5304
|
-
var OFFSET_PAGINATION = /\bOFFSET\s+(
|
|
5324
|
+
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5305
5325
|
var OFFSET_GATE = /offset/i;
|
|
5306
5326
|
var no_offset_pagination_default = ESLintUtils30.RuleCreator(
|
|
5307
5327
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
@@ -5493,7 +5513,6 @@ var no_positional_tuple_return_default = ESLintUtils31.RuleCreator(
|
|
|
5493
5513
|
// src/rules/no-repeated-string-literal.ts
|
|
5494
5514
|
import { AST_NODE_TYPES as AST_NODE_TYPES23, ESLintUtils as ESLintUtils32 } from "@typescript-eslint/utils";
|
|
5495
5515
|
var MIN_LENGTH = 40;
|
|
5496
|
-
var MIN_OCCURRENCES = 3;
|
|
5497
5516
|
var MIN_DISTINCT_SCOPES = 2;
|
|
5498
5517
|
var PREVIEW_LENGTH = 40;
|
|
5499
5518
|
var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
|
|
@@ -5575,9 +5594,6 @@ var no_repeated_string_literal_default = ESLintUtils32.RuleCreator(
|
|
|
5575
5594
|
},
|
|
5576
5595
|
"Program:exit": () => {
|
|
5577
5596
|
for (const [value, nodes] of occurrences) {
|
|
5578
|
-
if (nodes.length < MIN_OCCURRENCES) {
|
|
5579
|
-
continue;
|
|
5580
|
-
}
|
|
5581
5597
|
const distinctScopes = new Set(
|
|
5582
5598
|
nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
|
|
5583
5599
|
);
|
|
@@ -7258,6 +7274,442 @@ var no_tautological_expect_default = ESLintUtils45.RuleCreator(
|
|
|
7258
7274
|
}
|
|
7259
7275
|
});
|
|
7260
7276
|
|
|
7277
|
+
// src/rules/require-interface-for-injected-service.ts
|
|
7278
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES33, ESLintUtils as ESLintUtils46 } from "@typescript-eslint/utils";
|
|
7279
|
+
var CONFIGISH_TYPE_RE = /(?:Options|Opts|Config|Configuration|Settings|Params|Props|Args|Env|Environment|Callbacks|Flags)$/;
|
|
7280
|
+
var CONFIGISH_NAME_RE = /^(?:options|opts|config|configuration|settings|params|props|args|env|environment|callbacks|flags|logger|log|clock)$/i;
|
|
7281
|
+
var HTTP_TRANSPORT_TYPE_RE = /^(?:KyInstance|AxiosInstance|Session)$/;
|
|
7282
|
+
var TRANSPORT_WRAPPER_NAME_RE = /Client$/;
|
|
7283
|
+
var ROUTER_FACTORY_NAME = "Router";
|
|
7284
|
+
var FRAMEWORK_HTTP_TYPES = /* @__PURE__ */ new Set(["Request", "Response", "NextFunction"]);
|
|
7285
|
+
var isExportedClass = (node) => node.parent.type === AST_NODE_TYPES33.ExportNamedDeclaration || node.parent.type === AST_NODE_TYPES33.ExportDefaultDeclaration;
|
|
7286
|
+
var qualifiedName = (name) => name.type === AST_NODE_TYPES33.Identifier ? name.name : name.type === AST_NODE_TYPES33.TSQualifiedName ? `${qualifiedName(name.left)}.${name.right.name}` : "";
|
|
7287
|
+
var typeReferenceName = (annotated) => {
|
|
7288
|
+
let target = annotated;
|
|
7289
|
+
if (target.type === AST_NODE_TYPES33.TSParameterProperty) target = target.parameter;
|
|
7290
|
+
if (target.type === AST_NODE_TYPES33.AssignmentPattern) target = target.left;
|
|
7291
|
+
if (target.type !== AST_NODE_TYPES33.Identifier) return null;
|
|
7292
|
+
const annotation = target.typeAnnotation?.typeAnnotation;
|
|
7293
|
+
if (annotation === void 0 || annotation.type !== AST_NODE_TYPES33.TSTypeReference) {
|
|
7294
|
+
return null;
|
|
7295
|
+
}
|
|
7296
|
+
const { typeName } = annotation;
|
|
7297
|
+
const rightmost = typeName.type === AST_NODE_TYPES33.Identifier ? typeName.name : typeName.type === AST_NODE_TYPES33.TSQualifiedName ? typeName.right.name : null;
|
|
7298
|
+
if (rightmost === null) return null;
|
|
7299
|
+
return { name: target.name, typeName: rightmost, display: qualifiedName(typeName) };
|
|
7300
|
+
};
|
|
7301
|
+
var readConstructor = (ctor) => {
|
|
7302
|
+
const body = ctor.value.body;
|
|
7303
|
+
const storedFrom = /* @__PURE__ */ new Set();
|
|
7304
|
+
let constructedFields = 0;
|
|
7305
|
+
if (body !== null && body !== void 0) {
|
|
7306
|
+
for (const statement of body.body) {
|
|
7307
|
+
if (statement.type !== AST_NODE_TYPES33.ExpressionStatement) continue;
|
|
7308
|
+
const expression = statement.expression;
|
|
7309
|
+
if (expression.type !== AST_NODE_TYPES33.AssignmentExpression || expression.operator !== "=" || expression.left.type !== AST_NODE_TYPES33.MemberExpression || expression.left.object.type !== AST_NODE_TYPES33.ThisExpression) {
|
|
7310
|
+
continue;
|
|
7311
|
+
}
|
|
7312
|
+
const source = expression.right;
|
|
7313
|
+
if (source.type === AST_NODE_TYPES33.NewExpression) {
|
|
7314
|
+
constructedFields += 1;
|
|
7315
|
+
} else if (source.type === AST_NODE_TYPES33.Identifier) {
|
|
7316
|
+
storedFrom.add(source.name);
|
|
7317
|
+
} else if (source.type === AST_NODE_TYPES33.MemberExpression && source.object.type === AST_NODE_TYPES33.Identifier) {
|
|
7318
|
+
storedFrom.add(source.object.name);
|
|
7319
|
+
}
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
const collaborators = [];
|
|
7323
|
+
for (const parameter of ctor.value.params) {
|
|
7324
|
+
const reference = typeReferenceName(parameter);
|
|
7325
|
+
if (reference === null) continue;
|
|
7326
|
+
const stored = parameter.type === AST_NODE_TYPES33.TSParameterProperty || storedFrom.has(reference.name);
|
|
7327
|
+
if (!stored) continue;
|
|
7328
|
+
if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
|
|
7329
|
+
if (CONFIGISH_NAME_RE.test(reference.name)) continue;
|
|
7330
|
+
collaborators.push(reference);
|
|
7331
|
+
}
|
|
7332
|
+
return { collaborators, constructedFields };
|
|
7333
|
+
};
|
|
7334
|
+
var subtreeHas = (root, found) => {
|
|
7335
|
+
let hit = false;
|
|
7336
|
+
const visit = (current) => {
|
|
7337
|
+
if (hit) return;
|
|
7338
|
+
if (found(current)) {
|
|
7339
|
+
hit = true;
|
|
7340
|
+
return;
|
|
7341
|
+
}
|
|
7342
|
+
for (const key of Object.keys(current)) {
|
|
7343
|
+
if (key === "parent") continue;
|
|
7344
|
+
const value = current[key];
|
|
7345
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
7346
|
+
if (child !== null && typeof child === "object" && typeof child.type === "string") {
|
|
7347
|
+
visit(child);
|
|
7348
|
+
}
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
};
|
|
7352
|
+
visit(root);
|
|
7353
|
+
return hit;
|
|
7354
|
+
};
|
|
7355
|
+
var isFrameworkWiring = (body) => subtreeHas(body, (node) => {
|
|
7356
|
+
if (node.type === AST_NODE_TYPES33.CallExpression) {
|
|
7357
|
+
const { callee } = node;
|
|
7358
|
+
if (callee.type === AST_NODE_TYPES33.Identifier) return callee.name === ROUTER_FACTORY_NAME;
|
|
7359
|
+
return callee.type === AST_NODE_TYPES33.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES33.Identifier && callee.property.name === ROUTER_FACTORY_NAME;
|
|
7360
|
+
}
|
|
7361
|
+
return node.type === AST_NODE_TYPES33.TSTypeReference && node.typeName.type === AST_NODE_TYPES33.TSQualifiedName && FRAMEWORK_HTTP_TYPES.has(node.typeName.right.name);
|
|
7362
|
+
});
|
|
7363
|
+
var stem2 = (name) => name.replace(/^I(?=[A-Z])/, "").replace(/Impl$/, "");
|
|
7364
|
+
var fileInterfaceNames = (program) => {
|
|
7365
|
+
const names = [];
|
|
7366
|
+
for (const statement of program.body) {
|
|
7367
|
+
const declaration = statement.type === AST_NODE_TYPES33.ExportNamedDeclaration ? statement.declaration : statement;
|
|
7368
|
+
if (declaration?.type === AST_NODE_TYPES33.TSInterfaceDeclaration) names.push(declaration.id.name);
|
|
7369
|
+
}
|
|
7370
|
+
return names;
|
|
7371
|
+
};
|
|
7372
|
+
var isTransportWrapper = (className, collaborators, program) => {
|
|
7373
|
+
const [only] = collaborators;
|
|
7374
|
+
if (collaborators.length !== 1 || only === void 0) return false;
|
|
7375
|
+
if (!HTTP_TRANSPORT_TYPE_RE.test(only.typeName)) return false;
|
|
7376
|
+
if (!TRANSPORT_WRAPPER_NAME_RE.test(className)) return false;
|
|
7377
|
+
const target = stem2(className);
|
|
7378
|
+
return !fileInterfaceNames(program).some((name) => {
|
|
7379
|
+
const other = stem2(name);
|
|
7380
|
+
return target.endsWith(other) || other.endsWith(target);
|
|
7381
|
+
});
|
|
7382
|
+
};
|
|
7383
|
+
var publicMethodNames = (body) => {
|
|
7384
|
+
const names = [];
|
|
7385
|
+
for (const member of body.body) {
|
|
7386
|
+
if (member.type !== AST_NODE_TYPES33.MethodDefinition) continue;
|
|
7387
|
+
if (member.kind !== "method" || member.static) continue;
|
|
7388
|
+
if (member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
7389
|
+
if (member.key.type === AST_NODE_TYPES33.PrivateIdentifier) continue;
|
|
7390
|
+
if (member.key.type === AST_NODE_TYPES33.Identifier) names.push(member.key.name);
|
|
7391
|
+
else names.push("\u2026");
|
|
7392
|
+
}
|
|
7393
|
+
return names;
|
|
7394
|
+
};
|
|
7395
|
+
var require_interface_for_injected_service_default = ESLintUtils46.RuleCreator(
|
|
7396
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7397
|
+
)({
|
|
7398
|
+
name: "require-interface-for-injected-service",
|
|
7399
|
+
meta: {
|
|
7400
|
+
type: "suggestion",
|
|
7401
|
+
docs: {
|
|
7402
|
+
description: "An exported service class with constructor-injected collaborators must implement an interface, so consumers depend on a port they can substitute instead of mocking the class."
|
|
7403
|
+
},
|
|
7404
|
+
schema: [],
|
|
7405
|
+
messages: {
|
|
7406
|
+
requireInterface: "`{{name}}` stores injected collaborator(s) ({{deps}}) but implements no interface, so every consumer depends on this concrete class and can only be tested by mocking it. Declare an interface with its public method signature(s) ({{methods}}) and `class {{name}} implements <Interface>`."
|
|
7407
|
+
}
|
|
7408
|
+
},
|
|
7409
|
+
defaultOptions: [],
|
|
7410
|
+
create(context) {
|
|
7411
|
+
const { filename } = context;
|
|
7412
|
+
if (isTestFile(filename) || isStoryFile(filename) || isScriptFile(filename)) return {};
|
|
7413
|
+
if (isGeneratedFile(filename, context.sourceCode.getText())) return {};
|
|
7414
|
+
return {
|
|
7415
|
+
ClassDeclaration(node) {
|
|
7416
|
+
if (node.id === null) return;
|
|
7417
|
+
if (!isExportedClass(node)) return;
|
|
7418
|
+
if (node.abstract === true) return;
|
|
7419
|
+
if (node.superClass !== null) return;
|
|
7420
|
+
if (node.implements.length > 0) return;
|
|
7421
|
+
if (node.decorators.length > 0) return;
|
|
7422
|
+
const ctor = node.body.body.find(
|
|
7423
|
+
(member) => member.type === AST_NODE_TYPES33.MethodDefinition && member.kind === "constructor"
|
|
7424
|
+
);
|
|
7425
|
+
if (ctor === void 0) return;
|
|
7426
|
+
const { collaborators, constructedFields } = readConstructor(ctor);
|
|
7427
|
+
if (collaborators.length === 0) return;
|
|
7428
|
+
if (constructedFields > collaborators.length) return;
|
|
7429
|
+
if (isFrameworkWiring(node.body)) return;
|
|
7430
|
+
if (isTransportWrapper(node.id.name, collaborators, context.sourceCode.ast)) return;
|
|
7431
|
+
const methods = publicMethodNames(node.body);
|
|
7432
|
+
if (methods.length === 0) return;
|
|
7433
|
+
context.report({
|
|
7434
|
+
node: node.id,
|
|
7435
|
+
messageId: "requireInterface",
|
|
7436
|
+
data: {
|
|
7437
|
+
name: node.id.name,
|
|
7438
|
+
deps: collaborators.map((c) => `${c.name}: ${c.display}`).join(", "),
|
|
7439
|
+
methods: methods.join(", ")
|
|
7440
|
+
}
|
|
7441
|
+
});
|
|
7442
|
+
}
|
|
7443
|
+
};
|
|
7444
|
+
}
|
|
7445
|
+
});
|
|
7446
|
+
|
|
7447
|
+
// src/rules/prefer-non-nullable-collection.ts
|
|
7448
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES34, ESLintUtils as ESLintUtils47 } from "@typescript-eslint/utils";
|
|
7449
|
+
var ARRAY_TYPE_NAMES = /* @__PURE__ */ new Set(["Array", "ReadonlyArray"]);
|
|
7450
|
+
function propertyName(node) {
|
|
7451
|
+
const key = node.key;
|
|
7452
|
+
if (key.type === AST_NODE_TYPES34.Identifier) return key.name;
|
|
7453
|
+
if (key.type === AST_NODE_TYPES34.Literal) return String(key.value);
|
|
7454
|
+
return "collection";
|
|
7455
|
+
}
|
|
7456
|
+
function isArrayType(node) {
|
|
7457
|
+
if (node.type === AST_NODE_TYPES34.TSArrayType) return true;
|
|
7458
|
+
return node.type === AST_NODE_TYPES34.TSTypeReference && node.typeName.type === AST_NODE_TYPES34.Identifier && ARRAY_TYPE_NAMES.has(node.typeName.name);
|
|
7459
|
+
}
|
|
7460
|
+
function isNullishType(node) {
|
|
7461
|
+
return node.type === AST_NODE_TYPES34.TSNullKeyword || node.type === AST_NODE_TYPES34.TSUndefinedKeyword;
|
|
7462
|
+
}
|
|
7463
|
+
function isNullableArrayOnly(node) {
|
|
7464
|
+
const values = node.types.filter((member) => !isNullishType(member));
|
|
7465
|
+
return values.length > 0 && values.length < node.types.length && values.every(isArrayType);
|
|
7466
|
+
}
|
|
7467
|
+
var prefer_non_nullable_collection_default = ESLintUtils47.RuleCreator(
|
|
7468
|
+
(name) => `https://github.com/sarj-ai/standards/tree/main/packages/typescript#${name}`
|
|
7469
|
+
)({
|
|
7470
|
+
name: "prefer-non-nullable-collection",
|
|
7471
|
+
meta: {
|
|
7472
|
+
type: "suggestion",
|
|
7473
|
+
docs: {
|
|
7474
|
+
description: "Require array types to use an empty array instead of explicit nullish unions with two equivalent empty states."
|
|
7475
|
+
},
|
|
7476
|
+
schema: [],
|
|
7477
|
+
messages: {
|
|
7478
|
+
preferNonNullableCollection: "`{{name}}` is a nullable array, so nullish and `[]` represent the same empty collection. Use a non-null array and default omitted values to `[]`."
|
|
7479
|
+
}
|
|
7480
|
+
},
|
|
7481
|
+
defaultOptions: [],
|
|
7482
|
+
create(context) {
|
|
7483
|
+
const normalizedFilename = context.filename.replaceAll("\\", "/");
|
|
7484
|
+
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || /\/(?:vendor|vendored)\//u.test(normalizedFilename)) {
|
|
7485
|
+
return {};
|
|
7486
|
+
}
|
|
7487
|
+
function checkOptionalProperty(node) {
|
|
7488
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
7489
|
+
if (annotation === void 0) return;
|
|
7490
|
+
if (annotation.type !== AST_NODE_TYPES34.TSUnionType || !isNullableArrayOnly(annotation)) {
|
|
7491
|
+
return;
|
|
7492
|
+
}
|
|
7493
|
+
context.report({
|
|
7494
|
+
node,
|
|
7495
|
+
messageId: "preferNonNullableCollection",
|
|
7496
|
+
data: { name: propertyName(node) }
|
|
7497
|
+
});
|
|
7498
|
+
}
|
|
7499
|
+
return {
|
|
7500
|
+
TSPropertySignature: checkOptionalProperty,
|
|
7501
|
+
PropertyDefinition: checkOptionalProperty,
|
|
7502
|
+
TSTypeAliasDeclaration(node) {
|
|
7503
|
+
if (node.typeAnnotation.type !== AST_NODE_TYPES34.TSUnionType) return;
|
|
7504
|
+
if (!isNullableArrayOnly(node.typeAnnotation)) return;
|
|
7505
|
+
context.report({
|
|
7506
|
+
node,
|
|
7507
|
+
messageId: "preferNonNullableCollection",
|
|
7508
|
+
data: { name: node.id.name }
|
|
7509
|
+
});
|
|
7510
|
+
}
|
|
7511
|
+
};
|
|
7512
|
+
}
|
|
7513
|
+
});
|
|
7514
|
+
|
|
7515
|
+
// src/rules/primary-export-file-name.ts
|
|
7516
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES35, ESLintUtils as ESLintUtils48 } from "@typescript-eslint/utils";
|
|
7517
|
+
var CONVENTIONAL_BUCKET_EXPORTS2 = /* @__PURE__ */ new Set(["cn"]);
|
|
7518
|
+
var GENERIC_ENTRYPOINTS = /* @__PURE__ */ new Set(["main", "run", "cli", "setup", "teardown", "execute"]);
|
|
7519
|
+
var ACRONYM_OVERRIDES2 = [
|
|
7520
|
+
[/OAuth/g, "Oauth"],
|
|
7521
|
+
[/GraphQL/g, "Graphql"],
|
|
7522
|
+
[/gRPC/g, "Grpc"]
|
|
7523
|
+
];
|
|
7524
|
+
var CAMEL_BOUNDARY_RE2 = /(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/g;
|
|
7525
|
+
var basename2 = (filename) => filename.split(/[/\\]/).pop() ?? filename;
|
|
7526
|
+
var stemOf2 = (base) => base.replace(/\.(test|spec|config|d|stories)\.[cm]?[jt]sx?$/i, "").replace(/\.[cm]?[jt]sx?$/i, "");
|
|
7527
|
+
var kebabCase3 = (name) => {
|
|
7528
|
+
let normalized = name;
|
|
7529
|
+
for (const [pattern, replacement] of ACRONYM_OVERRIDES2) {
|
|
7530
|
+
normalized = normalized.replace(pattern, replacement);
|
|
7531
|
+
}
|
|
7532
|
+
return normalized.replace(CAMEL_BOUNDARY_RE2, "-").toLowerCase();
|
|
7533
|
+
};
|
|
7534
|
+
var isFunctionOrClass = (node) => node.type === AST_NODE_TYPES35.ArrowFunctionExpression || node.type === AST_NODE_TYPES35.FunctionExpression;
|
|
7535
|
+
var findPrimaryExport = (body) => {
|
|
7536
|
+
let exportCount = 0;
|
|
7537
|
+
let primaryCandidate = null;
|
|
7538
|
+
for (const statement of body) {
|
|
7539
|
+
if (statement.type === AST_NODE_TYPES35.ExportAllDeclaration) {
|
|
7540
|
+
return null;
|
|
7541
|
+
}
|
|
7542
|
+
if (statement.type === AST_NODE_TYPES35.ExportDefaultDeclaration) {
|
|
7543
|
+
exportCount += 1;
|
|
7544
|
+
const decl = statement.declaration;
|
|
7545
|
+
if ((decl.type === AST_NODE_TYPES35.FunctionDeclaration || decl.type === AST_NODE_TYPES35.ClassDeclaration) && decl.id !== null) {
|
|
7546
|
+
primaryCandidate = { name: decl.id.name, node: statement, isDefault: true };
|
|
7547
|
+
} else if (decl.type === AST_NODE_TYPES35.Identifier) {
|
|
7548
|
+
primaryCandidate = { name: decl.name, node: statement, isDefault: true };
|
|
7549
|
+
}
|
|
7550
|
+
} else if (statement.type === AST_NODE_TYPES35.ExportNamedDeclaration) {
|
|
7551
|
+
if (statement.source !== null) {
|
|
7552
|
+
return null;
|
|
7553
|
+
}
|
|
7554
|
+
const decl = statement.declaration;
|
|
7555
|
+
if (decl === null) {
|
|
7556
|
+
exportCount += statement.specifiers.length;
|
|
7557
|
+
if (statement.specifiers.length === 1 && primaryCandidate === null) {
|
|
7558
|
+
const spec = statement.specifiers[0];
|
|
7559
|
+
if (spec && spec.exported.type === AST_NODE_TYPES35.Identifier) {
|
|
7560
|
+
primaryCandidate = { name: spec.exported.name, node: statement, isDefault: false };
|
|
7561
|
+
}
|
|
7562
|
+
}
|
|
7563
|
+
} else if (decl.type === AST_NODE_TYPES35.FunctionDeclaration || decl.type === AST_NODE_TYPES35.ClassDeclaration) {
|
|
7564
|
+
if (decl.id !== null) {
|
|
7565
|
+
exportCount += 1;
|
|
7566
|
+
if (primaryCandidate === null) {
|
|
7567
|
+
primaryCandidate = { name: decl.id.name, node: statement, isDefault: false };
|
|
7568
|
+
}
|
|
7569
|
+
}
|
|
7570
|
+
} else if (decl.type === AST_NODE_TYPES35.VariableDeclaration) {
|
|
7571
|
+
for (const declarator of decl.declarations) {
|
|
7572
|
+
if (declarator.id.type === AST_NODE_TYPES35.Identifier) {
|
|
7573
|
+
exportCount += 1;
|
|
7574
|
+
if (primaryCandidate === null && declarator.init && isFunctionOrClass(declarator.init)) {
|
|
7575
|
+
primaryCandidate = { name: declarator.id.name, node: statement, isDefault: false };
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7579
|
+
} else if (decl.type === AST_NODE_TYPES35.TSTypeAliasDeclaration || decl.type === AST_NODE_TYPES35.TSInterfaceDeclaration) {
|
|
7580
|
+
exportCount += 1;
|
|
7581
|
+
if (primaryCandidate === null) {
|
|
7582
|
+
primaryCandidate = { name: decl.id.name, node: statement, isDefault: false };
|
|
7583
|
+
}
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
}
|
|
7587
|
+
if (primaryCandidate === null || exportCount > 2) {
|
|
7588
|
+
return null;
|
|
7589
|
+
}
|
|
7590
|
+
return { ...primaryCandidate, count: exportCount };
|
|
7591
|
+
};
|
|
7592
|
+
var primary_export_file_name_default = ESLintUtils48.RuleCreator(
|
|
7593
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7594
|
+
)({
|
|
7595
|
+
name: "primary-export-file-name",
|
|
7596
|
+
meta: {
|
|
7597
|
+
type: "suggestion",
|
|
7598
|
+
docs: {
|
|
7599
|
+
description: "Enforce semantic naming alignment: a file with a single primary export must be named after that export."
|
|
7600
|
+
},
|
|
7601
|
+
schema: [],
|
|
7602
|
+
messages: {
|
|
7603
|
+
primaryExportMismatch: "Module stem '{{stem}}' does not match its primary export '{{name}}' \u2014 rename the file to '{{expected}}{{ext}}' to describe its responsibility."
|
|
7604
|
+
}
|
|
7605
|
+
},
|
|
7606
|
+
defaultOptions: [],
|
|
7607
|
+
create(context) {
|
|
7608
|
+
const base = basename2(context.filename);
|
|
7609
|
+
if (base.endsWith(".d.ts") || base.startsWith("index.") || base.startsWith("_")) return {};
|
|
7610
|
+
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text)) return {};
|
|
7611
|
+
const stem3 = stemOf2(base);
|
|
7612
|
+
if (stem3 === "page" || stem3 === "layout" || stem3 === "loading" || stem3 === "error" || stem3 === "not-found" || stem3 === "route" || stem3 === "__root" || stem3 === "config" || stem3 === "constants" || stem3 === "types" || stem3.startsWith("$") || stem3.startsWith("[")) {
|
|
7613
|
+
return {};
|
|
7614
|
+
}
|
|
7615
|
+
return {
|
|
7616
|
+
Program(node) {
|
|
7617
|
+
const primary = findPrimaryExport(node.body);
|
|
7618
|
+
if (primary === null) return;
|
|
7619
|
+
if (CONVENTIONAL_BUCKET_EXPORTS2.has(primary.name) || GENERIC_ENTRYPOINTS.has(primary.name)) return;
|
|
7620
|
+
const expectedStem = kebabCase3(primary.name);
|
|
7621
|
+
if (stem3 === expectedStem) return;
|
|
7622
|
+
const ext = base.endsWith(".tsx") ? ".tsx" : ".ts";
|
|
7623
|
+
context.report({
|
|
7624
|
+
node: primary.node,
|
|
7625
|
+
messageId: "primaryExportMismatch",
|
|
7626
|
+
data: { stem: stem3, name: primary.name, expected: expectedStem, ext }
|
|
7627
|
+
});
|
|
7628
|
+
}
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
});
|
|
7632
|
+
|
|
7633
|
+
// src/rules/no-implicit-attribute-access.ts
|
|
7634
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES36, ESLintUtils as ESLintUtils49 } from "@typescript-eslint/utils";
|
|
7635
|
+
var EXCLUDED_BASES = /* @__PURE__ */ new Set([
|
|
7636
|
+
"environ",
|
|
7637
|
+
"headers",
|
|
7638
|
+
"cookies",
|
|
7639
|
+
"session",
|
|
7640
|
+
"redis",
|
|
7641
|
+
"cache",
|
|
7642
|
+
"state",
|
|
7643
|
+
"config",
|
|
7644
|
+
"env",
|
|
7645
|
+
"process",
|
|
7646
|
+
"localStorage",
|
|
7647
|
+
"sessionStorage"
|
|
7648
|
+
]);
|
|
7649
|
+
function getBaseName(node) {
|
|
7650
|
+
if (node.type === AST_NODE_TYPES36.Identifier) {
|
|
7651
|
+
return node.name;
|
|
7652
|
+
}
|
|
7653
|
+
if (node.type === AST_NODE_TYPES36.MemberExpression && node.property.type === AST_NODE_TYPES36.Identifier) {
|
|
7654
|
+
return node.property.name;
|
|
7655
|
+
}
|
|
7656
|
+
return null;
|
|
7657
|
+
}
|
|
7658
|
+
var no_implicit_attribute_access_default = ESLintUtils49.RuleCreator(
|
|
7659
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7660
|
+
)({
|
|
7661
|
+
name: "no-implicit-attribute-access",
|
|
7662
|
+
meta: {
|
|
7663
|
+
type: "problem",
|
|
7664
|
+
docs: {
|
|
7665
|
+
description: "Disallow imperative dictionary access with string literals; use declarative Zod schemas."
|
|
7666
|
+
},
|
|
7667
|
+
schema: [],
|
|
7668
|
+
messages: {
|
|
7669
|
+
noImplicitAttributeAccess: "Imperative lookup for '{{key}}' \u2014 if the key is known, parse the object declaratively with a Zod schema instead."
|
|
7670
|
+
}
|
|
7671
|
+
},
|
|
7672
|
+
defaultOptions: [],
|
|
7673
|
+
create(context) {
|
|
7674
|
+
return {
|
|
7675
|
+
MemberExpression(node) {
|
|
7676
|
+
if (!node.computed) {
|
|
7677
|
+
return;
|
|
7678
|
+
}
|
|
7679
|
+
if (node.property.type === AST_NODE_TYPES36.Literal && typeof node.property.value === "string") {
|
|
7680
|
+
const baseName = getBaseName(node.object);
|
|
7681
|
+
if (!baseName || !EXCLUDED_BASES.has(baseName)) {
|
|
7682
|
+
context.report({
|
|
7683
|
+
node,
|
|
7684
|
+
messageId: "noImplicitAttributeAccess",
|
|
7685
|
+
data: { key: node.property.value }
|
|
7686
|
+
});
|
|
7687
|
+
}
|
|
7688
|
+
}
|
|
7689
|
+
},
|
|
7690
|
+
CallExpression(node) {
|
|
7691
|
+
const callee = node.callee;
|
|
7692
|
+
if (callee.type === AST_NODE_TYPES36.MemberExpression) {
|
|
7693
|
+
const method = callee.property.type === AST_NODE_TYPES36.Identifier ? callee.property.name : null;
|
|
7694
|
+
if (method === "get") {
|
|
7695
|
+
const arg = node.arguments[0];
|
|
7696
|
+
if (arg && arg.type === AST_NODE_TYPES36.Literal && typeof arg.value === "string") {
|
|
7697
|
+
const baseName = getBaseName(callee.object);
|
|
7698
|
+
if (!baseName || !EXCLUDED_BASES.has(baseName)) {
|
|
7699
|
+
context.report({
|
|
7700
|
+
node,
|
|
7701
|
+
messageId: "noImplicitAttributeAccess",
|
|
7702
|
+
data: { key: arg.value }
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
});
|
|
7712
|
+
|
|
7261
7713
|
// src/index.ts
|
|
7262
7714
|
var rules = {
|
|
7263
7715
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -7286,6 +7738,7 @@ var rules = {
|
|
|
7286
7738
|
"no-unsafe-cast": no_unsafe_cast_default,
|
|
7287
7739
|
"prefer-string-literal-union": prefer_string_literal_union_default,
|
|
7288
7740
|
"single-public-export": single_public_export_default,
|
|
7741
|
+
"primary-export-file-name": primary_export_file_name_default,
|
|
7289
7742
|
"no-silent-promise-catch": no_silent_promise_catch_default,
|
|
7290
7743
|
"require-fetch-timeout": require_fetch_timeout_default,
|
|
7291
7744
|
"require-schema-validate-search": require_schema_validate_search_default,
|
|
@@ -7304,12 +7757,15 @@ var rules = {
|
|
|
7304
7757
|
"jsdoc-restates-signature": jsdoc_restates_signature_default,
|
|
7305
7758
|
"no-restated-comment": no_restated_comment_default,
|
|
7306
7759
|
"trailing-value-narration": trailing_value_narration_default,
|
|
7307
|
-
"no-tautological-expect": no_tautological_expect_default
|
|
7760
|
+
"no-tautological-expect": no_tautological_expect_default,
|
|
7761
|
+
"require-interface-for-injected-service": require_interface_for_injected_service_default,
|
|
7762
|
+
"prefer-non-nullable-collection": prefer_non_nullable_collection_default,
|
|
7763
|
+
"no-implicit-attribute-access": no_implicit_attribute_access_default
|
|
7308
7764
|
};
|
|
7309
7765
|
var plugin = {
|
|
7310
7766
|
meta: {
|
|
7311
7767
|
name: "@sarj/eslint-plugin",
|
|
7312
|
-
version: "2.
|
|
7768
|
+
version: "2.16.0"
|
|
7313
7769
|
},
|
|
7314
7770
|
rules,
|
|
7315
7771
|
configs: {
|
|
@@ -7334,13 +7790,17 @@ var plugin = {
|
|
|
7334
7790
|
"@sarj/prefer-discriminated-union": "warn",
|
|
7335
7791
|
"@sarj/no-comment-cruft": "warn",
|
|
7336
7792
|
// Frontend / styling — distilled from frontend PR-review mining.
|
|
7337
|
-
"@sarj/prefer-semantic-colors": [
|
|
7793
|
+
"@sarj/prefer-semantic-colors": [
|
|
7794
|
+
"warn",
|
|
7795
|
+
{ requireSemanticTokens: true }
|
|
7796
|
+
],
|
|
7338
7797
|
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
7339
7798
|
"@sarj/no-fat-try-blocks": "warn",
|
|
7340
7799
|
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
7341
7800
|
"@sarj/no-secret-in-log": "warn",
|
|
7342
7801
|
"@sarj/no-unsafe-cast": "warn",
|
|
7343
7802
|
"@sarj/single-public-export": "warn",
|
|
7803
|
+
"@sarj/primary-export-file-name": "warn",
|
|
7344
7804
|
"@sarj/prefer-string-literal-union": "warn",
|
|
7345
7805
|
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
7346
7806
|
"@sarj/require-fetch-timeout": "warn",
|
|
@@ -7384,7 +7844,14 @@ var plugin = {
|
|
|
7384
7844
|
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
7385
7845
|
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
7386
7846
|
// positives, 0 false positives.
|
|
7387
|
-
"@sarj/no-tautological-expect": "warn"
|
|
7847
|
+
"@sarj/no-tautological-expect": "warn",
|
|
7848
|
+
// Substitutability: an exported service class with injected
|
|
7849
|
+
// collaborators and no interface above it can only be tested by
|
|
7850
|
+
// mocking. 11-repo sweep: 229 exported classes, 82% already carry a
|
|
7851
|
+
// port, 29 fire, 28 of them true positives.
|
|
7852
|
+
"@sarj/require-interface-for-injected-service": "warn",
|
|
7853
|
+
"@sarj/prefer-non-nullable-collection": "warn",
|
|
7854
|
+
"@sarj/no-implicit-attribute-access": "warn"
|
|
7388
7855
|
}
|
|
7389
7856
|
},
|
|
7390
7857
|
strict: {
|
|
@@ -7412,13 +7879,17 @@ var plugin = {
|
|
|
7412
7879
|
"@sarj/no-comment-cruft": "error",
|
|
7413
7880
|
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
7414
7881
|
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
7415
|
-
"@sarj/prefer-semantic-colors": [
|
|
7882
|
+
"@sarj/prefer-semantic-colors": [
|
|
7883
|
+
"error",
|
|
7884
|
+
{ requireSemanticTokens: true }
|
|
7885
|
+
],
|
|
7416
7886
|
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
7417
7887
|
"@sarj/no-fat-try-blocks": "error",
|
|
7418
7888
|
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
7419
7889
|
"@sarj/no-secret-in-log": "error",
|
|
7420
7890
|
"@sarj/no-unsafe-cast": "error",
|
|
7421
7891
|
"@sarj/single-public-export": "error",
|
|
7892
|
+
"@sarj/primary-export-file-name": "error",
|
|
7422
7893
|
// Promoted to error 2026-07-25 — strict means strict (user directive).
|
|
7423
7894
|
"@sarj/prefer-string-literal-union": "error",
|
|
7424
7895
|
// Mined from 2y of PR review feedback + 5-repo code-smell audit (2026-07).
|
|
@@ -7454,7 +7925,13 @@ var plugin = {
|
|
|
7454
7925
|
"@sarj/jsdoc-restates-signature": "error",
|
|
7455
7926
|
"@sarj/trailing-value-narration": "error",
|
|
7456
7927
|
// TS half of SARJ057 — see the `recommended` block for the measurement.
|
|
7457
|
-
"@sarj/no-tautological-expect": "error"
|
|
7928
|
+
"@sarj/no-tautological-expect": "error",
|
|
7929
|
+
// Substitutability: the TS sibling of the Python `prefer-real-store-in-tests`
|
|
7930
|
+
// / `prefer-library-fake` wave. The convention already exists in the
|
|
7931
|
+
// corpus (175 `implements` clauses vs 29 hits), so strict enforces it.
|
|
7932
|
+
"@sarj/require-interface-for-injected-service": "error",
|
|
7933
|
+
"@sarj/prefer-non-nullable-collection": "error",
|
|
7934
|
+
"@sarj/no-implicit-attribute-access": "error"
|
|
7458
7935
|
}
|
|
7459
7936
|
}
|
|
7460
7937
|
}
|