@reticlehq/eslint-plugin 2.13.1 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/constants.d.ts +12 -1
- package/dist/constants.js +12 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +5 -2
- package/dist/no-internal-tags.d.ts +6 -8
- package/dist/no-internal-tags.js +79 -29
- package/dist/no-runaway-comment.d.ts +19 -0
- package/dist/no-runaway-comment.js +46 -0
- package/package.json +3 -3
package/dist/constants.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ export declare const DEFAULT_MUTATORS: readonly string[];
|
|
|
15
15
|
/** Plugin meta name used in the flat-config export + RuleCreator url namespace. */
|
|
16
16
|
export declare const PLUGIN_NAME = "reticle";
|
|
17
17
|
/** Docs URL builder root for ESLintUtils.RuleCreator. */
|
|
18
|
-
export declare const DOCS_URL_ROOT = "https://github.com/reticlehq/reticle/blob/main/
|
|
18
|
+
export declare const DOCS_URL_ROOT = "https://github.com/reticlehq/reticle/blob/main/eslint-plugin/README.md";
|
|
19
19
|
/** Rule that keeps design-doc codes and internal version strings out of source comments. */
|
|
20
20
|
export declare const INTERNAL_TAGS_RULE_NAME = "no-internal-tags";
|
|
21
|
+
/** Rule that caps how long one block comment may be. */
|
|
22
|
+
export declare const RUNAWAY_COMMENT_RULE_NAME = "no-runaway-comment";
|
|
23
|
+
/**
|
|
24
|
+
* Longest permitted block comment, in lines.
|
|
25
|
+
*
|
|
26
|
+
* Set above the longest genuine comment in the repository (45) and far below the 423-line accident
|
|
27
|
+
* this rule exists to catch, so it reports a paste and never a paragraph.
|
|
28
|
+
*/
|
|
29
|
+
export declare const MAX_COMMENT_LINES = 60;
|
|
30
|
+
/** The human-readable report text (the assertion in tests matches this verbatim). */
|
|
31
|
+
export declare const RUNAWAY_COMMENT_MESSAGE = "block comment is {{lines}} lines (max {{max}}) \u2014 a comment this long is usually pasted output, not prose";
|
package/dist/constants.js
CHANGED
|
@@ -14,6 +14,17 @@ export const DEFAULT_MUTATORS = [];
|
|
|
14
14
|
/** Plugin meta name used in the flat-config export + RuleCreator url namespace. */
|
|
15
15
|
export const PLUGIN_NAME = 'reticle';
|
|
16
16
|
/** Docs URL builder root for ESLintUtils.RuleCreator. */
|
|
17
|
-
export const DOCS_URL_ROOT = 'https://github.com/reticlehq/reticle/blob/main/
|
|
17
|
+
export const DOCS_URL_ROOT = 'https://github.com/reticlehq/reticle/blob/main/eslint-plugin/README.md';
|
|
18
18
|
/** Rule that keeps design-doc codes and internal version strings out of source comments. */
|
|
19
19
|
export const INTERNAL_TAGS_RULE_NAME = 'no-internal-tags';
|
|
20
|
+
/** Rule that caps how long one block comment may be. */
|
|
21
|
+
export const RUNAWAY_COMMENT_RULE_NAME = 'no-runaway-comment';
|
|
22
|
+
/**
|
|
23
|
+
* Longest permitted block comment, in lines.
|
|
24
|
+
*
|
|
25
|
+
* Set above the longest genuine comment in the repository (45) and far below the 423-line accident
|
|
26
|
+
* this rule exists to catch, so it reports a paste and never a paragraph.
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_COMMENT_LINES = 60;
|
|
29
|
+
/** The human-readable report text (the assertion in tests matches this verbatim). */
|
|
30
|
+
export const RUNAWAY_COMMENT_MESSAGE = 'block comment is {{lines}} lines (max {{max}}) — a comment this long is usually pasted output, not prose';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/** @reticlehq/eslint-plugin — flat-config plugin export. */
|
|
2
2
|
import { requireSignalOnMutation } from './require-signal-on-mutation.js';
|
|
3
3
|
import { noInternalTags } from './no-internal-tags.js';
|
|
4
|
+
import { noRunawayComment } from './no-runaway-comment.js';
|
|
4
5
|
export declare const rules: {
|
|
5
6
|
readonly "require-signal-on-mutation": import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutationWithoutSignal", [import("./options.js").RawRuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
6
7
|
name: string;
|
|
7
8
|
};
|
|
8
|
-
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
9
|
+
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
readonly "no-runaway-comment": import("@typescript-eslint/utils/ts-eslint").RuleModule<"runawayComment", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
9
13
|
name: string;
|
|
10
14
|
};
|
|
11
15
|
};
|
|
@@ -17,11 +21,14 @@ declare const plugin: {
|
|
|
17
21
|
readonly "require-signal-on-mutation": import("@typescript-eslint/utils/ts-eslint").RuleModule<"mutationWithoutSignal", [import("./options.js").RawRuleOptions], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
18
22
|
name: string;
|
|
19
23
|
};
|
|
20
|
-
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
24
|
+
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
25
|
+
name: string;
|
|
26
|
+
};
|
|
27
|
+
readonly "no-runaway-comment": import("@typescript-eslint/utils/ts-eslint").RuleModule<"runawayComment", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
21
28
|
name: string;
|
|
22
29
|
};
|
|
23
30
|
};
|
|
24
31
|
configs: Record<string, unknown>;
|
|
25
32
|
};
|
|
26
33
|
export default plugin;
|
|
27
|
-
export { requireSignalOnMutation, noInternalTags };
|
|
34
|
+
export { requireSignalOnMutation, noInternalTags, noRunawayComment };
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/** @reticlehq/eslint-plugin — flat-config plugin export. */
|
|
2
2
|
import { requireSignalOnMutation } from './require-signal-on-mutation.js';
|
|
3
3
|
import { noInternalTags } from './no-internal-tags.js';
|
|
4
|
-
import {
|
|
4
|
+
import { noRunawayComment } from './no-runaway-comment.js';
|
|
5
|
+
import { PLUGIN_NAME, RULE_NAME, INTERNAL_TAGS_RULE_NAME, RUNAWAY_COMMENT_RULE_NAME, } from './constants.js';
|
|
5
6
|
export const rules = {
|
|
6
7
|
[RULE_NAME]: requireSignalOnMutation,
|
|
7
8
|
[INTERNAL_TAGS_RULE_NAME]: noInternalTags,
|
|
9
|
+
[RUNAWAY_COMMENT_RULE_NAME]: noRunawayComment,
|
|
8
10
|
};
|
|
9
11
|
const plugin = {
|
|
10
12
|
meta: { name: PLUGIN_NAME },
|
|
@@ -20,7 +22,8 @@ plugin.configs['recommended'] = {
|
|
|
20
22
|
rules: {
|
|
21
23
|
[`${PLUGIN_NAME}/${RULE_NAME}`]: 'warn',
|
|
22
24
|
[`${PLUGIN_NAME}/${INTERNAL_TAGS_RULE_NAME}`]: 'error',
|
|
25
|
+
[`${PLUGIN_NAME}/${RUNAWAY_COMMENT_RULE_NAME}`]: 'error',
|
|
23
26
|
},
|
|
24
27
|
};
|
|
25
28
|
export default plugin;
|
|
26
|
-
export { requireSignalOnMutation, noInternalTags };
|
|
29
|
+
export { requireSignalOnMutation, noInternalTags, noRunawayComment };
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule: no-internal-tags.
|
|
3
3
|
*
|
|
4
|
-
* Bans design-doc reference codes and internal version strings from comments,
|
|
5
|
-
*
|
|
4
|
+
* Bans design-doc reference codes and internal version strings from comments, TEST DESCRIPTIONS and
|
|
5
|
+
* the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
|
|
6
|
+
* that only means something in a document they cannot see.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* across the library packages alone, including in the contract package and in test descriptions, plus
|
|
10
|
-
* codes that leaked into user-facing docs. The gap was enforcement, not intent, so the rule is now a
|
|
11
|
-
* lint error rather than a paragraph.
|
|
8
|
+
* A path is the first thing a reader meets, before any comment, so `n5-ring-buffer.ts` is the shape
|
|
9
|
+
* this bans in the loudest available place.
|
|
12
10
|
*
|
|
13
11
|
* Deliberately narrow, to avoid punishing legitimate prose:
|
|
14
12
|
* - a bare `W11`, `B37`, `N5`, `M8` style token (one or two capitals + digits) standing alone;
|
|
@@ -18,6 +16,6 @@
|
|
|
18
16
|
* `H2`, `Welford M2` when part of a sentence) is matched only when it stands alone as a tag.
|
|
19
17
|
*/
|
|
20
18
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
21
|
-
export declare const noInternalTags: ESLintUtils.RuleModule<"internalTag", [], unknown, ESLintUtils.RuleListener> & {
|
|
19
|
+
export declare const noInternalTags: ESLintUtils.RuleModule<"internalTag" | "pathTag", [], unknown, ESLintUtils.RuleListener> & {
|
|
22
20
|
name: string;
|
|
23
21
|
};
|
package/dist/no-internal-tags.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule: no-internal-tags.
|
|
3
3
|
*
|
|
4
|
-
* Bans design-doc reference codes and internal version strings from comments,
|
|
5
|
-
*
|
|
4
|
+
* Bans design-doc reference codes and internal version strings from comments, TEST DESCRIPTIONS and
|
|
5
|
+
* the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
|
|
6
|
+
* that only means something in a document they cannot see.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* across the library packages alone, including in the contract package and in test descriptions, plus
|
|
10
|
-
* codes that leaked into user-facing docs. The gap was enforcement, not intent, so the rule is now a
|
|
11
|
-
* lint error rather than a paragraph.
|
|
8
|
+
* A path is the first thing a reader meets, before any comment, so `n5-ring-buffer.ts` is the shape
|
|
9
|
+
* this bans in the loudest available place.
|
|
12
10
|
*
|
|
13
11
|
* Deliberately narrow, to avoid punishing legitimate prose:
|
|
14
12
|
* - a bare `W11`, `B37`, `N5`, `M8` style token (one or two capitals + digits) standing alone;
|
|
@@ -17,33 +15,43 @@
|
|
|
17
15
|
* Ordinary technical prose that happens to contain a letter and digits inside a larger word (`UTF-8`,
|
|
18
16
|
* `H2`, `Welford M2` when part of a sentence) is matched only when it stands alone as a tag.
|
|
19
17
|
*/
|
|
18
|
+
import { relative, sep } from 'node:path';
|
|
20
19
|
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
|
|
21
20
|
import { DOCS_URL_ROOT } from './constants.js';
|
|
22
21
|
const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_URL_ROOT}#${name}`);
|
|
23
22
|
const INTERNAL_TAG_MESSAGE = "Internal reference '{{tag}}' does not belong in source. It means nothing to a reader without the design doc — describe the behaviour instead.";
|
|
24
|
-
|
|
25
|
-
const TRACKING_CODE = /(?<![A-Za-z0-9])[A-Z]{1,2}\d{1,2}(?:\.\d{1,2})?(?![A-Za-z0-9])/g;
|
|
23
|
+
const PATH_TAG_MESSAGE = "Internal reference '{{tag}}' does not belong in a file or directory name. It means nothing to a reader without the design doc — name the module after what it does.";
|
|
26
24
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
25
|
+
* `(W11)`, `B37`, `W10.3`, `N5:`, `P5a` — a short capital-prefixed code used as a label. The optional
|
|
26
|
+
* trailing letter is how a design document tells sibling parts apart. Exactly ONE, and lower case:
|
|
27
|
+
* two would start matching ordinary identifiers, and an upper-case one would catch names like `UTF8B`.
|
|
28
|
+
* Exempted by `ALLOWED` below.
|
|
29
|
+
*/
|
|
30
|
+
const TRACKING_CODE = /(?<![A-Za-z0-9])[A-Z]{1,2}\d{1,2}(?:\.\d{1,2})?[a-z]?(?![A-Za-z0-9])/g;
|
|
31
|
+
/**
|
|
32
|
+
* `§4.3` — a design-doc section reference. Flags by default and exempts only a citation of a NAMED
|
|
33
|
+
* EXTERNAL spec (`EXTERNAL_SPECS`); exempting any § preceded by a word would permit `see PLAN §4.3`,
|
|
34
|
+
* the very shape being banned.
|
|
32
35
|
*/
|
|
33
36
|
const SECTION_REF = /§\s?\d+(?:\.\d+)*/g;
|
|
34
37
|
/** Specs whose section numbers are real references a reader can follow. */
|
|
35
38
|
const EXTERNAL_SPECS = /\b(RFC|WCAG|ECMA|ISO|IEEE|W3C|WHATWG|HTML|CSS|ARIA|HTTP|OAuth|JSON-LD)\b[^§]{0,20}$/i;
|
|
36
39
|
/**
|
|
37
|
-
* `v2.2.0` — an INTERNAL version string.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* "any capitalised preceding word" permitted `new in Reticle v2.2.0` — our own product name made the
|
|
41
|
-
* banned string legal.
|
|
40
|
+
* `v2.2.0` — an INTERNAL version string. Flags by default and exempts only a version attributed to a
|
|
41
|
+
* NAMED third party (`THIRD_PARTY`); exempting any capitalised preceding word would make
|
|
42
|
+
* `new in Reticle v2.2.0` legal.
|
|
42
43
|
*/
|
|
43
44
|
const VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/g;
|
|
45
|
+
/**
|
|
46
|
+
* The same two shapes over a PATH, case-insensitively: file names here are kebab-case, so `N5` in a
|
|
47
|
+
* sentence reads `n5` in a path and the upper-case patterns above cannot see it. The boundaries do the
|
|
48
|
+
* work that case was doing — `e2e`, `utf8`, `http2` and `base64` all need a non-alphanumeric on both
|
|
49
|
+
* sides of a letters-then-digits token, and so fail them.
|
|
50
|
+
*/
|
|
51
|
+
const PATH_TRACKING_CODE = /(?<![A-Za-z0-9])[A-Za-z]{1,2}\d{1,2}(?:\.\d{1,2})?(?![A-Za-z0-9])/g;
|
|
52
|
+
const PATH_VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/gi;
|
|
44
53
|
/** Third-party names whose versions are legitimate prose. Reticle is deliberately NOT here. */
|
|
45
54
|
const THIRD_PARTY = /\b(React|Vite|Node|TypeScript|Playwright|Chrome|Chromium|Firefox|Safari|Zod|Vitest|ESLint|pnpm|MCP)\b[^v]{0,12}$/i;
|
|
46
|
-
/** Tokens that look like codes but are established technical terms. */
|
|
47
55
|
/** Test-block callees whose first argument is a human-readable description. */
|
|
48
56
|
const TEST_BLOCKS = new Set(['describe', 'it', 'test', 'suite', 'bench']);
|
|
49
57
|
/**
|
|
@@ -69,13 +77,11 @@ const ALLOWED = new Set([
|
|
|
69
77
|
'Q4', // quarters
|
|
70
78
|
'L1',
|
|
71
79
|
'L2', // cache levels
|
|
72
|
-
// The JavaScript engine
|
|
73
|
-
// GC in a JavaScript codebase — the most likely place this rule runs, and the fastest way to get it
|
|
74
|
-
// switched off. Found by running the rule over directories the linter had never reached.
|
|
80
|
+
// The JavaScript engine — without it the rule fires on ordinary writing about heap growth and GC.
|
|
75
81
|
'V8',
|
|
76
82
|
'H4',
|
|
77
83
|
'H5',
|
|
78
|
-
'H6', // heading levels
|
|
84
|
+
'H6', // heading levels
|
|
79
85
|
]);
|
|
80
86
|
export const noInternalTags = createRule({
|
|
81
87
|
name: 'no-internal-tags',
|
|
@@ -83,7 +89,7 @@ export const noInternalTags = createRule({
|
|
|
83
89
|
type: 'problem',
|
|
84
90
|
docs: { description: 'Ban design-doc codes and internal version strings from comments.' },
|
|
85
91
|
schema: [],
|
|
86
|
-
messages: { internalTag: INTERNAL_TAG_MESSAGE },
|
|
92
|
+
messages: { internalTag: INTERNAL_TAG_MESSAGE, pathTag: PATH_TAG_MESSAGE },
|
|
87
93
|
},
|
|
88
94
|
defaultOptions: [],
|
|
89
95
|
create(context) {
|
|
@@ -109,18 +115,62 @@ export const noInternalTags = createRule({
|
|
|
109
115
|
}
|
|
110
116
|
return found;
|
|
111
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* Banned tokens in this module's own path, relative to the project root.
|
|
120
|
+
*
|
|
121
|
+
* Relative, never absolute: an absolute path runs through directories nobody chose as part of
|
|
122
|
+
* this codebase (a CI runner's workspace, a developer's home) and flagging those would report a
|
|
123
|
+
* violation the author cannot fix.
|
|
124
|
+
*/
|
|
125
|
+
const pathTags = () => {
|
|
126
|
+
const found = new Set();
|
|
127
|
+
const filename = context.filename;
|
|
128
|
+
// `<input>` and `<text>` are what a linter reports for source with no file, e.g. a RuleTester
|
|
129
|
+
// case or a piped snippet. There is no path to judge.
|
|
130
|
+
if (filename.startsWith('<'))
|
|
131
|
+
return found;
|
|
132
|
+
const rel = relative(context.cwd, filename);
|
|
133
|
+
// Outside the project root entirely: `relative` climbs out with `..`, and the segments above
|
|
134
|
+
// the root are not this codebase's to answer for.
|
|
135
|
+
if (rel.startsWith('..'))
|
|
136
|
+
return found;
|
|
137
|
+
const path = rel.split(sep).join('/');
|
|
138
|
+
// Version FIRST, and its spans claimed, because the tracking-code shape is a prefix of it:
|
|
139
|
+
// `replay-v2.2.0.ts` matches `v2.2.0` as a version and `v2.2` as a code, and reporting both
|
|
140
|
+
// would name the same offence twice under two different explanations.
|
|
141
|
+
const claimed = [];
|
|
142
|
+
for (const pattern of [PATH_VERSION_STRING, PATH_TRACKING_CODE]) {
|
|
143
|
+
pattern.lastIndex = 0;
|
|
144
|
+
for (const match of path.matchAll(pattern)) {
|
|
145
|
+
const start = match.index ?? 0;
|
|
146
|
+
const end = start + match[0].length;
|
|
147
|
+
if (claimed.some((c) => start < c.end && end > c.start))
|
|
148
|
+
continue;
|
|
149
|
+
const tag = match[0].trim();
|
|
150
|
+
claimed.push({ start, end });
|
|
151
|
+
if (ALLOWED.has(tag.replace(/[^A-Za-z0-9]/g, '').toUpperCase()))
|
|
152
|
+
continue;
|
|
153
|
+
found.add(tag);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return found;
|
|
157
|
+
};
|
|
112
158
|
return {
|
|
113
|
-
Program() {
|
|
159
|
+
Program(node) {
|
|
114
160
|
for (const comment of context.sourceCode.getAllComments()) {
|
|
115
161
|
for (const tag of tagsIn(comment.value)) {
|
|
116
162
|
context.report({ node: comment, messageId: 'internalTag', data: { tag } });
|
|
117
163
|
}
|
|
118
164
|
}
|
|
165
|
+
// Reported on the Program node: the offence is the module's name, which has no node of its
|
|
166
|
+
// own, and the first line is where a reader will look for it.
|
|
167
|
+
for (const tag of pathTags()) {
|
|
168
|
+
context.report({ node, messageId: 'pathTag', data: { tag } });
|
|
169
|
+
}
|
|
119
170
|
},
|
|
120
171
|
/**
|
|
121
|
-
* TEST DESCRIPTIONS are string literals, not comments, so a comment-only rule
|
|
122
|
-
*
|
|
123
|
-
* claimed to have removed them, precisely because nothing mechanical was looking here.
|
|
172
|
+
* TEST DESCRIPTIONS are string literals, not comments, so a comment-only rule cannot see them —
|
|
173
|
+
* and the project rule names them explicitly.
|
|
124
174
|
*/
|
|
125
175
|
CallExpression(node) {
|
|
126
176
|
const callee = node.callee;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-runaway-comment.
|
|
3
|
+
*
|
|
4
|
+
* Caps a single block comment's length.
|
|
5
|
+
*
|
|
6
|
+
* Incident: commit `ab26e749` authored a doc comment through an unquoted heredoc, so the shell
|
|
7
|
+
* evaluated the backticked code spans inside it. `git ls-files` expanded into the file, pasting 407
|
|
8
|
+
* lines of a private repository's layout — a prod CA cert path, a licence-keypair script, a deploy
|
|
9
|
+
* secrets template — into `server/src/language/flows/flows.ts`, and deleting every other code span
|
|
10
|
+
* the comment named. It compiled, it typechecked, it passed every gate, and it reached the published
|
|
11
|
+
* tarball because `dist` retains comments.
|
|
12
|
+
*
|
|
13
|
+
* The cap is the cheapest check that catches it: the accident was 423 lines and the longest genuine
|
|
14
|
+
* comment in the repository is 45, so nothing legitimate is near the limit.
|
|
15
|
+
*/
|
|
16
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
17
|
+
export declare const noRunawayComment: ESLintUtils.RuleModule<"runawayComment", [], unknown, ESLintUtils.RuleListener> & {
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule: no-runaway-comment.
|
|
3
|
+
*
|
|
4
|
+
* Caps a single block comment's length.
|
|
5
|
+
*
|
|
6
|
+
* Incident: commit `ab26e749` authored a doc comment through an unquoted heredoc, so the shell
|
|
7
|
+
* evaluated the backticked code spans inside it. `git ls-files` expanded into the file, pasting 407
|
|
8
|
+
* lines of a private repository's layout — a prod CA cert path, a licence-keypair script, a deploy
|
|
9
|
+
* secrets template — into `server/src/language/flows/flows.ts`, and deleting every other code span
|
|
10
|
+
* the comment named. It compiled, it typechecked, it passed every gate, and it reached the published
|
|
11
|
+
* tarball because `dist` retains comments.
|
|
12
|
+
*
|
|
13
|
+
* The cap is the cheapest check that catches it: the accident was 423 lines and the longest genuine
|
|
14
|
+
* comment in the repository is 45, so nothing legitimate is near the limit.
|
|
15
|
+
*/
|
|
16
|
+
import { AST_TOKEN_TYPES, ESLintUtils } from '@typescript-eslint/utils';
|
|
17
|
+
import { DOCS_URL_ROOT, MAX_COMMENT_LINES, RUNAWAY_COMMENT_MESSAGE } from './constants.js';
|
|
18
|
+
const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_URL_ROOT}#${name}`);
|
|
19
|
+
export const noRunawayComment = createRule({
|
|
20
|
+
name: 'no-runaway-comment',
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: { description: 'Cap the length of a single block comment.' },
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: { runawayComment: RUNAWAY_COMMENT_MESSAGE },
|
|
26
|
+
},
|
|
27
|
+
defaultOptions: [],
|
|
28
|
+
create(context) {
|
|
29
|
+
return {
|
|
30
|
+
Program() {
|
|
31
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
32
|
+
if (AST_TOKEN_TYPES.Block !== comment.type)
|
|
33
|
+
continue;
|
|
34
|
+
const lines = comment.loc.end.line - comment.loc.start.line + 1;
|
|
35
|
+
if (lines <= MAX_COMMENT_LINES)
|
|
36
|
+
continue;
|
|
37
|
+
context.report({
|
|
38
|
+
loc: comment.loc,
|
|
39
|
+
messageId: 'runawayComment',
|
|
40
|
+
data: { lines: String(lines), max: String(MAX_COMMENT_LINES) },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/eslint-plugin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Reticle ESLint plugin — keeps the signal layer self-enforcing (mutation-without-signal).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
27
|
"url": "git+https://github.com/reticlehq/reticle.git",
|
|
28
|
-
"directory": "
|
|
28
|
+
"directory": "adapters/lint/eslint"
|
|
29
29
|
},
|
|
30
30
|
"homepage": "https://github.com/reticlehq/reticle#readme",
|
|
31
31
|
"bugs": "https://github.com/reticlehq/reticle/issues",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"node": ">=20.0.0"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"build": "tsc -b",
|
|
52
|
+
"build": "tsc -b && tsc-alias",
|
|
53
53
|
"typecheck": "tsc -b",
|
|
54
54
|
"lint": "eslint src",
|
|
55
55
|
"test:unit": "vitest run src --passWithNoTests"
|