@sarj/eslint-plugin 15.26.7 → 15.27.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/_docs-XCw_6XV9.d.cts +88 -0
- package/dist/_docs-XCw_6XV9.d.ts +88 -0
- package/dist/index.cjs +91 -26
- package/dist/index.d.cts +5 -88
- package/dist/index.d.ts +5 -88
- package/dist/index.js +91 -26
- package/dist/rule-examples.cjs +112 -0
- package/dist/rule-examples.d.cts +7 -0
- package/dist/rule-examples.d.ts +7 -0
- package/dist/rule-examples.js +77 -0
- package/package.json +4 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview _docs — typed source-owned rule documentation and executable examples.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type RuleCategory = "architecture" | "correctness" | "maintainability" | "performance" | "security" | "style" | "testing";
|
|
9
|
+
type AutofixPolicy = "none" | "safe" | "suggestion";
|
|
10
|
+
type DefaultLevel = "error" | "warning";
|
|
11
|
+
type ExampleOutcome = "match" | "no-match";
|
|
12
|
+
interface ExampleFile {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly source: string;
|
|
15
|
+
}
|
|
16
|
+
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
17
|
+
interface RuleExample {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly scenarioId?: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly outcome: ExampleOutcome;
|
|
22
|
+
readonly files: readonly ExampleFile[];
|
|
23
|
+
readonly focusPath: string;
|
|
24
|
+
readonly expectedCount: number;
|
|
25
|
+
readonly public?: boolean;
|
|
26
|
+
readonly fixedFiles?: readonly ExampleFile[];
|
|
27
|
+
}
|
|
28
|
+
/** Human-authored fields that cannot be derived from the ESLint rule module. */
|
|
29
|
+
interface RuleDocumentation {
|
|
30
|
+
readonly summary: string;
|
|
31
|
+
readonly rationale: string;
|
|
32
|
+
readonly remediation: string;
|
|
33
|
+
readonly category: RuleCategory;
|
|
34
|
+
readonly defaultLevel?: DefaultLevel;
|
|
35
|
+
readonly languages?: readonly "typescript"[];
|
|
36
|
+
readonly autofix?: AutofixPolicy;
|
|
37
|
+
readonly aliases?: readonly string[];
|
|
38
|
+
readonly limitations?: readonly string[];
|
|
39
|
+
readonly filePatterns?: readonly string[];
|
|
40
|
+
readonly references?: readonly string[];
|
|
41
|
+
readonly since?: string;
|
|
42
|
+
readonly examples?: readonly RuleExample[];
|
|
43
|
+
}
|
|
44
|
+
/** Complete native record exposed to catalog generation without changing ESLint metadata. */
|
|
45
|
+
interface NativeRuleSpec extends Required<Omit<RuleDocumentation, "since">> {
|
|
46
|
+
readonly engine: "eslint";
|
|
47
|
+
readonly ruleId: string;
|
|
48
|
+
readonly code: null;
|
|
49
|
+
readonly messageIds: readonly string[];
|
|
50
|
+
readonly optionsSchema: object | null;
|
|
51
|
+
readonly since: string | null;
|
|
52
|
+
readonly key: string;
|
|
53
|
+
readonly publicExamples: readonly RuleExample[];
|
|
54
|
+
}
|
|
55
|
+
/** The explicit allowlist projection consumed by generated public documentation. */
|
|
56
|
+
interface PublicRuleSpec {
|
|
57
|
+
readonly engine: "eslint";
|
|
58
|
+
readonly ruleId: string;
|
|
59
|
+
readonly code: null;
|
|
60
|
+
readonly summary: string;
|
|
61
|
+
readonly rationale: string;
|
|
62
|
+
readonly remediation: string;
|
|
63
|
+
readonly category: RuleCategory;
|
|
64
|
+
readonly defaultLevel: DefaultLevel;
|
|
65
|
+
readonly languages: readonly "typescript"[];
|
|
66
|
+
readonly autofix: AutofixPolicy;
|
|
67
|
+
readonly aliases: readonly string[];
|
|
68
|
+
readonly limitations: readonly string[];
|
|
69
|
+
readonly filePatterns: readonly string[];
|
|
70
|
+
readonly references: readonly string[];
|
|
71
|
+
readonly since: string | null;
|
|
72
|
+
readonly messageIds: readonly string[];
|
|
73
|
+
readonly optionsSchema: object | null;
|
|
74
|
+
readonly examples: readonly RuleExample[];
|
|
75
|
+
}
|
|
76
|
+
declare const eslintCreateRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
77
|
+
name: string;
|
|
78
|
+
};
|
|
79
|
+
type DocumentedRule<Options extends readonly unknown[], MessageIds extends string> = ReturnType<typeof eslintCreateRule<Options, MessageIds>> & {
|
|
80
|
+
/** Non-enumerable so the public ESLint rule shape remains unchanged. */
|
|
81
|
+
readonly documentation?: NativeRuleSpec;
|
|
82
|
+
};
|
|
83
|
+
/** Serialize only reviewed fields; test-only examples never cross this boundary. */
|
|
84
|
+
declare function publicDocumentation(rules: Readonly<Record<string, {
|
|
85
|
+
readonly documentation?: NativeRuleSpec;
|
|
86
|
+
}>>): readonly PublicRuleSpec[];
|
|
87
|
+
|
|
88
|
+
export { type DocumentedRule as D, publicDocumentation as p };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @fileoverview _docs — typed source-owned rule documentation and executable examples.
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type RuleCategory = "architecture" | "correctness" | "maintainability" | "performance" | "security" | "style" | "testing";
|
|
9
|
+
type AutofixPolicy = "none" | "safe" | "suggestion";
|
|
10
|
+
type DefaultLevel = "error" | "warning";
|
|
11
|
+
type ExampleOutcome = "match" | "no-match";
|
|
12
|
+
interface ExampleFile {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly source: string;
|
|
15
|
+
}
|
|
16
|
+
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
17
|
+
interface RuleExample {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly scenarioId?: string;
|
|
20
|
+
readonly title: string;
|
|
21
|
+
readonly outcome: ExampleOutcome;
|
|
22
|
+
readonly files: readonly ExampleFile[];
|
|
23
|
+
readonly focusPath: string;
|
|
24
|
+
readonly expectedCount: number;
|
|
25
|
+
readonly public?: boolean;
|
|
26
|
+
readonly fixedFiles?: readonly ExampleFile[];
|
|
27
|
+
}
|
|
28
|
+
/** Human-authored fields that cannot be derived from the ESLint rule module. */
|
|
29
|
+
interface RuleDocumentation {
|
|
30
|
+
readonly summary: string;
|
|
31
|
+
readonly rationale: string;
|
|
32
|
+
readonly remediation: string;
|
|
33
|
+
readonly category: RuleCategory;
|
|
34
|
+
readonly defaultLevel?: DefaultLevel;
|
|
35
|
+
readonly languages?: readonly "typescript"[];
|
|
36
|
+
readonly autofix?: AutofixPolicy;
|
|
37
|
+
readonly aliases?: readonly string[];
|
|
38
|
+
readonly limitations?: readonly string[];
|
|
39
|
+
readonly filePatterns?: readonly string[];
|
|
40
|
+
readonly references?: readonly string[];
|
|
41
|
+
readonly since?: string;
|
|
42
|
+
readonly examples?: readonly RuleExample[];
|
|
43
|
+
}
|
|
44
|
+
/** Complete native record exposed to catalog generation without changing ESLint metadata. */
|
|
45
|
+
interface NativeRuleSpec extends Required<Omit<RuleDocumentation, "since">> {
|
|
46
|
+
readonly engine: "eslint";
|
|
47
|
+
readonly ruleId: string;
|
|
48
|
+
readonly code: null;
|
|
49
|
+
readonly messageIds: readonly string[];
|
|
50
|
+
readonly optionsSchema: object | null;
|
|
51
|
+
readonly since: string | null;
|
|
52
|
+
readonly key: string;
|
|
53
|
+
readonly publicExamples: readonly RuleExample[];
|
|
54
|
+
}
|
|
55
|
+
/** The explicit allowlist projection consumed by generated public documentation. */
|
|
56
|
+
interface PublicRuleSpec {
|
|
57
|
+
readonly engine: "eslint";
|
|
58
|
+
readonly ruleId: string;
|
|
59
|
+
readonly code: null;
|
|
60
|
+
readonly summary: string;
|
|
61
|
+
readonly rationale: string;
|
|
62
|
+
readonly remediation: string;
|
|
63
|
+
readonly category: RuleCategory;
|
|
64
|
+
readonly defaultLevel: DefaultLevel;
|
|
65
|
+
readonly languages: readonly "typescript"[];
|
|
66
|
+
readonly autofix: AutofixPolicy;
|
|
67
|
+
readonly aliases: readonly string[];
|
|
68
|
+
readonly limitations: readonly string[];
|
|
69
|
+
readonly filePatterns: readonly string[];
|
|
70
|
+
readonly references: readonly string[];
|
|
71
|
+
readonly since: string | null;
|
|
72
|
+
readonly messageIds: readonly string[];
|
|
73
|
+
readonly optionsSchema: object | null;
|
|
74
|
+
readonly examples: readonly RuleExample[];
|
|
75
|
+
}
|
|
76
|
+
declare const eslintCreateRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
77
|
+
name: string;
|
|
78
|
+
};
|
|
79
|
+
type DocumentedRule<Options extends readonly unknown[], MessageIds extends string> = ReturnType<typeof eslintCreateRule<Options, MessageIds>> & {
|
|
80
|
+
/** Non-enumerable so the public ESLint rule shape remains unchanged. */
|
|
81
|
+
readonly documentation?: NativeRuleSpec;
|
|
82
|
+
};
|
|
83
|
+
/** Serialize only reviewed fields; test-only examples never cross this boundary. */
|
|
84
|
+
declare function publicDocumentation(rules: Readonly<Record<string, {
|
|
85
|
+
readonly documentation?: NativeRuleSpec;
|
|
86
|
+
}>>): readonly PublicRuleSpec[];
|
|
87
|
+
|
|
88
|
+
export { type DocumentedRule as D, publicDocumentation as p };
|
package/dist/index.cjs
CHANGED
|
@@ -9486,7 +9486,7 @@ var NO_SECRET_IN_LOG_DOCUMENTATION = {
|
|
|
9486
9486
|
rationale: "Logs are widely retained and distributed, so credentials and raw bodies can become durable data leaks.",
|
|
9487
9487
|
remediation: "Omit the value, log allowlisted non-sensitive context, or use an approved redactor; truncation alone is not a safety guarantee.",
|
|
9488
9488
|
category: "security",
|
|
9489
|
-
limitations: ["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers. Name-based exemptions are policy heuristics, not proof that a value is safely redacted."],
|
|
9489
|
+
limitations: ["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers. Literal containers and template/string concatenation expressions are inspected; unknown spreads, computed properties, aliases, and opaque calls are not followed. Name-based exemptions are policy heuristics, not proof that a value is safely redacted."],
|
|
9490
9490
|
examples: [
|
|
9491
9491
|
{ id: "redacted-secret", title: "Log non-sensitive context instead of the secret", outcome: "no-match", files: [{ path: "src/auth.ts", source: "logger.info('auth', { requestId });" }], focusPath: "src/auth.ts", expectedCount: 0, public: true },
|
|
9492
9492
|
{ id: "logged-secret", title: "Do not send a secret to logs", outcome: "match", files: [{ path: "src/auth.ts", source: "logger.error('auth failed', { token });" }], focusPath: "src/auth.ts", expectedCount: 1, public: true }
|
|
@@ -9537,13 +9537,25 @@ var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
9537
9537
|
"len",
|
|
9538
9538
|
"length"
|
|
9539
9539
|
]);
|
|
9540
|
-
var
|
|
9541
|
-
|
|
9540
|
+
var SECRET_REDACTION_TOKENS = /* @__PURE__ */ new Set([
|
|
9541
|
+
"prefix",
|
|
9542
|
+
"suffix",
|
|
9543
|
+
"redact",
|
|
9544
|
+
"redacted",
|
|
9545
|
+
"mask",
|
|
9546
|
+
"masked",
|
|
9547
|
+
"hash",
|
|
9548
|
+
"hashed",
|
|
9549
|
+
"hint",
|
|
9550
|
+
"len",
|
|
9551
|
+
"length",
|
|
9552
|
+
"tag"
|
|
9553
|
+
]);
|
|
9542
9554
|
function isSecretKeyword(name) {
|
|
9543
9555
|
return !hasRedactionMarker(name) && isSecretName(name, LOG_INNOCUOUS_WORDS);
|
|
9544
9556
|
}
|
|
9545
9557
|
function hasRedactionMarker(name) {
|
|
9546
|
-
return
|
|
9558
|
+
return tokenize(name).some((token) => SECRET_REDACTION_TOKENS.has(token));
|
|
9547
9559
|
}
|
|
9548
9560
|
function valueName(node) {
|
|
9549
9561
|
if (node.type === "Identifier") return node.name;
|
|
@@ -9563,8 +9575,27 @@ var RAW_BLOB_WORDS = /* @__PURE__ */ new Set([
|
|
|
9563
9575
|
"params"
|
|
9564
9576
|
]);
|
|
9565
9577
|
var RAW_BLOB_IDENTIFIERS = /* @__PURE__ */ new Set(["formdata"]);
|
|
9566
|
-
var
|
|
9567
|
-
|
|
9578
|
+
var BLOB_TRANSFORM_TOKENS = /* @__PURE__ */ new Set([
|
|
9579
|
+
"redact",
|
|
9580
|
+
"redacted",
|
|
9581
|
+
"sanitize",
|
|
9582
|
+
"sanitized",
|
|
9583
|
+
"scrub",
|
|
9584
|
+
"scrubbed",
|
|
9585
|
+
"mask",
|
|
9586
|
+
"masked",
|
|
9587
|
+
"truncate",
|
|
9588
|
+
"truncated",
|
|
9589
|
+
"anonymize",
|
|
9590
|
+
"anonymized",
|
|
9591
|
+
"filter",
|
|
9592
|
+
"filtered",
|
|
9593
|
+
"preview",
|
|
9594
|
+
"summarize",
|
|
9595
|
+
"summarized",
|
|
9596
|
+
"summary"
|
|
9597
|
+
]);
|
|
9598
|
+
var BLOB_SAFE_TOKENS = /* @__PURE__ */ new Set([
|
|
9568
9599
|
"safe",
|
|
9569
9600
|
"clean",
|
|
9570
9601
|
"shape",
|
|
@@ -9586,11 +9617,8 @@ function rawBlobValueName(value) {
|
|
|
9586
9617
|
return null;
|
|
9587
9618
|
}
|
|
9588
9619
|
function isRawBlobName(name) {
|
|
9589
|
-
if (REDACTION_RE.test(name) || BLOB_REDACTION_RE.test(name)) {
|
|
9590
|
-
return false;
|
|
9591
|
-
}
|
|
9592
9620
|
const tokens = tokenize(name);
|
|
9593
|
-
if (tokens.some((tok) =>
|
|
9621
|
+
if (tokens.some((tok) => SECRET_REDACTION_TOKENS.has(tok) || BLOB_TRANSFORM_TOKENS.has(tok) || BLOB_SAFE_TOKENS.has(tok))) {
|
|
9594
9622
|
return false;
|
|
9595
9623
|
}
|
|
9596
9624
|
const first = leadingWord(name);
|
|
@@ -9660,12 +9688,62 @@ var no_secret_in_log_default = createRule({
|
|
|
9660
9688
|
}
|
|
9661
9689
|
function reportRawBlob(node, value) {
|
|
9662
9690
|
if (!blobArmApplies) {
|
|
9663
|
-
return;
|
|
9691
|
+
return false;
|
|
9664
9692
|
}
|
|
9665
9693
|
const name = rawBlobValueName(value);
|
|
9666
9694
|
if (name !== null) {
|
|
9667
9695
|
context.report({ node, messageId: "noRawBodyInLog", data: { name } });
|
|
9696
|
+
return true;
|
|
9668
9697
|
}
|
|
9698
|
+
return false;
|
|
9699
|
+
}
|
|
9700
|
+
function literalProperties(value) {
|
|
9701
|
+
const effective = /* @__PURE__ */ new Map();
|
|
9702
|
+
for (const entry of value.properties) {
|
|
9703
|
+
if (entry.type === "SpreadElement") {
|
|
9704
|
+
if (entry.argument.type !== "ObjectExpression") {
|
|
9705
|
+
effective.clear();
|
|
9706
|
+
continue;
|
|
9707
|
+
}
|
|
9708
|
+
for (const property of literalProperties(entry.argument)) {
|
|
9709
|
+
const key2 = propertyKeyName2(property);
|
|
9710
|
+
if (key2 !== null) effective.set(key2, property);
|
|
9711
|
+
}
|
|
9712
|
+
continue;
|
|
9713
|
+
}
|
|
9714
|
+
const key = propertyKeyName2(entry);
|
|
9715
|
+
if (key === null) {
|
|
9716
|
+
effective.clear();
|
|
9717
|
+
continue;
|
|
9718
|
+
}
|
|
9719
|
+
effective.set(key, entry);
|
|
9720
|
+
}
|
|
9721
|
+
return [...effective.values()];
|
|
9722
|
+
}
|
|
9723
|
+
function inspectLoggedValue(value) {
|
|
9724
|
+
if (value.type === "ObjectExpression") {
|
|
9725
|
+
for (const property of literalProperties(value)) {
|
|
9726
|
+
if (reportSecretProperty(property) || reportRawBlob(property, property.value)) continue;
|
|
9727
|
+
inspectLoggedValue(property.value);
|
|
9728
|
+
}
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9731
|
+
if (value.type === "ArrayExpression") {
|
|
9732
|
+
for (const element of value.elements) {
|
|
9733
|
+
if (element !== null && element.type !== "SpreadElement") inspectLoggedValue(element);
|
|
9734
|
+
}
|
|
9735
|
+
return;
|
|
9736
|
+
}
|
|
9737
|
+
if (value.type === "TemplateLiteral") {
|
|
9738
|
+
for (const expression of value.expressions) inspectLoggedValue(expression);
|
|
9739
|
+
return;
|
|
9740
|
+
}
|
|
9741
|
+
if (value.type === "BinaryExpression" && value.operator === "+") {
|
|
9742
|
+
inspectLoggedValue(value.left);
|
|
9743
|
+
inspectLoggedValue(value.right);
|
|
9744
|
+
return;
|
|
9745
|
+
}
|
|
9746
|
+
if (!reportSecretArgument(value)) reportRawBlob(value, value);
|
|
9669
9747
|
}
|
|
9670
9748
|
return {
|
|
9671
9749
|
CallExpression(node) {
|
|
@@ -9673,20 +9751,7 @@ var no_secret_in_log_default = createRule({
|
|
|
9673
9751
|
return;
|
|
9674
9752
|
}
|
|
9675
9753
|
for (const arg of node.arguments) {
|
|
9676
|
-
if (arg.type
|
|
9677
|
-
for (const prop of arg.properties) {
|
|
9678
|
-
if (prop.type !== "Property") {
|
|
9679
|
-
continue;
|
|
9680
|
-
}
|
|
9681
|
-
if (!reportSecretProperty(prop)) {
|
|
9682
|
-
reportRawBlob(prop, prop.value);
|
|
9683
|
-
}
|
|
9684
|
-
}
|
|
9685
|
-
continue;
|
|
9686
|
-
}
|
|
9687
|
-
if (!reportSecretArgument(arg)) {
|
|
9688
|
-
reportRawBlob(arg, arg);
|
|
9689
|
-
}
|
|
9754
|
+
if (arg.type !== "SpreadElement") inspectLoggedValue(arg);
|
|
9690
9755
|
}
|
|
9691
9756
|
}
|
|
9692
9757
|
};
|
|
@@ -23162,7 +23227,7 @@ var RULES = {
|
|
|
23162
23227
|
};
|
|
23163
23228
|
var meta = {
|
|
23164
23229
|
name: "@sarj/eslint-plugin",
|
|
23165
|
-
version: "15.
|
|
23230
|
+
version: "15.27.0"
|
|
23166
23231
|
};
|
|
23167
23232
|
var APPLICATION_ONLY_RULES = [];
|
|
23168
23233
|
var LIBRARY_IMPORT_POLICY = ["error", {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.cjs';
|
|
2
|
+
export { p as publicDocumentation } from './_docs-XCw_6XV9.cjs';
|
|
3
|
+
import '@typescript-eslint/utils';
|
|
2
4
|
|
|
3
5
|
/** @fileoverview _retired — burned rule names and their migration action. */
|
|
4
6
|
/** Why a name was withdrawn, and in which release. */
|
|
@@ -10,91 +12,6 @@ interface RetiredRule {
|
|
|
10
12
|
}
|
|
11
13
|
declare const RETIRED_RULES: Readonly<Record<string, RetiredRule>>;
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
* @fileoverview _docs — typed source-owned rule documentation and executable examples.
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
type RuleCategory = "architecture" | "correctness" | "maintainability" | "performance" | "security" | "style" | "testing";
|
|
19
|
-
type AutofixPolicy = "none" | "safe" | "suggestion";
|
|
20
|
-
type DefaultLevel = "error" | "warning";
|
|
21
|
-
type ExampleOutcome = "match" | "no-match";
|
|
22
|
-
interface ExampleFile {
|
|
23
|
-
readonly path: string;
|
|
24
|
-
readonly source: string;
|
|
25
|
-
}
|
|
26
|
-
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
27
|
-
interface RuleExample {
|
|
28
|
-
readonly id: string;
|
|
29
|
-
readonly scenarioId?: string;
|
|
30
|
-
readonly title: string;
|
|
31
|
-
readonly outcome: ExampleOutcome;
|
|
32
|
-
readonly files: readonly ExampleFile[];
|
|
33
|
-
readonly focusPath: string;
|
|
34
|
-
readonly expectedCount: number;
|
|
35
|
-
readonly public?: boolean;
|
|
36
|
-
readonly fixedFiles?: readonly ExampleFile[];
|
|
37
|
-
}
|
|
38
|
-
/** Human-authored fields that cannot be derived from the ESLint rule module. */
|
|
39
|
-
interface RuleDocumentation {
|
|
40
|
-
readonly summary: string;
|
|
41
|
-
readonly rationale: string;
|
|
42
|
-
readonly remediation: string;
|
|
43
|
-
readonly category: RuleCategory;
|
|
44
|
-
readonly defaultLevel?: DefaultLevel;
|
|
45
|
-
readonly languages?: readonly "typescript"[];
|
|
46
|
-
readonly autofix?: AutofixPolicy;
|
|
47
|
-
readonly aliases?: readonly string[];
|
|
48
|
-
readonly limitations?: readonly string[];
|
|
49
|
-
readonly filePatterns?: readonly string[];
|
|
50
|
-
readonly references?: readonly string[];
|
|
51
|
-
readonly since?: string;
|
|
52
|
-
readonly examples?: readonly RuleExample[];
|
|
53
|
-
}
|
|
54
|
-
/** Complete native record exposed to catalog generation without changing ESLint metadata. */
|
|
55
|
-
interface NativeRuleSpec extends Required<Omit<RuleDocumentation, "since">> {
|
|
56
|
-
readonly engine: "eslint";
|
|
57
|
-
readonly ruleId: string;
|
|
58
|
-
readonly code: null;
|
|
59
|
-
readonly messageIds: readonly string[];
|
|
60
|
-
readonly optionsSchema: object | null;
|
|
61
|
-
readonly since: string | null;
|
|
62
|
-
readonly key: string;
|
|
63
|
-
readonly publicExamples: readonly RuleExample[];
|
|
64
|
-
}
|
|
65
|
-
/** The explicit allowlist projection consumed by generated public documentation. */
|
|
66
|
-
interface PublicRuleSpec {
|
|
67
|
-
readonly engine: "eslint";
|
|
68
|
-
readonly ruleId: string;
|
|
69
|
-
readonly code: null;
|
|
70
|
-
readonly summary: string;
|
|
71
|
-
readonly rationale: string;
|
|
72
|
-
readonly remediation: string;
|
|
73
|
-
readonly category: RuleCategory;
|
|
74
|
-
readonly defaultLevel: DefaultLevel;
|
|
75
|
-
readonly languages: readonly "typescript"[];
|
|
76
|
-
readonly autofix: AutofixPolicy;
|
|
77
|
-
readonly aliases: readonly string[];
|
|
78
|
-
readonly limitations: readonly string[];
|
|
79
|
-
readonly filePatterns: readonly string[];
|
|
80
|
-
readonly references: readonly string[];
|
|
81
|
-
readonly since: string | null;
|
|
82
|
-
readonly messageIds: readonly string[];
|
|
83
|
-
readonly optionsSchema: object | null;
|
|
84
|
-
readonly examples: readonly RuleExample[];
|
|
85
|
-
}
|
|
86
|
-
declare const eslintCreateRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
87
|
-
name: string;
|
|
88
|
-
};
|
|
89
|
-
type DocumentedRule<Options extends readonly unknown[], MessageIds extends string> = ReturnType<typeof eslintCreateRule<Options, MessageIds>> & {
|
|
90
|
-
/** Non-enumerable so the public ESLint rule shape remains unchanged. */
|
|
91
|
-
readonly documentation?: NativeRuleSpec;
|
|
92
|
-
};
|
|
93
|
-
/** Serialize only reviewed fields; test-only examples never cross this boundary. */
|
|
94
|
-
declare function publicDocumentation(rules: Readonly<Record<string, {
|
|
95
|
-
readonly documentation?: NativeRuleSpec;
|
|
96
|
-
}>>): readonly PublicRuleSpec[];
|
|
97
|
-
|
|
98
15
|
interface RuleOptions$5 {
|
|
99
16
|
readonly basePath?: string;
|
|
100
17
|
}
|
|
@@ -996,7 +913,7 @@ type FlatPreset = {
|
|
|
996
913
|
declare const PLUGIN: {
|
|
997
914
|
readonly meta: {
|
|
998
915
|
readonly name: "@sarj/eslint-plugin";
|
|
999
|
-
readonly version: "15.
|
|
916
|
+
readonly version: "15.27.0";
|
|
1000
917
|
};
|
|
1001
918
|
readonly rules: {
|
|
1002
919
|
readonly "no-conditional-empty-object-spread": DocumentedRule<[], "avoid">;
|
|
@@ -1161,4 +1078,4 @@ declare const PLUGIN: {
|
|
|
1161
1078
|
};
|
|
1162
1079
|
};
|
|
1163
1080
|
|
|
1164
|
-
export { ADVISORY_RULES, APPLICATION_ONLY_RULES, RECOMMENDED_RULES, RENAMED_RULES, RETIRED_RULES, RULES, type RetiredRule, STRICT_RULES, ADVISORY_RULES as advisoryRules, APPLICATION_ONLY_RULES as applicationOnlyRules, PLUGIN as default,
|
|
1081
|
+
export { ADVISORY_RULES, APPLICATION_ONLY_RULES, RECOMMENDED_RULES, RENAMED_RULES, RETIRED_RULES, RULES, type RetiredRule, STRICT_RULES, ADVISORY_RULES as advisoryRules, APPLICATION_ONLY_RULES as applicationOnlyRules, PLUGIN as default, RECOMMENDED_RULES as recommendedRules, RENAMED_RULES as renamedRules, RETIRED_RULES as retiredRules, RULES as rules, STRICT_RULES as strictRules };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.js';
|
|
2
|
+
export { p as publicDocumentation } from './_docs-XCw_6XV9.js';
|
|
3
|
+
import '@typescript-eslint/utils';
|
|
2
4
|
|
|
3
5
|
/** @fileoverview _retired — burned rule names and their migration action. */
|
|
4
6
|
/** Why a name was withdrawn, and in which release. */
|
|
@@ -10,91 +12,6 @@ interface RetiredRule {
|
|
|
10
12
|
}
|
|
11
13
|
declare const RETIRED_RULES: Readonly<Record<string, RetiredRule>>;
|
|
12
14
|
|
|
13
|
-
/**
|
|
14
|
-
* @fileoverview _docs — typed source-owned rule documentation and executable examples.
|
|
15
|
-
*
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
type RuleCategory = "architecture" | "correctness" | "maintainability" | "performance" | "security" | "style" | "testing";
|
|
19
|
-
type AutofixPolicy = "none" | "safe" | "suggestion";
|
|
20
|
-
type DefaultLevel = "error" | "warning";
|
|
21
|
-
type ExampleOutcome = "match" | "no-match";
|
|
22
|
-
interface ExampleFile {
|
|
23
|
-
readonly path: string;
|
|
24
|
-
readonly source: string;
|
|
25
|
-
}
|
|
26
|
-
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
27
|
-
interface RuleExample {
|
|
28
|
-
readonly id: string;
|
|
29
|
-
readonly scenarioId?: string;
|
|
30
|
-
readonly title: string;
|
|
31
|
-
readonly outcome: ExampleOutcome;
|
|
32
|
-
readonly files: readonly ExampleFile[];
|
|
33
|
-
readonly focusPath: string;
|
|
34
|
-
readonly expectedCount: number;
|
|
35
|
-
readonly public?: boolean;
|
|
36
|
-
readonly fixedFiles?: readonly ExampleFile[];
|
|
37
|
-
}
|
|
38
|
-
/** Human-authored fields that cannot be derived from the ESLint rule module. */
|
|
39
|
-
interface RuleDocumentation {
|
|
40
|
-
readonly summary: string;
|
|
41
|
-
readonly rationale: string;
|
|
42
|
-
readonly remediation: string;
|
|
43
|
-
readonly category: RuleCategory;
|
|
44
|
-
readonly defaultLevel?: DefaultLevel;
|
|
45
|
-
readonly languages?: readonly "typescript"[];
|
|
46
|
-
readonly autofix?: AutofixPolicy;
|
|
47
|
-
readonly aliases?: readonly string[];
|
|
48
|
-
readonly limitations?: readonly string[];
|
|
49
|
-
readonly filePatterns?: readonly string[];
|
|
50
|
-
readonly references?: readonly string[];
|
|
51
|
-
readonly since?: string;
|
|
52
|
-
readonly examples?: readonly RuleExample[];
|
|
53
|
-
}
|
|
54
|
-
/** Complete native record exposed to catalog generation without changing ESLint metadata. */
|
|
55
|
-
interface NativeRuleSpec extends Required<Omit<RuleDocumentation, "since">> {
|
|
56
|
-
readonly engine: "eslint";
|
|
57
|
-
readonly ruleId: string;
|
|
58
|
-
readonly code: null;
|
|
59
|
-
readonly messageIds: readonly string[];
|
|
60
|
-
readonly optionsSchema: object | null;
|
|
61
|
-
readonly since: string | null;
|
|
62
|
-
readonly key: string;
|
|
63
|
-
readonly publicExamples: readonly RuleExample[];
|
|
64
|
-
}
|
|
65
|
-
/** The explicit allowlist projection consumed by generated public documentation. */
|
|
66
|
-
interface PublicRuleSpec {
|
|
67
|
-
readonly engine: "eslint";
|
|
68
|
-
readonly ruleId: string;
|
|
69
|
-
readonly code: null;
|
|
70
|
-
readonly summary: string;
|
|
71
|
-
readonly rationale: string;
|
|
72
|
-
readonly remediation: string;
|
|
73
|
-
readonly category: RuleCategory;
|
|
74
|
-
readonly defaultLevel: DefaultLevel;
|
|
75
|
-
readonly languages: readonly "typescript"[];
|
|
76
|
-
readonly autofix: AutofixPolicy;
|
|
77
|
-
readonly aliases: readonly string[];
|
|
78
|
-
readonly limitations: readonly string[];
|
|
79
|
-
readonly filePatterns: readonly string[];
|
|
80
|
-
readonly references: readonly string[];
|
|
81
|
-
readonly since: string | null;
|
|
82
|
-
readonly messageIds: readonly string[];
|
|
83
|
-
readonly optionsSchema: object | null;
|
|
84
|
-
readonly examples: readonly RuleExample[];
|
|
85
|
-
}
|
|
86
|
-
declare const eslintCreateRule: <Options extends readonly unknown[], MessageIds extends string>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, unknown>>) => ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
|
|
87
|
-
name: string;
|
|
88
|
-
};
|
|
89
|
-
type DocumentedRule<Options extends readonly unknown[], MessageIds extends string> = ReturnType<typeof eslintCreateRule<Options, MessageIds>> & {
|
|
90
|
-
/** Non-enumerable so the public ESLint rule shape remains unchanged. */
|
|
91
|
-
readonly documentation?: NativeRuleSpec;
|
|
92
|
-
};
|
|
93
|
-
/** Serialize only reviewed fields; test-only examples never cross this boundary. */
|
|
94
|
-
declare function publicDocumentation(rules: Readonly<Record<string, {
|
|
95
|
-
readonly documentation?: NativeRuleSpec;
|
|
96
|
-
}>>): readonly PublicRuleSpec[];
|
|
97
|
-
|
|
98
15
|
interface RuleOptions$5 {
|
|
99
16
|
readonly basePath?: string;
|
|
100
17
|
}
|
|
@@ -996,7 +913,7 @@ type FlatPreset = {
|
|
|
996
913
|
declare const PLUGIN: {
|
|
997
914
|
readonly meta: {
|
|
998
915
|
readonly name: "@sarj/eslint-plugin";
|
|
999
|
-
readonly version: "15.
|
|
916
|
+
readonly version: "15.27.0";
|
|
1000
917
|
};
|
|
1001
918
|
readonly rules: {
|
|
1002
919
|
readonly "no-conditional-empty-object-spread": DocumentedRule<[], "avoid">;
|
|
@@ -1161,4 +1078,4 @@ declare const PLUGIN: {
|
|
|
1161
1078
|
};
|
|
1162
1079
|
};
|
|
1163
1080
|
|
|
1164
|
-
export { ADVISORY_RULES, APPLICATION_ONLY_RULES, RECOMMENDED_RULES, RENAMED_RULES, RETIRED_RULES, RULES, type RetiredRule, STRICT_RULES, ADVISORY_RULES as advisoryRules, APPLICATION_ONLY_RULES as applicationOnlyRules, PLUGIN as default,
|
|
1081
|
+
export { ADVISORY_RULES, APPLICATION_ONLY_RULES, RECOMMENDED_RULES, RENAMED_RULES, RETIRED_RULES, RULES, type RetiredRule, STRICT_RULES, ADVISORY_RULES as advisoryRules, APPLICATION_ONLY_RULES as applicationOnlyRules, PLUGIN as default, RECOMMENDED_RULES as recommendedRules, RENAMED_RULES as renamedRules, RETIRED_RULES as retiredRules, RULES as rules, STRICT_RULES as strictRules };
|
package/dist/index.js
CHANGED
|
@@ -9468,7 +9468,7 @@ var NO_SECRET_IN_LOG_DOCUMENTATION = {
|
|
|
9468
9468
|
rationale: "Logs are widely retained and distributed, so credentials and raw bodies can become durable data leaks.",
|
|
9469
9469
|
remediation: "Omit the value, log allowlisted non-sensitive context, or use an approved redactor; truncation alone is not a safety guarantee.",
|
|
9470
9470
|
category: "security",
|
|
9471
|
-
limitations: ["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers. Name-based exemptions are policy heuristics, not proof that a value is safely redacted."],
|
|
9471
|
+
limitations: ["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers. Literal containers and template/string concatenation expressions are inspected; unknown spreads, computed properties, aliases, and opaque calls are not followed. Name-based exemptions are policy heuristics, not proof that a value is safely redacted."],
|
|
9472
9472
|
examples: [
|
|
9473
9473
|
{ id: "redacted-secret", title: "Log non-sensitive context instead of the secret", outcome: "no-match", files: [{ path: "src/auth.ts", source: "logger.info('auth', { requestId });" }], focusPath: "src/auth.ts", expectedCount: 0, public: true },
|
|
9474
9474
|
{ id: "logged-secret", title: "Do not send a secret to logs", outcome: "match", files: [{ path: "src/auth.ts", source: "logger.error('auth failed', { token });" }], focusPath: "src/auth.ts", expectedCount: 1, public: true }
|
|
@@ -9519,13 +9519,25 @@ var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
|
|
|
9519
9519
|
"len",
|
|
9520
9520
|
"length"
|
|
9521
9521
|
]);
|
|
9522
|
-
var
|
|
9523
|
-
|
|
9522
|
+
var SECRET_REDACTION_TOKENS = /* @__PURE__ */ new Set([
|
|
9523
|
+
"prefix",
|
|
9524
|
+
"suffix",
|
|
9525
|
+
"redact",
|
|
9526
|
+
"redacted",
|
|
9527
|
+
"mask",
|
|
9528
|
+
"masked",
|
|
9529
|
+
"hash",
|
|
9530
|
+
"hashed",
|
|
9531
|
+
"hint",
|
|
9532
|
+
"len",
|
|
9533
|
+
"length",
|
|
9534
|
+
"tag"
|
|
9535
|
+
]);
|
|
9524
9536
|
function isSecretKeyword(name) {
|
|
9525
9537
|
return !hasRedactionMarker(name) && isSecretName(name, LOG_INNOCUOUS_WORDS);
|
|
9526
9538
|
}
|
|
9527
9539
|
function hasRedactionMarker(name) {
|
|
9528
|
-
return
|
|
9540
|
+
return tokenize(name).some((token) => SECRET_REDACTION_TOKENS.has(token));
|
|
9529
9541
|
}
|
|
9530
9542
|
function valueName(node) {
|
|
9531
9543
|
if (node.type === "Identifier") return node.name;
|
|
@@ -9545,8 +9557,27 @@ var RAW_BLOB_WORDS = /* @__PURE__ */ new Set([
|
|
|
9545
9557
|
"params"
|
|
9546
9558
|
]);
|
|
9547
9559
|
var RAW_BLOB_IDENTIFIERS = /* @__PURE__ */ new Set(["formdata"]);
|
|
9548
|
-
var
|
|
9549
|
-
|
|
9560
|
+
var BLOB_TRANSFORM_TOKENS = /* @__PURE__ */ new Set([
|
|
9561
|
+
"redact",
|
|
9562
|
+
"redacted",
|
|
9563
|
+
"sanitize",
|
|
9564
|
+
"sanitized",
|
|
9565
|
+
"scrub",
|
|
9566
|
+
"scrubbed",
|
|
9567
|
+
"mask",
|
|
9568
|
+
"masked",
|
|
9569
|
+
"truncate",
|
|
9570
|
+
"truncated",
|
|
9571
|
+
"anonymize",
|
|
9572
|
+
"anonymized",
|
|
9573
|
+
"filter",
|
|
9574
|
+
"filtered",
|
|
9575
|
+
"preview",
|
|
9576
|
+
"summarize",
|
|
9577
|
+
"summarized",
|
|
9578
|
+
"summary"
|
|
9579
|
+
]);
|
|
9580
|
+
var BLOB_SAFE_TOKENS = /* @__PURE__ */ new Set([
|
|
9550
9581
|
"safe",
|
|
9551
9582
|
"clean",
|
|
9552
9583
|
"shape",
|
|
@@ -9568,11 +9599,8 @@ function rawBlobValueName(value) {
|
|
|
9568
9599
|
return null;
|
|
9569
9600
|
}
|
|
9570
9601
|
function isRawBlobName(name) {
|
|
9571
|
-
if (REDACTION_RE.test(name) || BLOB_REDACTION_RE.test(name)) {
|
|
9572
|
-
return false;
|
|
9573
|
-
}
|
|
9574
9602
|
const tokens = tokenize(name);
|
|
9575
|
-
if (tokens.some((tok) =>
|
|
9603
|
+
if (tokens.some((tok) => SECRET_REDACTION_TOKENS.has(tok) || BLOB_TRANSFORM_TOKENS.has(tok) || BLOB_SAFE_TOKENS.has(tok))) {
|
|
9576
9604
|
return false;
|
|
9577
9605
|
}
|
|
9578
9606
|
const first = leadingWord(name);
|
|
@@ -9642,12 +9670,62 @@ var no_secret_in_log_default = createRule({
|
|
|
9642
9670
|
}
|
|
9643
9671
|
function reportRawBlob(node, value) {
|
|
9644
9672
|
if (!blobArmApplies) {
|
|
9645
|
-
return;
|
|
9673
|
+
return false;
|
|
9646
9674
|
}
|
|
9647
9675
|
const name = rawBlobValueName(value);
|
|
9648
9676
|
if (name !== null) {
|
|
9649
9677
|
context.report({ node, messageId: "noRawBodyInLog", data: { name } });
|
|
9678
|
+
return true;
|
|
9650
9679
|
}
|
|
9680
|
+
return false;
|
|
9681
|
+
}
|
|
9682
|
+
function literalProperties(value) {
|
|
9683
|
+
const effective = /* @__PURE__ */ new Map();
|
|
9684
|
+
for (const entry of value.properties) {
|
|
9685
|
+
if (entry.type === "SpreadElement") {
|
|
9686
|
+
if (entry.argument.type !== "ObjectExpression") {
|
|
9687
|
+
effective.clear();
|
|
9688
|
+
continue;
|
|
9689
|
+
}
|
|
9690
|
+
for (const property of literalProperties(entry.argument)) {
|
|
9691
|
+
const key2 = propertyKeyName2(property);
|
|
9692
|
+
if (key2 !== null) effective.set(key2, property);
|
|
9693
|
+
}
|
|
9694
|
+
continue;
|
|
9695
|
+
}
|
|
9696
|
+
const key = propertyKeyName2(entry);
|
|
9697
|
+
if (key === null) {
|
|
9698
|
+
effective.clear();
|
|
9699
|
+
continue;
|
|
9700
|
+
}
|
|
9701
|
+
effective.set(key, entry);
|
|
9702
|
+
}
|
|
9703
|
+
return [...effective.values()];
|
|
9704
|
+
}
|
|
9705
|
+
function inspectLoggedValue(value) {
|
|
9706
|
+
if (value.type === "ObjectExpression") {
|
|
9707
|
+
for (const property of literalProperties(value)) {
|
|
9708
|
+
if (reportSecretProperty(property) || reportRawBlob(property, property.value)) continue;
|
|
9709
|
+
inspectLoggedValue(property.value);
|
|
9710
|
+
}
|
|
9711
|
+
return;
|
|
9712
|
+
}
|
|
9713
|
+
if (value.type === "ArrayExpression") {
|
|
9714
|
+
for (const element of value.elements) {
|
|
9715
|
+
if (element !== null && element.type !== "SpreadElement") inspectLoggedValue(element);
|
|
9716
|
+
}
|
|
9717
|
+
return;
|
|
9718
|
+
}
|
|
9719
|
+
if (value.type === "TemplateLiteral") {
|
|
9720
|
+
for (const expression of value.expressions) inspectLoggedValue(expression);
|
|
9721
|
+
return;
|
|
9722
|
+
}
|
|
9723
|
+
if (value.type === "BinaryExpression" && value.operator === "+") {
|
|
9724
|
+
inspectLoggedValue(value.left);
|
|
9725
|
+
inspectLoggedValue(value.right);
|
|
9726
|
+
return;
|
|
9727
|
+
}
|
|
9728
|
+
if (!reportSecretArgument(value)) reportRawBlob(value, value);
|
|
9651
9729
|
}
|
|
9652
9730
|
return {
|
|
9653
9731
|
CallExpression(node) {
|
|
@@ -9655,20 +9733,7 @@ var no_secret_in_log_default = createRule({
|
|
|
9655
9733
|
return;
|
|
9656
9734
|
}
|
|
9657
9735
|
for (const arg of node.arguments) {
|
|
9658
|
-
if (arg.type
|
|
9659
|
-
for (const prop of arg.properties) {
|
|
9660
|
-
if (prop.type !== "Property") {
|
|
9661
|
-
continue;
|
|
9662
|
-
}
|
|
9663
|
-
if (!reportSecretProperty(prop)) {
|
|
9664
|
-
reportRawBlob(prop, prop.value);
|
|
9665
|
-
}
|
|
9666
|
-
}
|
|
9667
|
-
continue;
|
|
9668
|
-
}
|
|
9669
|
-
if (!reportSecretArgument(arg)) {
|
|
9670
|
-
reportRawBlob(arg, arg);
|
|
9671
|
-
}
|
|
9736
|
+
if (arg.type !== "SpreadElement") inspectLoggedValue(arg);
|
|
9672
9737
|
}
|
|
9673
9738
|
}
|
|
9674
9739
|
};
|
|
@@ -23183,7 +23248,7 @@ var RULES = {
|
|
|
23183
23248
|
};
|
|
23184
23249
|
var meta = {
|
|
23185
23250
|
name: "@sarj/eslint-plugin",
|
|
23186
|
-
version: "15.
|
|
23251
|
+
version: "15.27.0"
|
|
23187
23252
|
};
|
|
23188
23253
|
var APPLICATION_ONLY_RULES = [];
|
|
23189
23254
|
var LIBRARY_IMPORT_POLICY = ["error", {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/rule-examples.ts
|
|
31
|
+
var rule_examples_exports = {};
|
|
32
|
+
__export(rule_examples_exports, {
|
|
33
|
+
verifyRuleExamples: () => verifyRuleExamples
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(rule_examples_exports);
|
|
36
|
+
var import_promises = require("fs/promises");
|
|
37
|
+
var import_node_os = require("os");
|
|
38
|
+
var import_node_path = require("path");
|
|
39
|
+
var parser = __toESM(require("@typescript-eslint/parser"), 1);
|
|
40
|
+
var import_eslint = require("eslint");
|
|
41
|
+
async function verifyRuleExamples(rule) {
|
|
42
|
+
const spec = rule.documentation;
|
|
43
|
+
if (spec === void 0 || !spec.publicExamples.some((item) => item.outcome === "match") || !spec.publicExamples.some((item) => item.outcome === "no-match")) {
|
|
44
|
+
throw new Error("rule requires documented public matching and non-matching examples");
|
|
45
|
+
}
|
|
46
|
+
for (const example of spec.examples) {
|
|
47
|
+
const root = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "sarj-rule-example-"));
|
|
48
|
+
try {
|
|
49
|
+
for (const file of example.files) {
|
|
50
|
+
const path = fixturePath(root, file.path);
|
|
51
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
52
|
+
await (0, import_promises.writeFile)(path, file.source);
|
|
53
|
+
}
|
|
54
|
+
const tsconfig = (0, import_node_path.join)(root, "tsconfig.json");
|
|
55
|
+
if (!example.files.some((file) => file.path === "tsconfig.json")) {
|
|
56
|
+
await (0, import_promises.writeFile)(tsconfig, JSON.stringify({ compilerOptions: {
|
|
57
|
+
target: "ESNext",
|
|
58
|
+
module: "ESNext",
|
|
59
|
+
moduleResolution: "Bundler",
|
|
60
|
+
strict: true,
|
|
61
|
+
allowJs: true,
|
|
62
|
+
jsx: "preserve",
|
|
63
|
+
noEmit: true
|
|
64
|
+
}, include: ["**/*"] }));
|
|
65
|
+
}
|
|
66
|
+
const ruleId = `@sarj/${spec.ruleId}`;
|
|
67
|
+
const config = {
|
|
68
|
+
files: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
|
|
69
|
+
languageOptions: { parser, parserOptions: { project: tsconfig, tsconfigRootDir: root } },
|
|
70
|
+
plugins: { "@sarj": { rules: { [spec.ruleId]: rule } } },
|
|
71
|
+
rules: { [ruleId]: ["error", ...rule.defaultOptions ?? []] }
|
|
72
|
+
};
|
|
73
|
+
await verifyExample(new import_eslint.Linter({ cwd: root }), config, example, root, ruleId);
|
|
74
|
+
} finally {
|
|
75
|
+
await (0, import_promises.rm)(root, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return spec.examples.length;
|
|
79
|
+
}
|
|
80
|
+
function fixturePath(root, path) {
|
|
81
|
+
const resolved = (0, import_node_path.resolve)(root, path);
|
|
82
|
+
if (!resolved.startsWith(root + import_node_path.sep)) throw new Error(`unsafe example path: ${path}`);
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
async function verifyExample(linter, config, example, root, ruleId) {
|
|
86
|
+
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
87
|
+
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
88
|
+
const filename = fixturePath(root, focus.path);
|
|
89
|
+
const messages = linter.verify(focus.source, config, { filename });
|
|
90
|
+
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
91
|
+
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
92
|
+
}
|
|
93
|
+
if (messages.length !== example.expectedCount) {
|
|
94
|
+
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
95
|
+
}
|
|
96
|
+
for (const expected of example.fixedFiles ?? []) {
|
|
97
|
+
const input = example.files.find((file) => file.path === expected.path);
|
|
98
|
+
if (input === void 0) throw new Error(`${example.id}: fixed file has no original`);
|
|
99
|
+
const options = { filename: fixturePath(root, input.path) };
|
|
100
|
+
const fixed = linter.verifyAndFix(input.source, config, options);
|
|
101
|
+
if (fixed.output !== expected.source) throw new Error(`${example.id}: unexpected fixed source`);
|
|
102
|
+
await (0, import_promises.writeFile)(options.filename, fixed.output);
|
|
103
|
+
const repeated = linter.verifyAndFix(fixed.output, config, options);
|
|
104
|
+
if (repeated.fixed || repeated.output !== fixed.output || repeated.messages.length > 0) {
|
|
105
|
+
throw new Error(`${example.id}: fix is not clean and idempotent`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
110
|
+
0 && (module.exports = {
|
|
111
|
+
verifyRuleExamples
|
|
112
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.cjs';
|
|
2
|
+
import '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
/** Execute all authored examples, including private regressions, in isolated projects. */
|
|
5
|
+
declare function verifyRuleExamples<Options extends readonly unknown[], MessageIds extends string>(rule: DocumentedRule<Options, MessageIds>): Promise<number>;
|
|
6
|
+
|
|
7
|
+
export { verifyRuleExamples };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { D as DocumentedRule } from './_docs-XCw_6XV9.js';
|
|
2
|
+
import '@typescript-eslint/utils';
|
|
3
|
+
|
|
4
|
+
/** Execute all authored examples, including private regressions, in isolated projects. */
|
|
5
|
+
declare function verifyRuleExamples<Options extends readonly unknown[], MessageIds extends string>(rule: DocumentedRule<Options, MessageIds>): Promise<number>;
|
|
6
|
+
|
|
7
|
+
export { verifyRuleExamples };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/rule-examples.ts
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { dirname, join, resolve, sep } from "path";
|
|
5
|
+
import * as parser from "@typescript-eslint/parser";
|
|
6
|
+
import { Linter } from "eslint";
|
|
7
|
+
async function verifyRuleExamples(rule) {
|
|
8
|
+
const spec = rule.documentation;
|
|
9
|
+
if (spec === void 0 || !spec.publicExamples.some((item) => item.outcome === "match") || !spec.publicExamples.some((item) => item.outcome === "no-match")) {
|
|
10
|
+
throw new Error("rule requires documented public matching and non-matching examples");
|
|
11
|
+
}
|
|
12
|
+
for (const example of spec.examples) {
|
|
13
|
+
const root = await mkdtemp(join(tmpdir(), "sarj-rule-example-"));
|
|
14
|
+
try {
|
|
15
|
+
for (const file of example.files) {
|
|
16
|
+
const path = fixturePath(root, file.path);
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
await writeFile(path, file.source);
|
|
19
|
+
}
|
|
20
|
+
const tsconfig = join(root, "tsconfig.json");
|
|
21
|
+
if (!example.files.some((file) => file.path === "tsconfig.json")) {
|
|
22
|
+
await writeFile(tsconfig, JSON.stringify({ compilerOptions: {
|
|
23
|
+
target: "ESNext",
|
|
24
|
+
module: "ESNext",
|
|
25
|
+
moduleResolution: "Bundler",
|
|
26
|
+
strict: true,
|
|
27
|
+
allowJs: true,
|
|
28
|
+
jsx: "preserve",
|
|
29
|
+
noEmit: true
|
|
30
|
+
}, include: ["**/*"] }));
|
|
31
|
+
}
|
|
32
|
+
const ruleId = `@sarj/${spec.ruleId}`;
|
|
33
|
+
const config = {
|
|
34
|
+
files: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
|
|
35
|
+
languageOptions: { parser, parserOptions: { project: tsconfig, tsconfigRootDir: root } },
|
|
36
|
+
plugins: { "@sarj": { rules: { [spec.ruleId]: rule } } },
|
|
37
|
+
rules: { [ruleId]: ["error", ...rule.defaultOptions ?? []] }
|
|
38
|
+
};
|
|
39
|
+
await verifyExample(new Linter({ cwd: root }), config, example, root, ruleId);
|
|
40
|
+
} finally {
|
|
41
|
+
await rm(root, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return spec.examples.length;
|
|
45
|
+
}
|
|
46
|
+
function fixturePath(root, path) {
|
|
47
|
+
const resolved = resolve(root, path);
|
|
48
|
+
if (!resolved.startsWith(root + sep)) throw new Error(`unsafe example path: ${path}`);
|
|
49
|
+
return resolved;
|
|
50
|
+
}
|
|
51
|
+
async function verifyExample(linter, config, example, root, ruleId) {
|
|
52
|
+
const focus = example.files.find((file) => file.path === example.focusPath);
|
|
53
|
+
if (focus === void 0) throw new Error(`${example.id}: focus file is missing`);
|
|
54
|
+
const filename = fixturePath(root, focus.path);
|
|
55
|
+
const messages = linter.verify(focus.source, config, { filename });
|
|
56
|
+
if (messages.some((message) => message.fatal || message.ruleId !== ruleId)) {
|
|
57
|
+
throw new Error(`${example.id}: invalid fixture: ${messages.map((message) => message.message).join("; ")}`);
|
|
58
|
+
}
|
|
59
|
+
if (messages.length !== example.expectedCount) {
|
|
60
|
+
throw new Error(`${example.id}: expected ${example.expectedCount} findings, received ${messages.length}`);
|
|
61
|
+
}
|
|
62
|
+
for (const expected of example.fixedFiles ?? []) {
|
|
63
|
+
const input = example.files.find((file) => file.path === expected.path);
|
|
64
|
+
if (input === void 0) throw new Error(`${example.id}: fixed file has no original`);
|
|
65
|
+
const options = { filename: fixturePath(root, input.path) };
|
|
66
|
+
const fixed = linter.verifyAndFix(input.source, config, options);
|
|
67
|
+
if (fixed.output !== expected.source) throw new Error(`${example.id}: unexpected fixed source`);
|
|
68
|
+
await writeFile(options.filename, fixed.output);
|
|
69
|
+
const repeated = linter.verifyAndFix(fixed.output, config, options);
|
|
70
|
+
if (repeated.fixed || repeated.output !== fixed.output || repeated.messages.length > 0) {
|
|
71
|
+
throw new Error(`${example.id}: fix is not clean and idempotent`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
verifyRuleExamples
|
|
77
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sarj/eslint-plugin",
|
|
3
|
-
"version": "15.
|
|
3
|
+
"version": "15.27.0",
|
|
4
4
|
"packageManager": "npm@12.1.0",
|
|
5
5
|
"description": "Custom ESLint rules for hypermodern TypeScript / React / Next.js projects",
|
|
6
6
|
"type": "module",
|
|
@@ -34,12 +34,13 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsup",
|
|
37
|
-
"dogfood": "npm run build &&
|
|
37
|
+
"dogfood": "npm run build && npm run dogfood:built",
|
|
38
38
|
"lint": "eslint . --max-warnings 0",
|
|
39
39
|
"prepack": "npm run build && npm run verify-package",
|
|
40
40
|
"test": "vitest run",
|
|
41
41
|
"typecheck": "tsc --noEmit",
|
|
42
|
-
"verify-package": "node -e \"const fs=require('node:fs'),p=require('./package.json'),walk=x=>typeof x==='string'?[x]:x&&typeof x==='object'?Object.values(x).flatMap(walk):[];for(const file of new Set([p.main,p.module,p.types,...walk(p.exports)].filter(Boolean).map(x=>x.startsWith('./')?x.slice(2):x))){if(!file.startsWith('dist/'))throw new Error('export must live under dist: '+file);const stat=fs.statSync(file);if(!stat.isFile()||stat.size===0)throw new Error('missing or empty export: '+file)}\""
|
|
42
|
+
"verify-package": "node -e \"const fs=require('node:fs'),p=require('./package.json'),walk=x=>typeof x==='string'?[x]:x&&typeof x==='object'?Object.values(x).flatMap(walk):[];for(const file of new Set([p.main,p.module,p.types,...walk(p.exports)].filter(Boolean).map(x=>x.startsWith('./')?x.slice(2):x))){if(!file.startsWith('dist/'))throw new Error('export must live under dist: '+file);const stat=fs.statSync(file);if(!stat.isFile()||stat.size===0)throw new Error('missing or empty export: '+file)}\"",
|
|
43
|
+
"dogfood:built": "eslint --config eslint.dogfood.config.mjs src tests eslint.config.mjs eslint.dogfood.config.mjs module-constant-naming-options.mjs tsup.config.ts vitest.config.ts --max-warnings 0 && node -e \"import('./dist/index.js').then(({default:p}) => console.log('dogfood: '+Object.keys(p.rules).length+' TypeScript rules, 0 diagnostics'))\""
|
|
43
44
|
},
|
|
44
45
|
"keywords": [
|
|
45
46
|
"eslint",
|