@sarj/eslint-plugin 2.14.0 → 2.15.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 +269 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +269 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -346,6 +346,12 @@ declare const rules: {
|
|
|
346
346
|
"no-tautological-expect": _typescript_eslint_utils_ts_eslint.RuleModule<"tautologicalComparison" | "tautologicalMatcher", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
347
347
|
name: string;
|
|
348
348
|
};
|
|
349
|
+
"require-interface-for-injected-service": _typescript_eslint_utils_ts_eslint.RuleModule<"requireInterface", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
350
|
+
name: string;
|
|
351
|
+
};
|
|
352
|
+
"prefer-non-nullable-collection": _typescript_eslint_utils_ts_eslint.RuleModule<"preferNonNullableCollection", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
353
|
+
name: string;
|
|
354
|
+
};
|
|
349
355
|
};
|
|
350
356
|
declare const plugin: {
|
|
351
357
|
meta: {
|
|
@@ -502,6 +508,12 @@ declare const plugin: {
|
|
|
502
508
|
"no-tautological-expect": _typescript_eslint_utils_ts_eslint.RuleModule<"tautologicalComparison" | "tautologicalMatcher", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
503
509
|
name: string;
|
|
504
510
|
};
|
|
511
|
+
"require-interface-for-injected-service": _typescript_eslint_utils_ts_eslint.RuleModule<"requireInterface", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
512
|
+
name: string;
|
|
513
|
+
};
|
|
514
|
+
"prefer-non-nullable-collection": _typescript_eslint_utils_ts_eslint.RuleModule<"preferNonNullableCollection", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
515
|
+
name: string;
|
|
516
|
+
};
|
|
505
517
|
};
|
|
506
518
|
configs: {
|
|
507
519
|
recommended: {
|
|
@@ -549,6 +561,8 @@ declare const plugin: {
|
|
|
549
561
|
"@sarj/jsdoc-restates-signature": string;
|
|
550
562
|
"@sarj/trailing-value-narration": string;
|
|
551
563
|
"@sarj/no-tautological-expect": string;
|
|
564
|
+
"@sarj/require-interface-for-injected-service": string;
|
|
565
|
+
"@sarj/prefer-non-nullable-collection": string;
|
|
552
566
|
};
|
|
553
567
|
};
|
|
554
568
|
strict: {
|
|
@@ -601,6 +615,8 @@ declare const plugin: {
|
|
|
601
615
|
"@sarj/jsdoc-restates-signature": string;
|
|
602
616
|
"@sarj/trailing-value-narration": string;
|
|
603
617
|
"@sarj/no-tautological-expect": string;
|
|
618
|
+
"@sarj/require-interface-for-injected-service": string;
|
|
619
|
+
"@sarj/prefer-non-nullable-collection": string;
|
|
604
620
|
};
|
|
605
621
|
};
|
|
606
622
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -346,6 +346,12 @@ declare const rules: {
|
|
|
346
346
|
"no-tautological-expect": _typescript_eslint_utils_ts_eslint.RuleModule<"tautologicalComparison" | "tautologicalMatcher", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
347
347
|
name: string;
|
|
348
348
|
};
|
|
349
|
+
"require-interface-for-injected-service": _typescript_eslint_utils_ts_eslint.RuleModule<"requireInterface", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
350
|
+
name: string;
|
|
351
|
+
};
|
|
352
|
+
"prefer-non-nullable-collection": _typescript_eslint_utils_ts_eslint.RuleModule<"preferNonNullableCollection", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
353
|
+
name: string;
|
|
354
|
+
};
|
|
349
355
|
};
|
|
350
356
|
declare const plugin: {
|
|
351
357
|
meta: {
|
|
@@ -502,6 +508,12 @@ declare const plugin: {
|
|
|
502
508
|
"no-tautological-expect": _typescript_eslint_utils_ts_eslint.RuleModule<"tautologicalComparison" | "tautologicalMatcher", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
503
509
|
name: string;
|
|
504
510
|
};
|
|
511
|
+
"require-interface-for-injected-service": _typescript_eslint_utils_ts_eslint.RuleModule<"requireInterface", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
512
|
+
name: string;
|
|
513
|
+
};
|
|
514
|
+
"prefer-non-nullable-collection": _typescript_eslint_utils_ts_eslint.RuleModule<"preferNonNullableCollection", readonly [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
515
|
+
name: string;
|
|
516
|
+
};
|
|
505
517
|
};
|
|
506
518
|
configs: {
|
|
507
519
|
recommended: {
|
|
@@ -549,6 +561,8 @@ declare const plugin: {
|
|
|
549
561
|
"@sarj/jsdoc-restates-signature": string;
|
|
550
562
|
"@sarj/trailing-value-narration": string;
|
|
551
563
|
"@sarj/no-tautological-expect": string;
|
|
564
|
+
"@sarj/require-interface-for-injected-service": string;
|
|
565
|
+
"@sarj/prefer-non-nullable-collection": string;
|
|
552
566
|
};
|
|
553
567
|
};
|
|
554
568
|
strict: {
|
|
@@ -601,6 +615,8 @@ declare const plugin: {
|
|
|
601
615
|
"@sarj/jsdoc-restates-signature": string;
|
|
602
616
|
"@sarj/trailing-value-narration": string;
|
|
603
617
|
"@sarj/no-tautological-expect": string;
|
|
618
|
+
"@sarj/require-interface-for-injected-service": string;
|
|
619
|
+
"@sarj/prefer-non-nullable-collection": string;
|
|
604
620
|
};
|
|
605
621
|
};
|
|
606
622
|
};
|
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);
|
|
@@ -5134,8 +5134,8 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
|
|
|
5134
5134
|
if (base.endsWith(".d.ts")) return {};
|
|
5135
5135
|
if (TEST_FILE_RE.test(base)) return {};
|
|
5136
5136
|
if (isTestFile(context.filename)) return {};
|
|
5137
|
-
const
|
|
5138
|
-
if (!JUNK_DRAWER_STEMS.has(
|
|
5137
|
+
const stem3 = stemOf(base);
|
|
5138
|
+
if (!JUNK_DRAWER_STEMS.has(stem3.toLowerCase())) return {};
|
|
5139
5139
|
return {
|
|
5140
5140
|
Program(node) {
|
|
5141
5141
|
const { names, hasReExport, candidate } = summarizeExports(node.body);
|
|
@@ -5143,11 +5143,11 @@ var single_public_export_default = ESLintUtils29.RuleCreator(
|
|
|
5143
5143
|
if (names !== 1 || candidate === null) return;
|
|
5144
5144
|
if (CONVENTIONAL_BUCKET_EXPORTS.has(candidate.name)) return;
|
|
5145
5145
|
const expected = kebabCase2(candidate.name);
|
|
5146
|
-
if (
|
|
5146
|
+
if (stem3 === expected) return;
|
|
5147
5147
|
context.report({
|
|
5148
5148
|
node: candidate.node,
|
|
5149
5149
|
messageId: "renameJunkDrawer",
|
|
5150
|
-
data: { stem:
|
|
5150
|
+
data: { stem: stem3, name: candidate.name, expected }
|
|
5151
5151
|
});
|
|
5152
5152
|
}
|
|
5153
5153
|
};
|
|
@@ -5301,7 +5301,7 @@ function createSqlListener(handler) {
|
|
|
5301
5301
|
}
|
|
5302
5302
|
|
|
5303
5303
|
// src/rules/no-offset-pagination.ts
|
|
5304
|
-
var OFFSET_PAGINATION = /\bOFFSET\s+(
|
|
5304
|
+
var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
|
|
5305
5305
|
var OFFSET_GATE = /offset/i;
|
|
5306
5306
|
var no_offset_pagination_default = ESLintUtils30.RuleCreator(
|
|
5307
5307
|
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
@@ -5493,7 +5493,6 @@ var no_positional_tuple_return_default = ESLintUtils31.RuleCreator(
|
|
|
5493
5493
|
// src/rules/no-repeated-string-literal.ts
|
|
5494
5494
|
import { AST_NODE_TYPES as AST_NODE_TYPES23, ESLintUtils as ESLintUtils32 } from "@typescript-eslint/utils";
|
|
5495
5495
|
var MIN_LENGTH = 40;
|
|
5496
|
-
var MIN_OCCURRENCES = 3;
|
|
5497
5496
|
var MIN_DISTINCT_SCOPES = 2;
|
|
5498
5497
|
var PREVIEW_LENGTH = 40;
|
|
5499
5498
|
var SQL_KEYWORD_RE = /\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|VALUES|ON CONFLICT|RETURNING|GROUP BY|ORDER BY)\b/;
|
|
@@ -5575,9 +5574,6 @@ var no_repeated_string_literal_default = ESLintUtils32.RuleCreator(
|
|
|
5575
5574
|
},
|
|
5576
5575
|
"Program:exit": () => {
|
|
5577
5576
|
for (const [value, nodes] of occurrences) {
|
|
5578
|
-
if (nodes.length < MIN_OCCURRENCES) {
|
|
5579
|
-
continue;
|
|
5580
|
-
}
|
|
5581
5577
|
const distinctScopes = new Set(
|
|
5582
5578
|
nodes.map((node) => scopes.get(node)).filter((scope) => scope != null)
|
|
5583
5579
|
);
|
|
@@ -7258,6 +7254,244 @@ var no_tautological_expect_default = ESLintUtils45.RuleCreator(
|
|
|
7258
7254
|
}
|
|
7259
7255
|
});
|
|
7260
7256
|
|
|
7257
|
+
// src/rules/require-interface-for-injected-service.ts
|
|
7258
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES33, ESLintUtils as ESLintUtils46 } from "@typescript-eslint/utils";
|
|
7259
|
+
var CONFIGISH_TYPE_RE = /(?:Options|Opts|Config|Configuration|Settings|Params|Props|Args|Env|Environment|Callbacks|Flags)$/;
|
|
7260
|
+
var CONFIGISH_NAME_RE = /^(?:options|opts|config|configuration|settings|params|props|args|env|environment|callbacks|flags|logger|log|clock)$/i;
|
|
7261
|
+
var HTTP_TRANSPORT_TYPE_RE = /^(?:KyInstance|AxiosInstance|Session)$/;
|
|
7262
|
+
var TRANSPORT_WRAPPER_NAME_RE = /Client$/;
|
|
7263
|
+
var ROUTER_FACTORY_NAME = "Router";
|
|
7264
|
+
var FRAMEWORK_HTTP_TYPES = /* @__PURE__ */ new Set(["Request", "Response", "NextFunction"]);
|
|
7265
|
+
var isExportedClass = (node) => node.parent.type === AST_NODE_TYPES33.ExportNamedDeclaration || node.parent.type === AST_NODE_TYPES33.ExportDefaultDeclaration;
|
|
7266
|
+
var qualifiedName = (name) => name.type === AST_NODE_TYPES33.Identifier ? name.name : name.type === AST_NODE_TYPES33.TSQualifiedName ? `${qualifiedName(name.left)}.${name.right.name}` : "";
|
|
7267
|
+
var typeReferenceName = (annotated) => {
|
|
7268
|
+
let target = annotated;
|
|
7269
|
+
if (target.type === AST_NODE_TYPES33.TSParameterProperty) target = target.parameter;
|
|
7270
|
+
if (target.type === AST_NODE_TYPES33.AssignmentPattern) target = target.left;
|
|
7271
|
+
if (target.type !== AST_NODE_TYPES33.Identifier) return null;
|
|
7272
|
+
const annotation = target.typeAnnotation?.typeAnnotation;
|
|
7273
|
+
if (annotation === void 0 || annotation.type !== AST_NODE_TYPES33.TSTypeReference) {
|
|
7274
|
+
return null;
|
|
7275
|
+
}
|
|
7276
|
+
const { typeName } = annotation;
|
|
7277
|
+
const rightmost = typeName.type === AST_NODE_TYPES33.Identifier ? typeName.name : typeName.type === AST_NODE_TYPES33.TSQualifiedName ? typeName.right.name : null;
|
|
7278
|
+
if (rightmost === null) return null;
|
|
7279
|
+
return { name: target.name, typeName: rightmost, display: qualifiedName(typeName) };
|
|
7280
|
+
};
|
|
7281
|
+
var readConstructor = (ctor) => {
|
|
7282
|
+
const body = ctor.value.body;
|
|
7283
|
+
const storedFrom = /* @__PURE__ */ new Set();
|
|
7284
|
+
let constructedFields = 0;
|
|
7285
|
+
if (body !== null && body !== void 0) {
|
|
7286
|
+
for (const statement of body.body) {
|
|
7287
|
+
if (statement.type !== AST_NODE_TYPES33.ExpressionStatement) continue;
|
|
7288
|
+
const expression = statement.expression;
|
|
7289
|
+
if (expression.type !== AST_NODE_TYPES33.AssignmentExpression || expression.operator !== "=" || expression.left.type !== AST_NODE_TYPES33.MemberExpression || expression.left.object.type !== AST_NODE_TYPES33.ThisExpression) {
|
|
7290
|
+
continue;
|
|
7291
|
+
}
|
|
7292
|
+
const source = expression.right;
|
|
7293
|
+
if (source.type === AST_NODE_TYPES33.NewExpression) {
|
|
7294
|
+
constructedFields += 1;
|
|
7295
|
+
} else if (source.type === AST_NODE_TYPES33.Identifier) {
|
|
7296
|
+
storedFrom.add(source.name);
|
|
7297
|
+
} else if (source.type === AST_NODE_TYPES33.MemberExpression && source.object.type === AST_NODE_TYPES33.Identifier) {
|
|
7298
|
+
storedFrom.add(source.object.name);
|
|
7299
|
+
}
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
const collaborators = [];
|
|
7303
|
+
for (const parameter of ctor.value.params) {
|
|
7304
|
+
const reference = typeReferenceName(parameter);
|
|
7305
|
+
if (reference === null) continue;
|
|
7306
|
+
const stored = parameter.type === AST_NODE_TYPES33.TSParameterProperty || storedFrom.has(reference.name);
|
|
7307
|
+
if (!stored) continue;
|
|
7308
|
+
if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
|
|
7309
|
+
if (CONFIGISH_NAME_RE.test(reference.name)) continue;
|
|
7310
|
+
collaborators.push(reference);
|
|
7311
|
+
}
|
|
7312
|
+
return { collaborators, constructedFields };
|
|
7313
|
+
};
|
|
7314
|
+
var subtreeHas = (root, found) => {
|
|
7315
|
+
let hit = false;
|
|
7316
|
+
const visit = (current) => {
|
|
7317
|
+
if (hit) return;
|
|
7318
|
+
if (found(current)) {
|
|
7319
|
+
hit = true;
|
|
7320
|
+
return;
|
|
7321
|
+
}
|
|
7322
|
+
for (const key of Object.keys(current)) {
|
|
7323
|
+
if (key === "parent") continue;
|
|
7324
|
+
const value = current[key];
|
|
7325
|
+
for (const child of Array.isArray(value) ? value : [value]) {
|
|
7326
|
+
if (child !== null && typeof child === "object" && typeof child.type === "string") {
|
|
7327
|
+
visit(child);
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
};
|
|
7332
|
+
visit(root);
|
|
7333
|
+
return hit;
|
|
7334
|
+
};
|
|
7335
|
+
var isFrameworkWiring = (body) => subtreeHas(body, (node) => {
|
|
7336
|
+
if (node.type === AST_NODE_TYPES33.CallExpression) {
|
|
7337
|
+
const { callee } = node;
|
|
7338
|
+
if (callee.type === AST_NODE_TYPES33.Identifier) return callee.name === ROUTER_FACTORY_NAME;
|
|
7339
|
+
return callee.type === AST_NODE_TYPES33.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES33.Identifier && callee.property.name === ROUTER_FACTORY_NAME;
|
|
7340
|
+
}
|
|
7341
|
+
return node.type === AST_NODE_TYPES33.TSTypeReference && node.typeName.type === AST_NODE_TYPES33.TSQualifiedName && FRAMEWORK_HTTP_TYPES.has(node.typeName.right.name);
|
|
7342
|
+
});
|
|
7343
|
+
var stem2 = (name) => name.replace(/^I(?=[A-Z])/, "").replace(/Impl$/, "");
|
|
7344
|
+
var fileInterfaceNames = (program) => {
|
|
7345
|
+
const names = [];
|
|
7346
|
+
for (const statement of program.body) {
|
|
7347
|
+
const declaration = statement.type === AST_NODE_TYPES33.ExportNamedDeclaration ? statement.declaration : statement;
|
|
7348
|
+
if (declaration?.type === AST_NODE_TYPES33.TSInterfaceDeclaration) names.push(declaration.id.name);
|
|
7349
|
+
}
|
|
7350
|
+
return names;
|
|
7351
|
+
};
|
|
7352
|
+
var isTransportWrapper = (className, collaborators, program) => {
|
|
7353
|
+
const [only] = collaborators;
|
|
7354
|
+
if (collaborators.length !== 1 || only === void 0) return false;
|
|
7355
|
+
if (!HTTP_TRANSPORT_TYPE_RE.test(only.typeName)) return false;
|
|
7356
|
+
if (!TRANSPORT_WRAPPER_NAME_RE.test(className)) return false;
|
|
7357
|
+
const target = stem2(className);
|
|
7358
|
+
return !fileInterfaceNames(program).some((name) => {
|
|
7359
|
+
const other = stem2(name);
|
|
7360
|
+
return target.endsWith(other) || other.endsWith(target);
|
|
7361
|
+
});
|
|
7362
|
+
};
|
|
7363
|
+
var publicMethodNames = (body) => {
|
|
7364
|
+
const names = [];
|
|
7365
|
+
for (const member of body.body) {
|
|
7366
|
+
if (member.type !== AST_NODE_TYPES33.MethodDefinition) continue;
|
|
7367
|
+
if (member.kind !== "method" || member.static) continue;
|
|
7368
|
+
if (member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
7369
|
+
if (member.key.type === AST_NODE_TYPES33.PrivateIdentifier) continue;
|
|
7370
|
+
if (member.key.type === AST_NODE_TYPES33.Identifier) names.push(member.key.name);
|
|
7371
|
+
else names.push("\u2026");
|
|
7372
|
+
}
|
|
7373
|
+
return names;
|
|
7374
|
+
};
|
|
7375
|
+
var require_interface_for_injected_service_default = ESLintUtils46.RuleCreator(
|
|
7376
|
+
(name) => `https://github.com/sarj-ai/standards/blob/main/packages/typescript/src/rules/${name}.ts`
|
|
7377
|
+
)({
|
|
7378
|
+
name: "require-interface-for-injected-service",
|
|
7379
|
+
meta: {
|
|
7380
|
+
type: "suggestion",
|
|
7381
|
+
docs: {
|
|
7382
|
+
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."
|
|
7383
|
+
},
|
|
7384
|
+
schema: [],
|
|
7385
|
+
messages: {
|
|
7386
|
+
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>`."
|
|
7387
|
+
}
|
|
7388
|
+
},
|
|
7389
|
+
defaultOptions: [],
|
|
7390
|
+
create(context) {
|
|
7391
|
+
const { filename } = context;
|
|
7392
|
+
if (isTestFile(filename) || isStoryFile(filename) || isScriptFile(filename)) return {};
|
|
7393
|
+
if (isGeneratedFile(filename, context.sourceCode.getText())) return {};
|
|
7394
|
+
return {
|
|
7395
|
+
ClassDeclaration(node) {
|
|
7396
|
+
if (node.id === null) return;
|
|
7397
|
+
if (!isExportedClass(node)) return;
|
|
7398
|
+
if (node.abstract === true) return;
|
|
7399
|
+
if (node.superClass !== null) return;
|
|
7400
|
+
if (node.implements.length > 0) return;
|
|
7401
|
+
if (node.decorators.length > 0) return;
|
|
7402
|
+
const ctor = node.body.body.find(
|
|
7403
|
+
(member) => member.type === AST_NODE_TYPES33.MethodDefinition && member.kind === "constructor"
|
|
7404
|
+
);
|
|
7405
|
+
if (ctor === void 0) return;
|
|
7406
|
+
const { collaborators, constructedFields } = readConstructor(ctor);
|
|
7407
|
+
if (collaborators.length === 0) return;
|
|
7408
|
+
if (constructedFields > collaborators.length) return;
|
|
7409
|
+
if (isFrameworkWiring(node.body)) return;
|
|
7410
|
+
if (isTransportWrapper(node.id.name, collaborators, context.sourceCode.ast)) return;
|
|
7411
|
+
const methods = publicMethodNames(node.body);
|
|
7412
|
+
if (methods.length === 0) return;
|
|
7413
|
+
context.report({
|
|
7414
|
+
node: node.id,
|
|
7415
|
+
messageId: "requireInterface",
|
|
7416
|
+
data: {
|
|
7417
|
+
name: node.id.name,
|
|
7418
|
+
deps: collaborators.map((c) => `${c.name}: ${c.display}`).join(", "),
|
|
7419
|
+
methods: methods.join(", ")
|
|
7420
|
+
}
|
|
7421
|
+
});
|
|
7422
|
+
}
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
7425
|
+
});
|
|
7426
|
+
|
|
7427
|
+
// src/rules/prefer-non-nullable-collection.ts
|
|
7428
|
+
import { AST_NODE_TYPES as AST_NODE_TYPES34, ESLintUtils as ESLintUtils47 } from "@typescript-eslint/utils";
|
|
7429
|
+
var ARRAY_TYPE_NAMES = /* @__PURE__ */ new Set(["Array", "ReadonlyArray"]);
|
|
7430
|
+
function propertyName(node) {
|
|
7431
|
+
const key = node.key;
|
|
7432
|
+
if (key.type === AST_NODE_TYPES34.Identifier) return key.name;
|
|
7433
|
+
if (key.type === AST_NODE_TYPES34.Literal) return String(key.value);
|
|
7434
|
+
return "collection";
|
|
7435
|
+
}
|
|
7436
|
+
function isArrayType(node) {
|
|
7437
|
+
if (node.type === AST_NODE_TYPES34.TSArrayType) return true;
|
|
7438
|
+
return node.type === AST_NODE_TYPES34.TSTypeReference && node.typeName.type === AST_NODE_TYPES34.Identifier && ARRAY_TYPE_NAMES.has(node.typeName.name);
|
|
7439
|
+
}
|
|
7440
|
+
function isNullishType(node) {
|
|
7441
|
+
return node.type === AST_NODE_TYPES34.TSNullKeyword || node.type === AST_NODE_TYPES34.TSUndefinedKeyword;
|
|
7442
|
+
}
|
|
7443
|
+
function isNullableArrayOnly(node) {
|
|
7444
|
+
const values = node.types.filter((member) => !isNullishType(member));
|
|
7445
|
+
return values.length > 0 && values.length < node.types.length && values.every(isArrayType);
|
|
7446
|
+
}
|
|
7447
|
+
var prefer_non_nullable_collection_default = ESLintUtils47.RuleCreator(
|
|
7448
|
+
(name) => `https://github.com/sarj-ai/standards/tree/main/packages/typescript#${name}`
|
|
7449
|
+
)({
|
|
7450
|
+
name: "prefer-non-nullable-collection",
|
|
7451
|
+
meta: {
|
|
7452
|
+
type: "suggestion",
|
|
7453
|
+
docs: {
|
|
7454
|
+
description: "Require array types to use an empty array instead of explicit nullish unions with two equivalent empty states."
|
|
7455
|
+
},
|
|
7456
|
+
schema: [],
|
|
7457
|
+
messages: {
|
|
7458
|
+
preferNonNullableCollection: "`{{name}}` is a nullable array, so nullish and `[]` represent the same empty collection. Use a non-null array and default omitted values to `[]`."
|
|
7459
|
+
}
|
|
7460
|
+
},
|
|
7461
|
+
defaultOptions: [],
|
|
7462
|
+
create(context) {
|
|
7463
|
+
const normalizedFilename = context.filename.replaceAll("\\", "/");
|
|
7464
|
+
if (isTestFile(context.filename) || isGeneratedFile(context.filename, context.sourceCode.text) || /\/(?:vendor|vendored)\//u.test(normalizedFilename)) {
|
|
7465
|
+
return {};
|
|
7466
|
+
}
|
|
7467
|
+
function checkOptionalProperty(node) {
|
|
7468
|
+
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
7469
|
+
if (annotation === void 0) return;
|
|
7470
|
+
if (annotation.type !== AST_NODE_TYPES34.TSUnionType || !isNullableArrayOnly(annotation)) {
|
|
7471
|
+
return;
|
|
7472
|
+
}
|
|
7473
|
+
context.report({
|
|
7474
|
+
node,
|
|
7475
|
+
messageId: "preferNonNullableCollection",
|
|
7476
|
+
data: { name: propertyName(node) }
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
return {
|
|
7480
|
+
TSPropertySignature: checkOptionalProperty,
|
|
7481
|
+
PropertyDefinition: checkOptionalProperty,
|
|
7482
|
+
TSTypeAliasDeclaration(node) {
|
|
7483
|
+
if (node.typeAnnotation.type !== AST_NODE_TYPES34.TSUnionType) return;
|
|
7484
|
+
if (!isNullableArrayOnly(node.typeAnnotation)) return;
|
|
7485
|
+
context.report({
|
|
7486
|
+
node,
|
|
7487
|
+
messageId: "preferNonNullableCollection",
|
|
7488
|
+
data: { name: node.id.name }
|
|
7489
|
+
});
|
|
7490
|
+
}
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
});
|
|
7494
|
+
|
|
7261
7495
|
// src/index.ts
|
|
7262
7496
|
var rules = {
|
|
7263
7497
|
"enforce-file-structure": enforce_file_structure_default,
|
|
@@ -7304,12 +7538,14 @@ var rules = {
|
|
|
7304
7538
|
"jsdoc-restates-signature": jsdoc_restates_signature_default,
|
|
7305
7539
|
"no-restated-comment": no_restated_comment_default,
|
|
7306
7540
|
"trailing-value-narration": trailing_value_narration_default,
|
|
7307
|
-
"no-tautological-expect": no_tautological_expect_default
|
|
7541
|
+
"no-tautological-expect": no_tautological_expect_default,
|
|
7542
|
+
"require-interface-for-injected-service": require_interface_for_injected_service_default,
|
|
7543
|
+
"prefer-non-nullable-collection": prefer_non_nullable_collection_default
|
|
7308
7544
|
};
|
|
7309
7545
|
var plugin = {
|
|
7310
7546
|
meta: {
|
|
7311
7547
|
name: "@sarj/eslint-plugin",
|
|
7312
|
-
version: "2.
|
|
7548
|
+
version: "2.15.0"
|
|
7313
7549
|
},
|
|
7314
7550
|
rules,
|
|
7315
7551
|
configs: {
|
|
@@ -7334,7 +7570,10 @@ var plugin = {
|
|
|
7334
7570
|
"@sarj/prefer-discriminated-union": "warn",
|
|
7335
7571
|
"@sarj/no-comment-cruft": "warn",
|
|
7336
7572
|
// Frontend / styling — distilled from frontend PR-review mining.
|
|
7337
|
-
"@sarj/prefer-semantic-colors": [
|
|
7573
|
+
"@sarj/prefer-semantic-colors": [
|
|
7574
|
+
"warn",
|
|
7575
|
+
{ requireSemanticTokens: true }
|
|
7576
|
+
],
|
|
7338
7577
|
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
7339
7578
|
"@sarj/no-fat-try-blocks": "warn",
|
|
7340
7579
|
"@sarj/no-cors-wildcard-with-credentials": "warn",
|
|
@@ -7384,7 +7623,13 @@ var plugin = {
|
|
|
7384
7623
|
// Measured across 5,819 .ts/.tsx files (1,003 of them test files) in
|
|
7385
7624
|
// six internal repos plus got / hono / swr / trpc: 3 hits, 3 true
|
|
7386
7625
|
// positives, 0 false positives.
|
|
7387
|
-
"@sarj/no-tautological-expect": "warn"
|
|
7626
|
+
"@sarj/no-tautological-expect": "warn",
|
|
7627
|
+
// Substitutability: an exported service class with injected
|
|
7628
|
+
// collaborators and no interface above it can only be tested by
|
|
7629
|
+
// mocking. 11-repo sweep: 229 exported classes, 82% already carry a
|
|
7630
|
+
// port, 29 fire, 28 of them true positives.
|
|
7631
|
+
"@sarj/require-interface-for-injected-service": "warn",
|
|
7632
|
+
"@sarj/prefer-non-nullable-collection": "warn"
|
|
7388
7633
|
}
|
|
7389
7634
|
},
|
|
7390
7635
|
strict: {
|
|
@@ -7412,7 +7657,10 @@ var plugin = {
|
|
|
7412
7657
|
"@sarj/no-comment-cruft": "error",
|
|
7413
7658
|
// Frontend / styling — distilled from frontend PR-review mining. Stylistic,
|
|
7414
7659
|
// no autofix → warn (rollout should prove the FP rate before raising it).
|
|
7415
|
-
"@sarj/prefer-semantic-colors": [
|
|
7660
|
+
"@sarj/prefer-semantic-colors": [
|
|
7661
|
+
"error",
|
|
7662
|
+
{ requireSemanticTokens: true }
|
|
7663
|
+
],
|
|
7416
7664
|
// Ported from sarj-python-lint (SARJ), corpus-validated FP~0.
|
|
7417
7665
|
"@sarj/no-fat-try-blocks": "error",
|
|
7418
7666
|
"@sarj/no-cors-wildcard-with-credentials": "error",
|
|
@@ -7454,7 +7702,12 @@ var plugin = {
|
|
|
7454
7702
|
"@sarj/jsdoc-restates-signature": "error",
|
|
7455
7703
|
"@sarj/trailing-value-narration": "error",
|
|
7456
7704
|
// TS half of SARJ057 — see the `recommended` block for the measurement.
|
|
7457
|
-
"@sarj/no-tautological-expect": "error"
|
|
7705
|
+
"@sarj/no-tautological-expect": "error",
|
|
7706
|
+
// Substitutability: the TS sibling of the Python `prefer-real-store-in-tests`
|
|
7707
|
+
// / `prefer-library-fake` wave. The convention already exists in the
|
|
7708
|
+
// corpus (175 `implements` clauses vs 29 hits), so strict enforces it.
|
|
7709
|
+
"@sarj/require-interface-for-injected-service": "error",
|
|
7710
|
+
"@sarj/prefer-non-nullable-collection": "error"
|
|
7458
7711
|
}
|
|
7459
7712
|
}
|
|
7460
7713
|
}
|