@sarj/eslint-plugin 16.2.5 → 16.3.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/dist/index.cjs +386 -199
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +386 -199
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -298,69 +298,34 @@ function commentLineEnd(sourceText, start) {
|
|
|
298
298
|
// src/rules/require-explicit-contract-implementation.ts
|
|
299
299
|
var REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION = {
|
|
300
300
|
defaultLevel: "warning",
|
|
301
|
-
summary: "Require
|
|
302
|
-
rationale: "
|
|
303
|
-
remediation: "
|
|
301
|
+
summary: "Require nominal inheritance when an injected abstract class is rejected by an instanceof guard.",
|
|
302
|
+
rationale: "A structurally assignable fake can still fail a constructor's nominal runtime guard. Pure structural interfaces do not require an implements clause.",
|
|
303
|
+
remediation: "Use an implementation inheriting the existing abstract class, or remove the nominal guard if the boundary is intentionally structural.",
|
|
304
304
|
category: "architecture",
|
|
305
305
|
autofix: "none",
|
|
306
306
|
limitations: [
|
|
307
|
-
"
|
|
308
|
-
"
|
|
307
|
+
"Requires direct construction of both owned consumer and collaborator classes, with a first-statement consumer constructor guard rejecting the exact parameter with if (!(parameter instanceof Contract)) throw.",
|
|
308
|
+
"The contract must resolve to an abstract class without decorators, unresolved ancestors, or custom computed static members in its resolved hierarchy.",
|
|
309
|
+
"Structural interfaces, non-rejecting branches, reassignment before the guard, casts, unresolved heritage, dynamic factories, explicit implementation constructors or initializers, modules referencing prototype-mutator members or assigning prototypes, and third-party classes are excluded."
|
|
309
310
|
],
|
|
310
311
|
examples: [
|
|
311
312
|
{
|
|
312
|
-
id: "structural-fake
|
|
313
|
-
title: "A
|
|
313
|
+
id: "runtime-nominal-guard-with-structural-fake",
|
|
314
|
+
title: "A structurally compatible fake fails the constructor's runtime guard",
|
|
314
315
|
outcome: "match",
|
|
315
|
-
files: [{ path: "src/service.ts", source: "
|
|
316
|
+
files: [{ path: "src/service.ts", source: "abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher { publish() {} } new Consumer(new FakePublisher());" }],
|
|
316
317
|
focusPath: "src/service.ts",
|
|
317
318
|
expectedCount: 1,
|
|
318
319
|
public: true
|
|
319
320
|
},
|
|
320
321
|
{
|
|
321
|
-
id: "
|
|
322
|
-
title: "The fake
|
|
322
|
+
id: "runtime-nominal-guard-with-inherited-fake",
|
|
323
|
+
title: "The fake satisfies the existing nominal runtime contract",
|
|
323
324
|
outcome: "no-match",
|
|
324
|
-
files: [{ path: "src/service.ts", source: "
|
|
325
|
+
files: [{ path: "src/service.ts", source: "abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher extends Publisher { publish() {} } new Consumer(new FakePublisher());" }],
|
|
325
326
|
focusPath: "src/service.ts",
|
|
326
327
|
expectedCount: 0,
|
|
327
328
|
public: true
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
id: "inherited-implementation",
|
|
331
|
-
title: "The fake inherits an implementation of the port",
|
|
332
|
-
outcome: "no-match",
|
|
333
|
-
files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class BasePublisher implements Publisher { publish() {} } class FakePublisher extends BasePublisher {} new Consumer(new FakePublisher());" }],
|
|
334
|
-
focusPath: "src/service.ts",
|
|
335
|
-
expectedCount: 0,
|
|
336
|
-
public: false
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
id: "non-contract-data-argument",
|
|
340
|
-
title: "A data object is not a service port",
|
|
341
|
-
outcome: "no-match",
|
|
342
|
-
files: [{ path: "src/service.ts", source: "interface Options { name: string } class Consumer { constructor(readonly options: Options) {} } class FakeOptions { name = 'test' } new Consumer(new FakeOptions());" }],
|
|
343
|
-
focusPath: "src/service.ts",
|
|
344
|
-
expectedCount: 0,
|
|
345
|
-
public: false
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
id: "explicit-cast-origin-unknown",
|
|
349
|
-
title: "A cast hides the concrete source",
|
|
350
|
-
outcome: "no-match",
|
|
351
|
-
files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher { publish() {} } new Consumer(new FakePublisher() as Publisher);" }],
|
|
352
|
-
focusPath: "src/service.ts",
|
|
353
|
-
expectedCount: 0,
|
|
354
|
-
public: false
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
id: "unrelated-interface",
|
|
358
|
-
title: "Implementing another interface does not declare the supplied port",
|
|
359
|
-
outcome: "match",
|
|
360
|
-
files: [{ path: "src/service.ts", source: "interface Publisher { publish(): void } interface Other { publish(): void } class Consumer { constructor(readonly publisher: Publisher) {} } class FakePublisher implements Other { publish() {} } new Consumer(new FakePublisher());" }],
|
|
361
|
-
focusPath: "src/service.ts",
|
|
362
|
-
expectedCount: 1,
|
|
363
|
-
public: false
|
|
364
329
|
}
|
|
365
330
|
]
|
|
366
331
|
};
|
|
@@ -371,7 +336,7 @@ function behavioralContract(checker, type) {
|
|
|
371
336
|
return behavioralContract(checker, substantive[0]);
|
|
372
337
|
}
|
|
373
338
|
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
|
|
374
|
-
const declaration = type.getSymbol()?.declarations?.find((item) => ts.
|
|
339
|
+
const declaration = type.getSymbol()?.declarations?.find((item) => ts.isClassDeclaration(item) && item.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword));
|
|
375
340
|
if (declaration === void 0) return null;
|
|
376
341
|
return checker.getPropertiesOfType(type).some((property) => {
|
|
377
342
|
if (!property.declarations?.some((member) => ts.isMethodSignature(member) || ts.isMethodDeclaration(member))) return false;
|
|
@@ -379,43 +344,86 @@ function behavioralContract(checker, type) {
|
|
|
379
344
|
return propertyType.getCallSignatures().length > 0;
|
|
380
345
|
}) ? type : null;
|
|
381
346
|
}
|
|
382
|
-
function ownedClass(
|
|
347
|
+
function ownedClass(checker, expression) {
|
|
348
|
+
if (!ts.isNewExpression(expression)) return null;
|
|
349
|
+
const type = checker.getTypeAtLocation(expression);
|
|
383
350
|
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return null;
|
|
384
351
|
const declaration = type.getSymbol()?.declarations?.find(ts.isClassDeclaration);
|
|
385
|
-
|
|
352
|
+
const constructed = canonicalSymbol(checker, expression.expression)?.declarations?.find(ts.isClassDeclaration);
|
|
353
|
+
if (declaration === void 0 || declaration !== constructed || declaration.getSourceFile().isDeclarationFile || /(?:^|[/\\])(?:node_modules|vendor|generated)(?:[/\\])/u.test(declaration.getSourceFile().fileName)) return null;
|
|
386
354
|
return declaration;
|
|
387
355
|
}
|
|
388
|
-
function
|
|
356
|
+
function memberName(node) {
|
|
357
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
358
|
+
if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) return node.argumentExpression.text;
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
function hasPrototypeMutation(source) {
|
|
362
|
+
function visit(node) {
|
|
363
|
+
const member = memberName(node);
|
|
364
|
+
if (member === "setPrototypeOf" || member === "__proto__" || member === "hasInstance") return true;
|
|
365
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && memberName(node.left) === "prototype") return true;
|
|
366
|
+
return ts.forEachChild(node, visit);
|
|
367
|
+
}
|
|
368
|
+
return visit(source) === true;
|
|
369
|
+
}
|
|
370
|
+
function nominallyInherits(checker, declaration, target, seen) {
|
|
371
|
+
if (ts.canHaveDecorators(declaration) && ts.getDecorators(declaration)?.length) return null;
|
|
372
|
+
if (hasPrototypeMutation(declaration.getSourceFile()) || declaration.members.some((member) => ts.isConstructorDeclaration(member) || ts.isPropertyDeclaration(member) && member.initializer !== void 0)) return null;
|
|
389
373
|
const symbol = declaration.name === void 0 ? void 0 : canonicalSymbol(checker, declaration.name);
|
|
390
374
|
if (symbol !== void 0) {
|
|
391
375
|
if (seen.has(symbol)) return null;
|
|
392
376
|
seen.add(symbol);
|
|
393
377
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (parentSymbol === target) return true;
|
|
403
|
-
const inherited = parentSymbol.declarations?.find((item) => ts.isClassDeclaration(item) || ts.isInterfaceDeclaration(item));
|
|
404
|
-
if (inherited === void 0) {
|
|
405
|
-
unknown = true;
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
const relationship = explicitlyImplements(checker, inherited, target, new Set(seen));
|
|
409
|
-
if (relationship === true) return true;
|
|
410
|
-
unknown ||= relationship === null;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return unknown ? null : false;
|
|
378
|
+
const parents = declaration.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword)?.types;
|
|
379
|
+
if (parents === void 0) return false;
|
|
380
|
+
if (parents.length !== 1) return null;
|
|
381
|
+
const parentSymbol = canonicalSymbol(checker, parents[0].expression);
|
|
382
|
+
if (parentSymbol === void 0) return null;
|
|
383
|
+
if (parentSymbol === target) return true;
|
|
384
|
+
const inherited = parentSymbol.declarations?.find(ts.isClassDeclaration);
|
|
385
|
+
return inherited === void 0 ? null : nominallyInherits(checker, inherited, target, new Set(seen));
|
|
414
386
|
}
|
|
415
387
|
function canonicalSymbol(checker, node) {
|
|
416
388
|
const symbol = checker.getSymbolAtLocation(node);
|
|
417
389
|
return symbol === void 0 ? void 0 : (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
|
|
418
390
|
}
|
|
391
|
+
function hasNominalRejectionGuard(checker, signature, parameter, contract) {
|
|
392
|
+
const declaration = signature.getDeclaration();
|
|
393
|
+
if (declaration === void 0 || !ts.isConstructorDeclaration(declaration)) return false;
|
|
394
|
+
if (ts.canHaveDecorators(declaration.parent) && ts.getDecorators(declaration.parent)?.length) return false;
|
|
395
|
+
if (hasPrototypeMutation(declaration.getSourceFile())) return false;
|
|
396
|
+
const first = declaration.body?.statements[0];
|
|
397
|
+
if (first === void 0 || !ts.isIfStatement(first) || !alwaysThrows(first.thenStatement)) return false;
|
|
398
|
+
const condition = unparenthesized(first.expression);
|
|
399
|
+
if (!ts.isPrefixUnaryExpression(condition) || condition.operator !== ts.SyntaxKind.ExclamationToken) return false;
|
|
400
|
+
const comparison = unparenthesized(condition.operand);
|
|
401
|
+
if (!ts.isBinaryExpression(comparison) || comparison.operatorToken.kind !== ts.SyntaxKind.InstanceOfKeyword) return false;
|
|
402
|
+
if (canonicalSymbol(checker, comparison.left) !== parameter || canonicalSymbol(checker, comparison.right) !== contract) return false;
|
|
403
|
+
const target = contract.declarations?.find(ts.isClassDeclaration);
|
|
404
|
+
return target !== void 0 && hasOrdinaryInstanceCheck(checker, target, /* @__PURE__ */ new Set());
|
|
405
|
+
}
|
|
406
|
+
function unparenthesized(expression) {
|
|
407
|
+
let current = expression;
|
|
408
|
+
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
|
409
|
+
return current;
|
|
410
|
+
}
|
|
411
|
+
function alwaysThrows(statement) {
|
|
412
|
+
return ts.isThrowStatement(statement) || ts.isBlock(statement) && statement.statements.length === 1 && ts.isThrowStatement(statement.statements[0]);
|
|
413
|
+
}
|
|
414
|
+
function hasOrdinaryInstanceCheck(checker, declaration, seen) {
|
|
415
|
+
if (seen.has(declaration) || ts.getDecorators(declaration)?.length || hasPrototypeMutation(declaration.getSourceFile())) return false;
|
|
416
|
+
seen.add(declaration);
|
|
417
|
+
if (declaration.members.some((member) => member.name !== void 0 && ts.isComputedPropertyName(member.name) && ts.canHaveModifiers(member) && ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) return false;
|
|
418
|
+
for (const clause of declaration.heritageClauses ?? []) {
|
|
419
|
+
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
420
|
+
for (const parent of clause.types) {
|
|
421
|
+
const ancestor = canonicalSymbol(checker, parent.expression)?.declarations?.find(ts.isClassDeclaration);
|
|
422
|
+
if (ancestor === void 0 || !hasOrdinaryInstanceCheck(checker, ancestor, seen)) return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
419
427
|
var require_explicit_contract_implementation_default = createRule({
|
|
420
428
|
name: "require-explicit-contract-implementation",
|
|
421
429
|
documentation: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION,
|
|
@@ -424,7 +432,7 @@ var require_explicit_contract_implementation_default = createRule({
|
|
|
424
432
|
docs: { description: REQUIRE_EXPLICIT_CONTRACT_IMPLEMENTATION_DOCUMENTATION.summary },
|
|
425
433
|
schema: [],
|
|
426
434
|
messages: {
|
|
427
|
-
declareActualContract: "`{{implementation}}`
|
|
435
|
+
declareActualContract: "`{{implementation}}` structurally matches `{{contract}}` but fails the rejecting instanceof guard; use nominal inheritance or make the boundary structural."
|
|
428
436
|
}
|
|
429
437
|
},
|
|
430
438
|
defaultOptions: [],
|
|
@@ -435,25 +443,27 @@ var require_explicit_contract_implementation_default = createRule({
|
|
|
435
443
|
const checker = services.program.getTypeChecker();
|
|
436
444
|
function inspect(node) {
|
|
437
445
|
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
|
|
438
|
-
if (!ts.
|
|
446
|
+
if (!ts.isNewExpression(tsNode)) return;
|
|
447
|
+
const consumer = canonicalSymbol(checker, tsNode.expression)?.declarations?.find(ts.isClassDeclaration);
|
|
448
|
+
if (consumer === void 0) return;
|
|
439
449
|
const signature = checker.getResolvedSignature(tsNode);
|
|
440
|
-
if (signature === void 0) return;
|
|
450
|
+
if (signature === void 0 || signature.getDeclaration()?.parent !== consumer) return;
|
|
441
451
|
const parameters = signature.getParameters();
|
|
442
452
|
for (const [index, argument] of node.arguments.entries()) {
|
|
443
|
-
if (argument.type
|
|
453
|
+
if (argument.type !== AST_NODE_TYPES.NewExpression) continue;
|
|
444
454
|
const parameter = parameters[index];
|
|
445
455
|
if (parameter === void 0) continue;
|
|
446
456
|
const parameterType = checker.getTypeOfSymbolAtLocation(parameter, tsNode);
|
|
447
457
|
const contractType = behavioralContract(checker, parameterType);
|
|
448
458
|
if (contractType === null) continue;
|
|
449
459
|
const contractSymbol = contractType.getSymbol();
|
|
450
|
-
if (contractSymbol === void 0) continue;
|
|
460
|
+
if (contractSymbol === void 0 || !hasNominalRejectionGuard(checker, signature, parameter, contractSymbol)) continue;
|
|
451
461
|
const tsArgument = services.esTreeNodeToTSNodeMap.get(argument);
|
|
452
462
|
const actualType = checker.getTypeAtLocation(tsArgument);
|
|
453
463
|
if (!checker.isTypeAssignableTo(actualType, contractType)) continue;
|
|
454
|
-
const implementation = ownedClass(
|
|
464
|
+
const implementation = ownedClass(checker, tsArgument);
|
|
455
465
|
if (implementation === null) continue;
|
|
456
|
-
const relationship =
|
|
466
|
+
const relationship = nominallyInherits(checker, implementation, contractSymbol, /* @__PURE__ */ new Set());
|
|
457
467
|
if (relationship !== false) continue;
|
|
458
468
|
context.report({
|
|
459
469
|
node: argument,
|
|
@@ -463,7 +473,6 @@ var require_explicit_contract_implementation_default = createRule({
|
|
|
463
473
|
}
|
|
464
474
|
}
|
|
465
475
|
return {
|
|
466
|
-
CallExpression: inspect,
|
|
467
476
|
NewExpression: inspect
|
|
468
477
|
};
|
|
469
478
|
}
|
|
@@ -581,7 +590,7 @@ var propertySignatureTypes = (members) => {
|
|
|
581
590
|
};
|
|
582
591
|
var fileTypeIndex = (program) => {
|
|
583
592
|
const objects = /* @__PURE__ */ new Map();
|
|
584
|
-
const functionAliases =
|
|
593
|
+
const functionAliases = localFunctionAliases(program);
|
|
585
594
|
function collectDeclaredType(statement) {
|
|
586
595
|
const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
|
|
587
596
|
if (declaration?.type === AST_NODE_TYPES2.TSInterfaceDeclaration) {
|
|
@@ -590,10 +599,6 @@ var fileTypeIndex = (program) => {
|
|
|
590
599
|
}
|
|
591
600
|
if (declaration?.type !== AST_NODE_TYPES2.TSTypeAliasDeclaration) return;
|
|
592
601
|
const aliased = declaration.typeAnnotation;
|
|
593
|
-
if (aliased.type === AST_NODE_TYPES2.TSFunctionType || aliased.type === AST_NODE_TYPES2.TSConstructorType) {
|
|
594
|
-
functionAliases.add(declaration.id.name);
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
602
|
const literals = aliased.type === AST_NODE_TYPES2.TSTypeLiteral ? [aliased] : aliased.type === AST_NODE_TYPES2.TSIntersectionType ? aliased.types.filter((part) => part.type === AST_NODE_TYPES2.TSTypeLiteral) : [];
|
|
598
603
|
if (literals.length === 0) return;
|
|
599
604
|
const merged = /* @__PURE__ */ new Map();
|
|
@@ -609,6 +614,28 @@ var fileTypeIndex = (program) => {
|
|
|
609
614
|
}
|
|
610
615
|
return { objects, functionAliases };
|
|
611
616
|
};
|
|
617
|
+
function localFunctionAliases(program) {
|
|
618
|
+
const names = /* @__PURE__ */ new Set();
|
|
619
|
+
const parents = /* @__PURE__ */ new Map();
|
|
620
|
+
for (const statement of program.body) {
|
|
621
|
+
const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
|
|
622
|
+
if (declaration?.type !== AST_NODE_TYPES2.TSTypeAliasDeclaration) continue;
|
|
623
|
+
const annotation = declaration.typeAnnotation;
|
|
624
|
+
if (annotation.type === AST_NODE_TYPES2.TSFunctionType || annotation.type === AST_NODE_TYPES2.TSConstructorType) names.add(declaration.id.name);
|
|
625
|
+
if (annotation.type === AST_NODE_TYPES2.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES2.Identifier) parents.set(declaration.id.name, annotation.typeName.name);
|
|
626
|
+
}
|
|
627
|
+
for (let pass = 0; pass < parents.size; pass += 1) {
|
|
628
|
+
let changed = false;
|
|
629
|
+
for (const [name, parent] of parents) {
|
|
630
|
+
if (names.has(parent) && !names.has(name)) {
|
|
631
|
+
names.add(name);
|
|
632
|
+
changed = true;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (!changed) break;
|
|
636
|
+
}
|
|
637
|
+
return names;
|
|
638
|
+
}
|
|
612
639
|
var typeParameterNames = (...declarations) => {
|
|
613
640
|
const names = /* @__PURE__ */ new Set();
|
|
614
641
|
for (const declaration of declarations) {
|
|
@@ -620,12 +647,15 @@ var readConstructor = (ctor, declared, typeParameters) => {
|
|
|
620
647
|
const body2 = ctor.value.body;
|
|
621
648
|
const storedFieldsFrom = /* @__PURE__ */ new Map();
|
|
622
649
|
const storedMemberFieldsFrom = /* @__PURE__ */ new Map();
|
|
650
|
+
const constructedFieldNames = /* @__PURE__ */ new Set();
|
|
651
|
+
const localBindings2 = /* @__PURE__ */ new Set();
|
|
623
652
|
let constructedFields = 0;
|
|
624
653
|
if (body2 !== null && body2 !== void 0) scanStoredFields(body2);
|
|
625
654
|
function scanStoredFields(body3) {
|
|
626
655
|
const pending = [...body3.body];
|
|
627
656
|
function recordStoredField(current) {
|
|
628
657
|
if (current.type === AST_NODE_TYPES2.ArrowFunctionExpression || current.type === AST_NODE_TYPES2.FunctionExpression || current.type === AST_NODE_TYPES2.FunctionDeclaration || current.type === AST_NODE_TYPES2.ClassExpression || current.type === AST_NODE_TYPES2.ClassDeclaration) return;
|
|
658
|
+
if (current.type === AST_NODE_TYPES2.VariableDeclarator && current.id.type === AST_NODE_TYPES2.Identifier) localBindings2.add(current.id.name);
|
|
629
659
|
const expression = current.type === AST_NODE_TYPES2.ExpressionStatement ? current.expression : null;
|
|
630
660
|
const storedField = expression?.type === AST_NODE_TYPES2.AssignmentExpression && expression.left.type === AST_NODE_TYPES2.MemberExpression && expression.left.object.type === AST_NODE_TYPES2.ThisExpression ? staticMemberName(expression.left) : null;
|
|
631
661
|
if (expression?.type !== AST_NODE_TYPES2.AssignmentExpression || !STORAGE_ASSIGNMENT_OPERATORS.has(expression.operator) || expression.left.type !== AST_NODE_TYPES2.MemberExpression || expression.left.object.type !== AST_NODE_TYPES2.ThisExpression || storedField === null) {
|
|
@@ -636,6 +666,7 @@ var readConstructor = (ctor, declared, typeParameters) => {
|
|
|
636
666
|
while (source.type === AST_NODE_TYPES2.TSNonNullExpression || source.type === AST_NODE_TYPES2.TSAsExpression || source.type === AST_NODE_TYPES2.TSSatisfiesExpression || source.type === AST_NODE_TYPES2.TSTypeAssertion) source = source.expression;
|
|
637
667
|
if (source.type === AST_NODE_TYPES2.NewExpression) {
|
|
638
668
|
constructedFields += 1;
|
|
669
|
+
constructedFieldNames.add(storedField);
|
|
639
670
|
} else if (source.type === AST_NODE_TYPES2.Identifier) {
|
|
640
671
|
const fields = storedFieldsFrom.get(source.name) ?? /* @__PURE__ */ new Set();
|
|
641
672
|
fields.add(storedField);
|
|
@@ -668,15 +699,19 @@ var readConstructor = (ctor, declared, typeParameters) => {
|
|
|
668
699
|
const collaborators = [];
|
|
669
700
|
function collectCollaborators(parameter) {
|
|
670
701
|
for (const reference of parameterCollaborators(parameter, declared, storedMemberFieldsFrom)) {
|
|
671
|
-
|
|
672
|
-
|
|
702
|
+
if (localBindings2.has(reference.name)) continue;
|
|
703
|
+
let fields = reference.fields;
|
|
704
|
+
if (parameter.type === AST_NODE_TYPES2.TSParameterProperty) fields = [reference.name];
|
|
705
|
+
else if (fields.length === 0) fields = [...storedFieldsFrom.get(reference.name) ?? []];
|
|
706
|
+
const injectedFields = fields.filter((field) => !constructedFieldNames.has(field));
|
|
707
|
+
if (injectedFields.length === 0) continue;
|
|
673
708
|
if (CONFIGISH_TYPE_RE.test(reference.typeName)) continue;
|
|
674
709
|
if (CONFIGISH_NAME_RE.test(reference.name)) continue;
|
|
675
710
|
if (typeParameters.has(reference.typeName)) continue;
|
|
676
711
|
if (BUILTIN_CONTAINER_TYPE_RE.test(reference.typeName)) continue;
|
|
677
712
|
if (VALUE_TYPE_RE.test(reference.typeName)) continue;
|
|
678
713
|
if (declared().functionAliases.has(reference.typeName)) continue;
|
|
679
|
-
collaborators.push({ ...reference, fields });
|
|
714
|
+
collaborators.push({ ...reference, fields: injectedFields });
|
|
680
715
|
}
|
|
681
716
|
}
|
|
682
717
|
for (const parameter of ctor.value.params) {
|
|
@@ -895,11 +930,7 @@ function declaredMemberName(member) {
|
|
|
895
930
|
function localInterfaceSurfaces(program) {
|
|
896
931
|
const interfaces = /* @__PURE__ */ new Map();
|
|
897
932
|
const parents = /* @__PURE__ */ new Map();
|
|
898
|
-
const functionAliases =
|
|
899
|
-
for (const statement of program.body) {
|
|
900
|
-
const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
|
|
901
|
-
if (declaration?.type === AST_NODE_TYPES2.TSTypeAliasDeclaration && (declaration.typeAnnotation.type === AST_NODE_TYPES2.TSFunctionType || declaration.typeAnnotation.type === AST_NODE_TYPES2.TSConstructorType)) functionAliases.add(declaration.id.name);
|
|
902
|
-
}
|
|
933
|
+
const functionAliases = localFunctionAliases(program);
|
|
903
934
|
function collectInterfaceSurface(statement) {
|
|
904
935
|
const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration ? statement.declaration : statement;
|
|
905
936
|
if (declaration?.type === AST_NODE_TYPES2.TSTypeAliasDeclaration) {
|
|
@@ -911,6 +942,10 @@ function localInterfaceSurfaces(program) {
|
|
|
911
942
|
inherited.push(part.typeName.name);
|
|
912
943
|
continue;
|
|
913
944
|
}
|
|
945
|
+
if (part.type === AST_NODE_TYPES2.TSTypeReference) {
|
|
946
|
+
inherited.push("*");
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
914
949
|
if (part.type !== AST_NODE_TYPES2.TSTypeLiteral) continue;
|
|
915
950
|
addCallableMembers(part.members, callables2);
|
|
916
951
|
}
|
|
@@ -1074,30 +1109,18 @@ function createServiceOperationResolver(program) {
|
|
|
1074
1109
|
if (retained.size === 0) return [];
|
|
1075
1110
|
const callers = /* @__PURE__ */ new Map();
|
|
1076
1111
|
const active = /* @__PURE__ */ new Set();
|
|
1077
|
-
for (const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1087
|
-
if (current.type === AST_NODE_TYPES2.CallExpression) {
|
|
1088
|
-
if (retained.has(invokedInstanceField(current) ?? "")) active.add(name);
|
|
1089
|
-
if (current.callee.type === AST_NODE_TYPES2.MemberExpression && current.callee.object.type === AST_NODE_TYPES2.ThisExpression) {
|
|
1090
|
-
const callee = staticMemberName(current.callee);
|
|
1091
|
-
if (callee !== null) {
|
|
1092
|
-
const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
|
|
1093
|
-
linked.add(name);
|
|
1094
|
-
callers.set(callee, linked);
|
|
1095
|
-
}
|
|
1112
|
+
for (const [name, body2] of serviceOperationBodies(node)) {
|
|
1113
|
+
forEachServiceCall(body2, (call) => {
|
|
1114
|
+
if (retained.has(invokedInstanceField(call) ?? "")) active.add(name);
|
|
1115
|
+
if (call.callee.type === AST_NODE_TYPES2.MemberExpression && call.callee.object.type === AST_NODE_TYPES2.ThisExpression) {
|
|
1116
|
+
const callee = staticMemberName(call.callee);
|
|
1117
|
+
if (callee !== null) {
|
|
1118
|
+
const linked = callers.get(callee) ?? /* @__PURE__ */ new Set();
|
|
1119
|
+
linked.add(name);
|
|
1120
|
+
callers.set(callee, linked);
|
|
1096
1121
|
}
|
|
1097
1122
|
}
|
|
1098
|
-
|
|
1099
|
-
};
|
|
1100
|
-
for (const statement of member.value.body.body) visit(statement);
|
|
1123
|
+
});
|
|
1101
1124
|
}
|
|
1102
1125
|
const pending = [...active];
|
|
1103
1126
|
while (pending.length > 0) {
|
|
@@ -1112,38 +1135,127 @@ function createServiceOperationResolver(program) {
|
|
|
1112
1135
|
return publicMethodNames(node.body, declared().functionAliases).filter((name) => active.has(name));
|
|
1113
1136
|
};
|
|
1114
1137
|
}
|
|
1138
|
+
function serviceOperationBodies(node) {
|
|
1139
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (const member of node.body.body) {
|
|
1141
|
+
if (member.type !== AST_NODE_TYPES2.MethodDefinition && member.type !== AST_NODE_TYPES2.PropertyDefinition) continue;
|
|
1142
|
+
if (member.static || member.decorators.length > 0) continue;
|
|
1143
|
+
if (member.type === AST_NODE_TYPES2.MethodDefinition && member.kind !== "method") continue;
|
|
1144
|
+
if (member.value?.type !== AST_NODE_TYPES2.FunctionExpression && member.value?.type !== AST_NODE_TYPES2.ArrowFunctionExpression) continue;
|
|
1145
|
+
const name = declaredMemberName(member);
|
|
1146
|
+
if (name !== null && member.value.body !== null) bodies.set(name, member.value.body);
|
|
1147
|
+
}
|
|
1148
|
+
return bodies;
|
|
1149
|
+
}
|
|
1150
|
+
function forEachServiceCall(node, onCall) {
|
|
1151
|
+
if (node.type === AST_NODE_TYPES2.FunctionDeclaration || node.type === AST_NODE_TYPES2.FunctionExpression || node.type === AST_NODE_TYPES2.ArrowFunctionExpression || node.type === AST_NODE_TYPES2.ClassDeclaration || node.type === AST_NODE_TYPES2.ClassExpression) return;
|
|
1152
|
+
if (node.type === AST_NODE_TYPES2.BlockStatement) {
|
|
1153
|
+
for (const statement of node.body) {
|
|
1154
|
+
forEachServiceCall(statement, onCall);
|
|
1155
|
+
if (terminatesOperation(statement)) break;
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
const branch = literalExecutionBranch(node);
|
|
1160
|
+
if (branch !== null) {
|
|
1161
|
+
forEachServiceCall(branch, onCall);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (node.type === AST_NODE_TYPES2.CallExpression) onCall(node);
|
|
1165
|
+
forEachOwnAstChild(node, (child) => {
|
|
1166
|
+
forEachServiceCall(child, onCall);
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
function literalExecutionBranch(node) {
|
|
1170
|
+
if (node.type === AST_NODE_TYPES2.WhileStatement && node.test.type === AST_NODE_TYPES2.Literal && node.test.value === false) return node.test;
|
|
1171
|
+
if ((node.type === AST_NODE_TYPES2.IfStatement || node.type === AST_NODE_TYPES2.ConditionalExpression) && node.test.type === AST_NODE_TYPES2.Literal && typeof node.test.value === "boolean") {
|
|
1172
|
+
return node.test.value ? node.consequent : node.alternate ?? node.test;
|
|
1173
|
+
}
|
|
1174
|
+
if (node.type !== AST_NODE_TYPES2.LogicalExpression || node.left.type !== AST_NODE_TYPES2.Literal || typeof node.left.value !== "boolean") return null;
|
|
1175
|
+
if (node.operator === "&&") return node.left.value ? node.right : node.left;
|
|
1176
|
+
if (node.operator === "||") return node.left.value ? node.left : node.right;
|
|
1177
|
+
return node.left;
|
|
1178
|
+
}
|
|
1179
|
+
function terminatesOperation(node) {
|
|
1180
|
+
if (node.type === AST_NODE_TYPES2.ReturnStatement || node.type === AST_NODE_TYPES2.ThrowStatement) return true;
|
|
1181
|
+
if (node.type === AST_NODE_TYPES2.BlockStatement) return node.body.some(terminatesOperation);
|
|
1182
|
+
if (node.type !== AST_NODE_TYPES2.IfStatement) return false;
|
|
1183
|
+
const branch = literalExecutionBranch(node);
|
|
1184
|
+
if (branch !== null) return terminatesOperation(branch);
|
|
1185
|
+
return node.alternate !== null && terminatesOperation(node.consequent) && terminatesOperation(node.alternate);
|
|
1186
|
+
}
|
|
1115
1187
|
function hasDeclaredServiceContract(node, operations, program) {
|
|
1116
|
-
const classes = localClassAbstractness(program);
|
|
1117
1188
|
const interfaces = localInterfaceSurfaces(program);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1189
|
+
const classes = localClassDeclarations(program);
|
|
1190
|
+
const functionAliases = fileTypeIndex(program).functionAliases;
|
|
1191
|
+
const covered2 = /* @__PURE__ */ new Set();
|
|
1192
|
+
const pending = [node];
|
|
1193
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1194
|
+
function addImplementedContracts(current) {
|
|
1195
|
+
for (const implementation of current.implements) {
|
|
1196
|
+
if (implementation.expression.type !== AST_NODE_TYPES2.Identifier) return false;
|
|
1197
|
+
const name = implementation.expression.name;
|
|
1198
|
+
const surface = interfaces.get(name);
|
|
1199
|
+
const base = classes.get(name);
|
|
1200
|
+
if (surface === void 0 && base === void 0) return false;
|
|
1201
|
+
if (surface?.has("*")) return false;
|
|
1202
|
+
for (const method of surface ?? []) covered2.add(method);
|
|
1203
|
+
if (base?.abstract) pending.push(base);
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
while (pending.length > 0) {
|
|
1208
|
+
const current = pending.pop();
|
|
1209
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
1210
|
+
seen.add(current);
|
|
1211
|
+
if (current.decorators.length > 0) return null;
|
|
1212
|
+
if (current.abstract) {
|
|
1213
|
+
for (const method of abstractContractOperations(current, functionAliases)) covered2.add(method);
|
|
1214
|
+
}
|
|
1215
|
+
if (!addImplementedContracts(current)) return null;
|
|
1216
|
+
if (current.superClass === null) continue;
|
|
1217
|
+
if (current.superClass.type !== AST_NODE_TYPES2.Identifier) return null;
|
|
1218
|
+
const base = classes.get(current.superClass.name);
|
|
1219
|
+
if (base === void 0) return null;
|
|
1220
|
+
pending.push(base);
|
|
1221
|
+
}
|
|
1222
|
+
return operations.every((name) => covered2.has(name));
|
|
1223
|
+
}
|
|
1224
|
+
function localClassDeclarations(program) {
|
|
1225
|
+
const classes = /* @__PURE__ */ new Map();
|
|
1226
|
+
for (const statement of program.body) {
|
|
1227
|
+
const declaration = statement.type === AST_NODE_TYPES2.ExportNamedDeclaration || statement.type === AST_NODE_TYPES2.ExportDefaultDeclaration ? statement.declaration : statement;
|
|
1228
|
+
if (declaration?.type === AST_NODE_TYPES2.ClassDeclaration && declaration.id !== null) classes.set(declaration.id.name, declaration);
|
|
1229
|
+
}
|
|
1230
|
+
return classes;
|
|
1231
|
+
}
|
|
1232
|
+
function abstractContractOperations(node, functionAliases) {
|
|
1233
|
+
const methods = new Set(publicMethodNames(node.body, functionAliases));
|
|
1234
|
+
for (const member of node.body.body) {
|
|
1235
|
+
if (member.type !== AST_NODE_TYPES2.TSAbstractMethodDefinition && member.type !== AST_NODE_TYPES2.TSAbstractPropertyDefinition) continue;
|
|
1236
|
+
if (member.static || member.accessibility === "private" || member.accessibility === "protected") continue;
|
|
1237
|
+
if (member.type === AST_NODE_TYPES2.TSAbstractPropertyDefinition) {
|
|
1238
|
+
const annotation = member.typeAnnotation?.typeAnnotation;
|
|
1239
|
+
if (annotation?.type !== AST_NODE_TYPES2.TSFunctionType && !(annotation?.type === AST_NODE_TYPES2.TSTypeReference && annotation.typeName.type === AST_NODE_TYPES2.Identifier && functionAliases.has(annotation.typeName.name))) continue;
|
|
1240
|
+
}
|
|
1241
|
+
const name = declaredMemberName(member);
|
|
1242
|
+
if (name !== null) methods.add(name);
|
|
1243
|
+
}
|
|
1244
|
+
return methods;
|
|
1134
1245
|
}
|
|
1135
1246
|
|
|
1136
1247
|
// src/rules/require-explicit-service-contract.ts
|
|
1137
1248
|
var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
|
|
1138
1249
|
defaultLevel: "warning",
|
|
1139
1250
|
summary: "Require explicit contracts for classes that invoke retained collaborators.",
|
|
1140
|
-
rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that
|
|
1141
|
-
remediation: "
|
|
1251
|
+
rationale: "A one-operation service can orchestrate a collaborator without declaring the contract that callers consume and implementations and fakes satisfy.",
|
|
1252
|
+
remediation: "Explicitly implement a focused interface or extend an abstract class covering the service operations. Export the contract only when its consumers need it.",
|
|
1142
1253
|
category: "architecture",
|
|
1143
1254
|
autofix: "none",
|
|
1144
1255
|
limitations: [
|
|
1145
1256
|
"The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.",
|
|
1146
|
-
"
|
|
1257
|
+
"Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.",
|
|
1258
|
+
"Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.",
|
|
1147
1259
|
"Generated files and JavaScript without implements syntax are excluded."
|
|
1148
1260
|
],
|
|
1149
1261
|
examples: [
|
|
@@ -1165,7 +1277,7 @@ var REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION = {
|
|
|
1165
1277
|
outcome: "no-match",
|
|
1166
1278
|
files: [{
|
|
1167
1279
|
path: "src/service.ts",
|
|
1168
|
-
source: "
|
|
1280
|
+
source: "interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"
|
|
1169
1281
|
}],
|
|
1170
1282
|
focusPath: "src/service.ts",
|
|
1171
1283
|
expectedCount: 0,
|
|
@@ -1181,7 +1293,7 @@ var require_explicit_service_contract_default = createRule({
|
|
|
1181
1293
|
docs: { description: REQUIRE_EXPLICIT_SERVICE_CONTRACT_DOCUMENTATION.summary },
|
|
1182
1294
|
schema: [],
|
|
1183
1295
|
messages: {
|
|
1184
|
-
requireExplicitServiceContract: "`{{name}}` invokes a retained collaborator from {{operations}} without
|
|
1296
|
+
requireExplicitServiceContract: "`{{name}}` invokes a retained collaborator from {{operations}} without a declared interface or abstract-class contract covering those operations. Implement a focused contract; it can remain local."
|
|
1185
1297
|
}
|
|
1186
1298
|
},
|
|
1187
1299
|
defaultOptions: [],
|
|
@@ -1192,7 +1304,7 @@ var require_explicit_service_contract_default = createRule({
|
|
|
1192
1304
|
const isExportedServiceClass = createExportedServiceClassResolver(program);
|
|
1193
1305
|
return {
|
|
1194
1306
|
ClassDeclaration(node) {
|
|
1195
|
-
if (node.id === null || node.abstract || !isServiceRole(node.id.name, context.filename)) return;
|
|
1307
|
+
if (node.id === null || node.abstract || node.decorators.length > 0 || !isServiceRole(node.id.name, context.filename)) return;
|
|
1196
1308
|
if (isExportedServiceClass(node) && !isTestFile(context.filename) && !isStoryFile(context.filename) && !isScriptFile(context.filename)) return;
|
|
1197
1309
|
const operations = operationsFor(node);
|
|
1198
1310
|
if (operations.length === 0 || hasDeclaredServiceContract(node, operations, program) !== false) return;
|
|
@@ -7031,7 +7143,7 @@ var RESHAPING_METHODS = /* @__PURE__ */ new Set([
|
|
|
7031
7143
|
function importedName2(specifier) {
|
|
7032
7144
|
return specifier.imported.type === AST_NODE_TYPES34.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
|
|
7033
7145
|
}
|
|
7034
|
-
function
|
|
7146
|
+
function memberName2(node) {
|
|
7035
7147
|
if (!node.computed && node.property.type === AST_NODE_TYPES34.Identifier) {
|
|
7036
7148
|
return node.property.name;
|
|
7037
7149
|
}
|
|
@@ -7116,7 +7228,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
|
|
|
7116
7228
|
if (callee.type !== AST_NODE_TYPES34.MemberExpression || callee.object.type !== AST_NODE_TYPES34.CallExpression) {
|
|
7117
7229
|
return null;
|
|
7118
7230
|
}
|
|
7119
|
-
const method =
|
|
7231
|
+
const method = memberName2(callee);
|
|
7120
7232
|
if (method === null) return null;
|
|
7121
7233
|
calls.push({ method, node: current });
|
|
7122
7234
|
current = callee.object;
|
|
@@ -7130,7 +7242,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
|
|
|
7130
7242
|
if (callee.type !== AST_NODE_TYPES34.MemberExpression || callee.object.type !== AST_NODE_TYPES34.Identifier || !namespaces.has(callee.object.name) || !resolvesToTrackedImport(callee.object)) {
|
|
7131
7243
|
return null;
|
|
7132
7244
|
}
|
|
7133
|
-
const name =
|
|
7245
|
+
const name = memberName2(callee);
|
|
7134
7246
|
return name !== null && KINDS.has(name) ? name : null;
|
|
7135
7247
|
}
|
|
7136
7248
|
function isInsideReshapingCall(node) {
|
|
@@ -7139,7 +7251,7 @@ var no_impossible_zod_literal_bounds_default = createRule({
|
|
|
7139
7251
|
while (parent !== void 0 && parent.type !== AST_NODE_TYPES34.Program) {
|
|
7140
7252
|
if (parent.type === AST_NODE_TYPES34.CallExpression) {
|
|
7141
7253
|
const callee = parent.callee;
|
|
7142
|
-
if (callee.type === AST_NODE_TYPES34.MemberExpression && RESHAPING_METHODS.has(
|
|
7254
|
+
if (callee.type === AST_NODE_TYPES34.MemberExpression && RESHAPING_METHODS.has(memberName2(callee) ?? "") && (callee.object.type === AST_NODE_TYPES34.CallExpression || callee.object.type === AST_NODE_TYPES34.Identifier && namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object))) {
|
|
7143
7255
|
return true;
|
|
7144
7256
|
}
|
|
7145
7257
|
if (callee.type === AST_NODE_TYPES34.Identifier && preprocessors.has(callee.name) && resolvesToTrackedImport(callee)) {
|
|
@@ -7405,7 +7517,7 @@ var NO_REDUNDANT_OPTIONAL_ARRAY_DEFAULT_DOCUMENTATION = {
|
|
|
7405
7517
|
function importedName3(specifier) {
|
|
7406
7518
|
return specifier.imported.type === AST_NODE_TYPES35.Identifier ? specifier.imported.name : typeof specifier.imported.value === "string" ? specifier.imported.value : null;
|
|
7407
7519
|
}
|
|
7408
|
-
function
|
|
7520
|
+
function memberName3(node) {
|
|
7409
7521
|
if (!node.computed && node.property.type === AST_NODE_TYPES35.Identifier) {
|
|
7410
7522
|
return node.property.name;
|
|
7411
7523
|
}
|
|
@@ -7457,7 +7569,7 @@ var no_redundant_optional_array_default_default = createRule({
|
|
|
7457
7569
|
return arrayConstructors.has(callee.name) && resolvesToTrackedImport(callee);
|
|
7458
7570
|
}
|
|
7459
7571
|
if (callee.type !== AST_NODE_TYPES35.MemberExpression) return false;
|
|
7460
|
-
const method =
|
|
7572
|
+
const method = memberName3(callee);
|
|
7461
7573
|
if (method === "array" && (callee.object.type === AST_NODE_TYPES35.Identifier ? namespaces.has(callee.object.name) && resolvesToTrackedImport(callee.object) : isZodSchemaExpression(callee.object))) {
|
|
7462
7574
|
return true;
|
|
7463
7575
|
}
|
|
@@ -7498,12 +7610,12 @@ var no_redundant_optional_array_default_default = createRule({
|
|
|
7498
7610
|
},
|
|
7499
7611
|
CallExpression(node) {
|
|
7500
7612
|
const defaultCallee = node.callee;
|
|
7501
|
-
if (defaultCallee.type !== AST_NODE_TYPES35.MemberExpression ||
|
|
7613
|
+
if (defaultCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName3(defaultCallee) !== "default" || defaultCallee.object.type !== AST_NODE_TYPES35.CallExpression) {
|
|
7502
7614
|
return;
|
|
7503
7615
|
}
|
|
7504
7616
|
const optionalCall = defaultCallee.object;
|
|
7505
7617
|
const optionalCallee = optionalCall.callee;
|
|
7506
|
-
if (optionalCallee.type !== AST_NODE_TYPES35.MemberExpression ||
|
|
7618
|
+
if (optionalCallee.type !== AST_NODE_TYPES35.MemberExpression || memberName3(optionalCallee) !== "optional" || optionalCall.arguments.length !== 0 || !isArraySchemaExpression(optionalCallee.object) || sourceCode.getCommentsInside(optionalCall).length > 0) {
|
|
7507
7619
|
return;
|
|
7508
7620
|
}
|
|
7509
7621
|
context.report({
|
|
@@ -7527,12 +7639,12 @@ import * as ts8 from "typescript";
|
|
|
7527
7639
|
import {
|
|
7528
7640
|
AST_NODE_TYPES as AST_NODE_TYPES36
|
|
7529
7641
|
} from "@typescript-eslint/utils";
|
|
7530
|
-
function
|
|
7642
|
+
function memberName4(member) {
|
|
7531
7643
|
return !member.computed && member.key.type === AST_NODE_TYPES36.Identifier ? member.key.name : null;
|
|
7532
7644
|
}
|
|
7533
7645
|
function convertibleMemberName(member) {
|
|
7534
7646
|
if (member.type !== AST_NODE_TYPES36.MethodDefinition && member.type !== AST_NODE_TYPES36.PropertyDefinition && member.type !== AST_NODE_TYPES36.AccessorProperty) return null;
|
|
7535
|
-
return
|
|
7647
|
+
return memberName4(member);
|
|
7536
7648
|
}
|
|
7537
7649
|
|
|
7538
7650
|
// src/rules/interface-contract-members-private.ts
|
|
@@ -8134,7 +8246,7 @@ var LOWER_CAMEL_CASE_RE = /^[a-z][A-Za-z0-9]*$/;
|
|
|
8134
8246
|
function propertyName2(node) {
|
|
8135
8247
|
return node.type === AST_NODE_TYPES40.Identifier ? node.name : null;
|
|
8136
8248
|
}
|
|
8137
|
-
function
|
|
8249
|
+
function memberName5(node) {
|
|
8138
8250
|
let current = node;
|
|
8139
8251
|
while (current.type === AST_NODE_TYPES40.TSAsExpression || current.type === AST_NODE_TYPES40.TSNonNullExpression || current.type === AST_NODE_TYPES40.TSTypeAssertion) {
|
|
8140
8252
|
current = current.expression;
|
|
@@ -8198,7 +8310,7 @@ var no_bespoke_api_case_conversion_default = createRule({
|
|
|
8198
8310
|
Property(node) {
|
|
8199
8311
|
if (node.computed || node.method || node.shorthand) return;
|
|
8200
8312
|
const key = propertyName2(node.key);
|
|
8201
|
-
const value =
|
|
8313
|
+
const value = memberName5(node.value);
|
|
8202
8314
|
if (key === null || value === null || !isDirectCaseTranslation(key, value)) return;
|
|
8203
8315
|
if (!hasApiReceiver(node.value)) return;
|
|
8204
8316
|
const wireName = SNAKE_CASE_RE2.test(key) ? key : value;
|
|
@@ -8971,9 +9083,9 @@ var no_positional_tuple_return_default = createRule({
|
|
|
8971
9083
|
TSMethodSignature(node) {
|
|
8972
9084
|
const owner = owningInterface(node);
|
|
8973
9085
|
const alias = owningTypeAlias(node);
|
|
8974
|
-
const
|
|
8975
|
-
if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 ||
|
|
8976
|
-
report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${
|
|
9086
|
+
const memberName9 = staticMemberName4(node.key);
|
|
9087
|
+
if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || node.returnType === void 0 || memberName9 === null) return;
|
|
9088
|
+
report2(node.returnType.typeAnnotation, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
|
|
8977
9089
|
},
|
|
8978
9090
|
TSTypeAliasDeclaration(node) {
|
|
8979
9091
|
if (!typeExports.has(node.id.name)) return;
|
|
@@ -8984,11 +9096,11 @@ var no_positional_tuple_return_default = createRule({
|
|
|
8984
9096
|
const owner = owningInterface(node);
|
|
8985
9097
|
const alias = owningTypeAlias(node);
|
|
8986
9098
|
const annotation = node.typeAnnotation?.typeAnnotation;
|
|
8987
|
-
const
|
|
8988
|
-
if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) ||
|
|
9099
|
+
const memberName9 = staticMemberName4(node.key);
|
|
9100
|
+
if ((owner === null || !isExportedInterface(owner, typeExports)) && (alias === null || !typeExports.has(alias.id.name)) || memberName9 === null || annotation === void 0) return;
|
|
8989
9101
|
const returnType = callableReturnType(annotation, aliases);
|
|
8990
9102
|
if (returnType !== null) {
|
|
8991
|
-
report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${
|
|
9103
|
+
report2(returnType, `${owner?.id.name ?? alias?.id.name ?? "type"}.${memberName9}`);
|
|
8992
9104
|
}
|
|
8993
9105
|
},
|
|
8994
9106
|
PropertyDefinition(node) {
|
|
@@ -14723,7 +14835,8 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
|
14723
14835
|
remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
|
|
14724
14836
|
category: "correctness",
|
|
14725
14837
|
limitations: [
|
|
14726
|
-
"Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven."
|
|
14838
|
+
"Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven.",
|
|
14839
|
+
"Local type aliases are resolved before recognizing standard readonly types; imported or otherwise unknown type declarations are not inferred from their names. Readonly types have no runtime enforcement."
|
|
14727
14840
|
],
|
|
14728
14841
|
examples: [
|
|
14729
14842
|
{
|
|
@@ -14743,6 +14856,26 @@ var PREFER_IMMUTABLE_MODULE_CONSTANT_DOCUMENTATION = {
|
|
|
14743
14856
|
focusPath: "src/constants.ts",
|
|
14744
14857
|
expectedCount: 1,
|
|
14745
14858
|
public: true
|
|
14859
|
+
},
|
|
14860
|
+
{
|
|
14861
|
+
id: "shadowed-readonly-alias",
|
|
14862
|
+
scenarioId: "type-binding",
|
|
14863
|
+
title: "A type named Readonly still exposes mutable properties",
|
|
14864
|
+
outcome: "match",
|
|
14865
|
+
files: [{ path: "src/constants.ts", source: "type Readonly = { voice: boolean };\nexport const TOOL_SUPPORT: Readonly = { voice: true };" }],
|
|
14866
|
+
focusPath: "src/constants.ts",
|
|
14867
|
+
expectedCount: 1,
|
|
14868
|
+
public: true
|
|
14869
|
+
},
|
|
14870
|
+
{
|
|
14871
|
+
id: "resolved-readonly-alias",
|
|
14872
|
+
scenarioId: "type-binding",
|
|
14873
|
+
title: "A local alias resolves to readonly properties",
|
|
14874
|
+
outcome: "no-match",
|
|
14875
|
+
files: [{ path: "src/constants.ts", source: "type ToolSupport = Readonly<{ voice: boolean }>;\nexport const TOOL_SUPPORT: ToolSupport = { voice: true };" }],
|
|
14876
|
+
focusPath: "src/constants.ts",
|
|
14877
|
+
expectedCount: 0,
|
|
14878
|
+
public: true
|
|
14746
14879
|
}
|
|
14747
14880
|
]
|
|
14748
14881
|
};
|
|
@@ -14797,39 +14930,84 @@ function collectionKind(node, isUnshadowedGlobal3) {
|
|
|
14797
14930
|
}
|
|
14798
14931
|
return null;
|
|
14799
14932
|
}
|
|
14800
|
-
function declaredReadonlyType(node, kind,
|
|
14933
|
+
function declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) {
|
|
14801
14934
|
const annotation = node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation : void 0;
|
|
14802
|
-
if (annotation !== void 0 &&
|
|
14935
|
+
if (annotation !== void 0 && readonlyTypeState(annotation.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly") {
|
|
14803
14936
|
return true;
|
|
14804
14937
|
}
|
|
14805
|
-
return node.init?.type === AST_NODE_TYPES72.TSAsExpression &&
|
|
14938
|
+
return node.init?.type === AST_NODE_TYPES72.TSAsExpression && readonlyTypeState(node.init.typeAnnotation, kind, resolveLocalType, isUnshadowedType) === "readonly";
|
|
14806
14939
|
}
|
|
14807
|
-
function
|
|
14808
|
-
if (
|
|
14809
|
-
if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier)
|
|
14810
|
-
|
|
14811
|
-
|
|
14812
|
-
|
|
14813
|
-
|
|
14940
|
+
function readonlyTypeState(node, kind, resolveLocalType, isUnshadowedType, arguments_ = /* @__PURE__ */ new Map(), seen = /* @__PURE__ */ new Set()) {
|
|
14941
|
+
if (seen.has(node)) return "unknown";
|
|
14942
|
+
if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier) {
|
|
14943
|
+
return directReadonlyState(node, kind, isUnshadowedType);
|
|
14944
|
+
}
|
|
14945
|
+
const declaration = resolveLocalType(node.typeName);
|
|
14946
|
+
if (declaration === void 0) return directReadonlyState(node, kind, isUnshadowedType);
|
|
14947
|
+
if (declaration.type === AST_NODE_TYPES72.TSTypeParameter) {
|
|
14948
|
+
const argument = arguments_.get(declaration);
|
|
14949
|
+
return argument === void 0 ? "unknown" : readonlyTypeState(
|
|
14950
|
+
argument.node,
|
|
14951
|
+
kind,
|
|
14952
|
+
resolveLocalType,
|
|
14953
|
+
isUnshadowedType,
|
|
14954
|
+
argument.arguments,
|
|
14955
|
+
argument.seen
|
|
14956
|
+
);
|
|
14957
|
+
}
|
|
14958
|
+
const expanded = /* @__PURE__ */ new Set([...seen, node]);
|
|
14959
|
+
return readonlyTypeState(
|
|
14960
|
+
declaration.typeAnnotation,
|
|
14961
|
+
kind,
|
|
14962
|
+
resolveLocalType,
|
|
14963
|
+
isUnshadowedType,
|
|
14964
|
+
bindTypeArguments(declaration, node, arguments_, expanded),
|
|
14965
|
+
expanded
|
|
14966
|
+
);
|
|
14967
|
+
}
|
|
14968
|
+
function bindTypeArguments(declaration, reference, arguments_, seen) {
|
|
14969
|
+
const bindings = new Map(arguments_);
|
|
14970
|
+
for (const [index, parameter] of (declaration.typeParameters?.params ?? []).entries()) {
|
|
14971
|
+
const argument = reference.typeArguments?.params[index];
|
|
14972
|
+
const node = argument ?? parameter.default;
|
|
14973
|
+
if (node !== void 0) {
|
|
14974
|
+
bindings.set(parameter, { node, arguments: argument === void 0 ? new Map(bindings) : arguments_, seen });
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
return bindings;
|
|
14814
14978
|
}
|
|
14815
|
-
function
|
|
14979
|
+
function directReadonlyState(node, kind, isUnshadowedType) {
|
|
14816
14980
|
if (node.type === AST_NODE_TYPES72.TSTypeOperator && node.operator === "readonly") {
|
|
14817
|
-
return
|
|
14981
|
+
return "readonly";
|
|
14818
14982
|
}
|
|
14819
|
-
if (node.type
|
|
14820
|
-
return
|
|
14983
|
+
if (node.type === AST_NODE_TYPES72.TSTypeLiteral) {
|
|
14984
|
+
if (node.members.length === 0) return "unknown";
|
|
14985
|
+
const readonly2 = kind === "literal" && node.members.length > 0 && node.members.every(
|
|
14986
|
+
(member) => (member.type === AST_NODE_TYPES72.TSPropertySignature || member.type === AST_NODE_TYPES72.TSIndexSignature) && member.readonly
|
|
14987
|
+
);
|
|
14988
|
+
return readonly2 ? "readonly" : "mutable";
|
|
14989
|
+
}
|
|
14990
|
+
if (node.type === AST_NODE_TYPES72.TSArrayType || node.type === AST_NODE_TYPES72.TSTupleType) return "mutable";
|
|
14991
|
+
if (node.type !== AST_NODE_TYPES72.TSTypeReference || node.typeName.type !== AST_NODE_TYPES72.Identifier || !isUnshadowedType(node.typeName)) {
|
|
14992
|
+
return "unknown";
|
|
14821
14993
|
}
|
|
14822
14994
|
if (node.typeName.name === "Readonly") {
|
|
14823
|
-
return kind === "literal";
|
|
14995
|
+
return kind === "literal" ? "readonly" : "mutable";
|
|
14824
14996
|
}
|
|
14825
|
-
|
|
14997
|
+
if (["Array", "Map", "Set"].includes(node.typeName.name)) return "mutable";
|
|
14998
|
+
if (!["ReadonlyArray", "ReadonlyMap", "ReadonlySet"].includes(node.typeName.name)) return "unknown";
|
|
14999
|
+
const readonly = kind === "literal" ? node.typeName.name === "ReadonlyArray" : node.typeName.name === `Readonly${kind}`;
|
|
15000
|
+
return readonly ? "readonly" : "mutable";
|
|
14826
15001
|
}
|
|
14827
|
-
function hasUnknownExplicitType(node,
|
|
14828
|
-
const annotation = node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0;
|
|
15002
|
+
function hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType) {
|
|
15003
|
+
const annotation = (node.id.type === AST_NODE_TYPES72.Identifier ? node.id.typeAnnotation?.typeAnnotation : void 0) ?? (node.init?.type === AST_NODE_TYPES72.TSAsExpression ? node.init.typeAnnotation : void 0);
|
|
14829
15004
|
if (annotation === void 0) return false;
|
|
14830
15005
|
if (annotation.type === AST_NODE_TYPES72.TSArrayType || annotation.type === AST_NODE_TYPES72.TSTypeOperator) return false;
|
|
14831
15006
|
if (annotation.type !== AST_NODE_TYPES72.TSTypeReference || annotation.typeName.type !== AST_NODE_TYPES72.Identifier) return true;
|
|
14832
|
-
|
|
15007
|
+
if (resolveLocalType(annotation.typeName) !== void 0) {
|
|
15008
|
+
return readonlyTypeState(annotation, kind, resolveLocalType, isUnshadowedType) === "unknown";
|
|
15009
|
+
}
|
|
15010
|
+
return resolveLocalType(annotation.typeName) === void 0 && (!isUnshadowedType(annotation.typeName) || !["Array", "Map", "Readonly", "ReadonlyArray", "ReadonlyMap", "ReadonlySet", "Set"].includes(annotation.typeName.name));
|
|
14833
15011
|
}
|
|
14834
15012
|
function referenceMutates(identifier, isUnshadowedGlobal3) {
|
|
14835
15013
|
let member = identifier.parent;
|
|
@@ -14872,11 +15050,20 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
14872
15050
|
const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
14873
15051
|
return variable === null || variable.defs.length === 0;
|
|
14874
15052
|
};
|
|
15053
|
+
const isUnshadowedType = (identifier) => {
|
|
15054
|
+
const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
15055
|
+
return variable === null || !variable.defs.some((definition) => definition.isTypeDefinition);
|
|
15056
|
+
};
|
|
15057
|
+
const resolveLocalType = (identifier) => {
|
|
15058
|
+
const variable = ASTUtils32.findVariable(sourceCode.getScope(identifier), identifier.name);
|
|
15059
|
+
const definitions = variable?.defs.filter((definition) => definition.isTypeDefinition) ?? [];
|
|
15060
|
+
const declaration = definitions.length === 1 ? definitions[0]?.node : void 0;
|
|
15061
|
+
return declaration?.type === AST_NODE_TYPES72.TSTypeAliasDeclaration || declaration?.type === AST_NODE_TYPES72.TSTypeParameter ? declaration : void 0;
|
|
15062
|
+
};
|
|
14875
15063
|
if (JAVASCRIPT_FILE_RE.test(context.filename) || isTestFile(context.filename) || isGeneratedFile(context.filename, sourceCode.getText())) {
|
|
14876
15064
|
return {};
|
|
14877
15065
|
}
|
|
14878
15066
|
const exportedNames2 = /* @__PURE__ */ new Set();
|
|
14879
|
-
const typeAliases2 = /* @__PURE__ */ new Map();
|
|
14880
15067
|
const mutatesThroughAlias = (root) => {
|
|
14881
15068
|
const pending = [root];
|
|
14882
15069
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -14901,10 +15088,6 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
14901
15088
|
return {
|
|
14902
15089
|
Program(node) {
|
|
14903
15090
|
function collectModuleBindings(statement) {
|
|
14904
|
-
const declaration = statement.type === AST_NODE_TYPES72.ExportNamedDeclaration ? statement.declaration : statement;
|
|
14905
|
-
if (declaration?.type === AST_NODE_TYPES72.TSTypeAliasDeclaration) {
|
|
14906
|
-
typeAliases2.set(declaration.id.name, declaration.typeAnnotation);
|
|
14907
|
-
}
|
|
14908
15091
|
if (statement.type === AST_NODE_TYPES72.ExportNamedDeclaration) {
|
|
14909
15092
|
if (statement.source !== null || statement.exportKind === "type") return;
|
|
14910
15093
|
for (const specifier of statement.specifiers) {
|
|
@@ -14937,7 +15120,7 @@ var prefer_immutable_module_constant_default = createRule({
|
|
|
14937
15120
|
return;
|
|
14938
15121
|
}
|
|
14939
15122
|
const kind = collectionKind(node.init, isUnshadowedGlobal3);
|
|
14940
|
-
if (kind === null || declaredReadonlyType(node, kind,
|
|
15123
|
+
if (kind === null || declaredReadonlyType(node, kind, resolveLocalType, isUnshadowedType) || hasUnknownExplicitType(node, kind, resolveLocalType, isUnshadowedType)) {
|
|
14941
15124
|
return;
|
|
14942
15125
|
}
|
|
14943
15126
|
const variable = sourceCode.getDeclaredVariables(node)[0];
|
|
@@ -16975,7 +17158,7 @@ var PREFER_NODE_CRYPTO_HASH_DOCUMENTATION = {
|
|
|
16975
17158
|
{ id: "mutable-one-shot-chain", title: "Avoid mutable state for one value", outcome: "match", files: [{ path: "case.ts", source: "import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');" }], focusPath: "case.ts", expectedCount: 1, public: true }
|
|
16976
17159
|
]
|
|
16977
17160
|
};
|
|
16978
|
-
function
|
|
17161
|
+
function memberName6(node) {
|
|
16979
17162
|
if (!node.computed && node.property.type === AST_NODE_TYPES82.Identifier) return node.property.name;
|
|
16980
17163
|
if (node.computed && node.property.type === AST_NODE_TYPES82.Literal && typeof node.property.value === "string") {
|
|
16981
17164
|
return node.property.value;
|
|
@@ -16991,7 +17174,7 @@ function isCryptoLoader(node, resolve2) {
|
|
|
16991
17174
|
if (node.callee.type === AST_NODE_TYPES82.Identifier) {
|
|
16992
17175
|
return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
|
|
16993
17176
|
}
|
|
16994
|
-
return node.callee.type === AST_NODE_TYPES82.MemberExpression && node.callee.object.type === AST_NODE_TYPES82.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) &&
|
|
17177
|
+
return node.callee.type === AST_NODE_TYPES82.MemberExpression && node.callee.object.type === AST_NODE_TYPES82.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName6(node.callee) === "getBuiltinModule";
|
|
16995
17178
|
}
|
|
16996
17179
|
function isCryptoSpecifier(node) {
|
|
16997
17180
|
return node.type === AST_NODE_TYPES82.Literal && (node.value === "crypto" || node.value === "node:crypto");
|
|
@@ -17073,14 +17256,14 @@ function importedName6(node) {
|
|
|
17073
17256
|
return node.type === AST_NODE_TYPES82.Identifier ? node.name : node.value;
|
|
17074
17257
|
}
|
|
17075
17258
|
function isMemberCall(node, name) {
|
|
17076
|
-
return node.callee.type === AST_NODE_TYPES82.MemberExpression &&
|
|
17259
|
+
return node.callee.type === AST_NODE_TYPES82.MemberExpression && memberName6(node.callee) === name;
|
|
17077
17260
|
}
|
|
17078
17261
|
function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
|
|
17079
17262
|
if (node.callee.type === AST_NODE_TYPES82.Identifier) {
|
|
17080
17263
|
const variable2 = resolve2(node.callee);
|
|
17081
17264
|
return variable2 !== null && directBindings.has(variable2);
|
|
17082
17265
|
}
|
|
17083
|
-
if (node.callee.type !== AST_NODE_TYPES82.MemberExpression ||
|
|
17266
|
+
if (node.callee.type !== AST_NODE_TYPES82.MemberExpression || memberName6(node.callee) !== "createHash") {
|
|
17084
17267
|
return false;
|
|
17085
17268
|
}
|
|
17086
17269
|
if (isCryptoLoader(node.callee.object, resolve2)) return true;
|
|
@@ -17108,7 +17291,7 @@ var PREFER_NODE_FS_PROMISES_DOCUMENTATION = {
|
|
|
17108
17291
|
{ id: "sync-read", title: "Do not block on a synchronous read", outcome: "match", files: [{ path: "src/store.ts", source: "import { readFileSync } from 'node:fs'; export function load(path: string) { return readFileSync(path, 'utf8'); }" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
|
|
17109
17292
|
]
|
|
17110
17293
|
};
|
|
17111
|
-
function
|
|
17294
|
+
function memberName7(node) {
|
|
17112
17295
|
if (!node.computed && node.property.type === AST_NODE_TYPES83.Identifier) return node.property.name;
|
|
17113
17296
|
if (node.computed && node.property.type === AST_NODE_TYPES83.Literal && typeof node.property.value === "string") {
|
|
17114
17297
|
return node.property.value;
|
|
@@ -17125,7 +17308,7 @@ function isFsLoader(node, isGlobal) {
|
|
|
17125
17308
|
const [argument] = expression.arguments;
|
|
17126
17309
|
if (argument === void 0 || argument.type === AST_NODE_TYPES83.SpreadElement || !isFsSpecifier(argument)) return false;
|
|
17127
17310
|
if (expression.callee.type === AST_NODE_TYPES83.Identifier) return expression.callee.name === "require" && isGlobal(expression.callee);
|
|
17128
|
-
return expression.callee.type === AST_NODE_TYPES83.MemberExpression && expression.callee.object.type === AST_NODE_TYPES83.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) &&
|
|
17311
|
+
return expression.callee.type === AST_NODE_TYPES83.MemberExpression && expression.callee.object.type === AST_NODE_TYPES83.Identifier && expression.callee.object.name === "process" && isGlobal(expression.callee.object) && memberName7(expression.callee) === "getBuiltinModule";
|
|
17129
17312
|
}
|
|
17130
17313
|
function isFsSpecifier(node) {
|
|
17131
17314
|
return node.type === AST_NODE_TYPES83.Literal && (node.value === "node:fs" || node.value === "fs");
|
|
@@ -17206,7 +17389,7 @@ var prefer_node_fs_promises_default = createRule({
|
|
|
17206
17389
|
}
|
|
17207
17390
|
},
|
|
17208
17391
|
MemberExpression(node) {
|
|
17209
|
-
const name =
|
|
17392
|
+
const name = memberName7(node);
|
|
17210
17393
|
if (name?.endsWith("Sync") !== true) return;
|
|
17211
17394
|
const object = unwrapAwait2(node.object);
|
|
17212
17395
|
if (object.type === AST_NODE_TYPES83.Identifier && isNamespace(object) || isFsLoader(object, isGlobal)) {
|
|
@@ -21405,7 +21588,7 @@ var REQUIRE_SQL_ACCESS_CLASS_DOCUMENTATION = {
|
|
|
21405
21588
|
}
|
|
21406
21589
|
]
|
|
21407
21590
|
};
|
|
21408
|
-
function
|
|
21591
|
+
function memberName8(node) {
|
|
21409
21592
|
if (!node.computed && node.property.type === AST_NODE_TYPES98.Identifier)
|
|
21410
21593
|
return node.property.name;
|
|
21411
21594
|
if (node.computed && node.property.type === AST_NODE_TYPES98.Literal && typeof node.property.value === "string")
|
|
@@ -21423,7 +21606,7 @@ function databaseReceiver(node) {
|
|
|
21423
21606
|
if (node.type === AST_NODE_TYPES98.Identifier)
|
|
21424
21607
|
return DATABASE_NAMES.test(node.name);
|
|
21425
21608
|
if (node.type !== AST_NODE_TYPES98.MemberExpression) return false;
|
|
21426
|
-
const name =
|
|
21609
|
+
const name = memberName8(node);
|
|
21427
21610
|
return name === "DB" || name !== null && DATABASE_NAMES.test(name);
|
|
21428
21611
|
}
|
|
21429
21612
|
function databaseRootMember(node) {
|
|
@@ -21444,7 +21627,7 @@ function databaseRootMember(node) {
|
|
|
21444
21627
|
}
|
|
21445
21628
|
if (current.type === AST_NODE_TYPES98.MemberExpression) {
|
|
21446
21629
|
if (current.object.type === AST_NODE_TYPES98.ThisExpression)
|
|
21447
|
-
return
|
|
21630
|
+
return memberName8(current);
|
|
21448
21631
|
current = current.object;
|
|
21449
21632
|
continue;
|
|
21450
21633
|
}
|
|
@@ -21468,7 +21651,7 @@ function expressionHasDatabaseMarker(node) {
|
|
|
21468
21651
|
continue;
|
|
21469
21652
|
}
|
|
21470
21653
|
if (current.type === AST_NODE_TYPES98.MemberExpression) {
|
|
21471
|
-
const name =
|
|
21654
|
+
const name = memberName8(current);
|
|
21472
21655
|
if (name === "DB" || name !== null && DATABASE_NAMES.test(name))
|
|
21473
21656
|
return true;
|
|
21474
21657
|
current = current.object;
|
|
@@ -21490,7 +21673,7 @@ function expressionCallsBuilder(node) {
|
|
|
21490
21673
|
}
|
|
21491
21674
|
if (current.type === AST_NODE_TYPES98.CallExpression) {
|
|
21492
21675
|
if (current.callee.type !== AST_NODE_TYPES98.MemberExpression) return false;
|
|
21493
|
-
const name =
|
|
21676
|
+
const name = memberName8(current.callee);
|
|
21494
21677
|
if (name !== null && BUILDER_METHODS.has(name)) return true;
|
|
21495
21678
|
current = current.callee.object;
|
|
21496
21679
|
continue;
|
|
@@ -21541,7 +21724,7 @@ function thisRootMember(node) {
|
|
|
21541
21724
|
let current = node;
|
|
21542
21725
|
let root = null;
|
|
21543
21726
|
while (current.type === AST_NODE_TYPES98.MemberExpression) {
|
|
21544
|
-
const name =
|
|
21727
|
+
const name = memberName8(current);
|
|
21545
21728
|
if (name === null) return null;
|
|
21546
21729
|
root = name;
|
|
21547
21730
|
current = current.object;
|
|
@@ -21595,7 +21778,7 @@ var require_sql_access_class_default = createRule({
|
|
|
21595
21778
|
CallExpression(node) {
|
|
21596
21779
|
if (node.callee.type !== AST_NODE_TYPES98.MemberExpression)
|
|
21597
21780
|
return;
|
|
21598
|
-
const method =
|
|
21781
|
+
const method = memberName8(node.callee);
|
|
21599
21782
|
if (knownNonDatabase(node.callee.object)) return;
|
|
21600
21783
|
if (method === null || !isDatabaseOperation(method, node.callee.object))
|
|
21601
21784
|
return;
|
|
@@ -23228,10 +23411,11 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
23228
23411
|
import { dirname as dirname3, join as join3, relative as relative2 } from "path";
|
|
23229
23412
|
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]);
|
|
23230
23413
|
var NEXT_COMPONENT = /^(?:src\/)?app\/(?:.*\/)?(?:page|layout|loading|not-found|template|error|global-error|default|forbidden|unauthorized)\.[jt]sx?$/u;
|
|
23414
|
+
var NEXT_METADATA = /^(?:src\/)?app\/(?:.*\/)?(?:(?:opengraph-image|twitter-image|icon|apple-icon)\.[jt]sx?|(?:robots|sitemap|manifest)\.[jt]s)$/u;
|
|
23231
23415
|
var NEXT_ROUTE = /^(?:src\/)?app\/(?:.*\/)?route\.[jt]s$/u;
|
|
23232
23416
|
function isFrameworkEntrypoint(filename, exportKey) {
|
|
23233
23417
|
const normalized = filename.replaceAll("\\", "/");
|
|
23234
|
-
if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy)\.[jt]s$/u.test(normalized)) return false;
|
|
23418
|
+
if (!/(?:^|\/)(?:app|pages)\/|(?:^|\/)(?:middleware|proxy|instrumentation)\.[jt]s$/u.test(normalized)) return false;
|
|
23235
23419
|
const owner = packageOwner(normalized);
|
|
23236
23420
|
if (owner === null) return false;
|
|
23237
23421
|
const path = relative2(owner.root, normalized).replaceAll("\\", "/");
|
|
@@ -23240,9 +23424,12 @@ function isFrameworkEntrypoint(filename, exportKey) {
|
|
|
23240
23424
|
}
|
|
23241
23425
|
function isNextEntrypoint(path, exportKey) {
|
|
23242
23426
|
const privateAppFolder = /^(?:src\/)?app\/(?:.*\/)?_[^/]+\//u.test(path);
|
|
23243
|
-
if (!privateAppFolder && NEXT_COMPONENT.test(path) && exportKey === "default") return true;
|
|
23427
|
+
if (!privateAppFolder && (NEXT_COMPONENT.test(path) || NEXT_METADATA.test(path)) && exportKey === "default") return true;
|
|
23244
23428
|
if (!privateAppFolder && NEXT_ROUTE.test(path) && HTTP_METHODS.has(exportKey)) return true;
|
|
23245
23429
|
if (/^(?:src\/)?pages\/.*\.[jt]sx?$/u.test(path) && exportKey === "default") return true;
|
|
23430
|
+
if (/^(?:src\/)?instrumentation\.[jt]s$/u.test(path)) {
|
|
23431
|
+
return exportKey === "register" || exportKey === "onRequestError";
|
|
23432
|
+
}
|
|
23246
23433
|
const middleware = /^(?:src\/)?(middleware|proxy)\.[jt]s$/u.exec(path);
|
|
23247
23434
|
return middleware !== null && (exportKey === "default" || exportKey === middleware[1]);
|
|
23248
23435
|
}
|
|
@@ -23285,7 +23472,7 @@ var SOLE_EXPORT_MATCHES_FILENAME_DOCUMENTATION = {
|
|
|
23285
23472
|
"Framework entrypoints, generic stems covered by no-generic-single-export-module, tests, generated files, anonymous defaults, CommonJS, unresolved re-exports, and emission-dependent exports are excluded.",
|
|
23286
23473
|
"The rule compares the primary filename stem and preserves a single private underscore prefix and conventional suffixes such as .server or .worker. Supporting types do not exempt a sole runtime export; public runtime aliases count as distinct exports.",
|
|
23287
23474
|
"Exported destructuring patterns are excluded rather than undercounted as public exports.",
|
|
23288
|
-
"Next.js and Astro
|
|
23475
|
+
"Next.js metadata routes, instrumentation, and other Next.js and Astro entrypoint exemptions require the nearest package.json to declare the framework, a conventional entrypoint path, and its expected runtime export. Custom routing roots and pageExtensions are not inferred."
|
|
23289
23476
|
],
|
|
23290
23477
|
examples: [
|
|
23291
23478
|
{ id: "matching-class", title: "Match a class and module", outcome: "no-match", files: [{ path: "src/artifact-store.ts", source: "export class ArtifactStore {}" }], focusPath: "src/artifact-store.ts", expectedCount: 0, public: true },
|
|
@@ -23854,7 +24041,7 @@ var RULES = {
|
|
|
23854
24041
|
};
|
|
23855
24042
|
var meta = {
|
|
23856
24043
|
name: "@sarj/eslint-plugin",
|
|
23857
|
-
version: "16.
|
|
24044
|
+
version: "16.3.0"
|
|
23858
24045
|
};
|
|
23859
24046
|
var APPLICATION_ONLY_RULES = [];
|
|
23860
24047
|
var LIBRARY_IMPORT_POLICY = ["error", {
|