@reticlehq/eslint-plugin 2.14.0 → 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 +8 -1
- package/dist/index.js +5 -2
- package/dist/no-internal-tags.d.ts +2 -9
- package/dist/no-internal-tags.js +23 -35
- 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,6 +1,7 @@
|
|
|
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;
|
|
@@ -8,6 +9,9 @@ export declare const rules: {
|
|
|
8
9
|
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
9
10
|
name: string;
|
|
10
11
|
};
|
|
12
|
+
readonly "no-runaway-comment": import("@typescript-eslint/utils/ts-eslint").RuleModule<"runawayComment", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
13
|
+
name: string;
|
|
14
|
+
};
|
|
11
15
|
};
|
|
12
16
|
declare const plugin: {
|
|
13
17
|
meta: {
|
|
@@ -20,8 +24,11 @@ declare const plugin: {
|
|
|
20
24
|
readonly "no-internal-tags": import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalTag" | "pathTag", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
21
25
|
name: string;
|
|
22
26
|
};
|
|
27
|
+
readonly "no-runaway-comment": import("@typescript-eslint/utils/ts-eslint").RuleModule<"runawayComment", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
28
|
+
name: string;
|
|
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 };
|
|
@@ -5,15 +5,8 @@
|
|
|
5
5
|
* the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
|
|
6
6
|
* that only means something in a document they cannot see.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* exists to prevent stated in the loudest available place.
|
|
11
|
-
*
|
|
12
|
-
* This rule exists because the prose version of it did not hold. An audit of the codebase found the
|
|
13
|
-
* machine-checked rules at ~100% compliance and every prose-only rule violated — 59 tracking codes
|
|
14
|
-
* across the library packages alone, including in the contract package and in test descriptions, plus
|
|
15
|
-
* codes that leaked into user-facing docs. The gap was enforcement, not intent, so the rule is now a
|
|
16
|
-
* 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.
|
|
17
10
|
*
|
|
18
11
|
* Deliberately narrow, to avoid punishing legitimate prose:
|
|
19
12
|
* - a bare `W11`, `B37`, `N5`, `M8` style token (one or two capitals + digits) standing alone;
|
package/dist/no-internal-tags.js
CHANGED
|
@@ -5,15 +5,8 @@
|
|
|
5
5
|
* the FILE AND DIRECTORY NAMES of the module itself, so a reader of the source never meets a token
|
|
6
6
|
* that only means something in a document they cannot see.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* exists to prevent stated in the loudest available place.
|
|
11
|
-
*
|
|
12
|
-
* This rule exists because the prose version of it did not hold. An audit of the codebase found the
|
|
13
|
-
* machine-checked rules at ~100% compliance and every prose-only rule violated — 59 tracking codes
|
|
14
|
-
* across the library packages alone, including in the contract package and in test descriptions, plus
|
|
15
|
-
* codes that leaked into user-facing docs. The gap was enforcement, not intent, so the rule is now a
|
|
16
|
-
* 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.
|
|
17
10
|
*
|
|
18
11
|
* Deliberately narrow, to avoid punishing legitimate prose:
|
|
19
12
|
* - a bare `W11`, `B37`, `N5`, `M8` style token (one or two capitals + digits) standing alone;
|
|
@@ -28,39 +21,37 @@ import { DOCS_URL_ROOT } from './constants.js';
|
|
|
28
21
|
const createRule = ESLintUtils.RuleCreator((name) => `${DOCS_URL_ROOT}#${name}`);
|
|
29
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.";
|
|
30
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.";
|
|
31
|
-
/** `(W11)`, `B37`, `W10.3`, `N5:` — a short capital-prefixed code used as a label. */
|
|
32
|
-
const TRACKING_CODE = /(?<![A-Za-z0-9])[A-Z]{1,2}\d{1,2}(?:\.\d{1,2})?(?![A-Za-z0-9])/g;
|
|
33
24
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
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.
|
|
39
35
|
*/
|
|
40
36
|
const SECTION_REF = /§\s?\d+(?:\.\d+)*/g;
|
|
41
37
|
/** Specs whose section numbers are real references a reader can follow. */
|
|
42
38
|
const EXTERNAL_SPECS = /\b(RFC|WCAG|ECMA|ISO|IEEE|W3C|WHATWG|HTML|CSS|ARIA|HTTP|OAuth|JSON-LD)\b[^§]{0,20}$/i;
|
|
43
39
|
/**
|
|
44
|
-
* `v2.2.0` — an INTERNAL version string.
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* "any capitalised preceding word" permitted `new in Reticle v2.2.0` — our own product name made the
|
|
48
|
-
* 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.
|
|
49
43
|
*/
|
|
50
44
|
const VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/g;
|
|
51
45
|
/**
|
|
52
|
-
* The same two shapes over a PATH, case-insensitively
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `e2e`, `utf8`, `http2` and `base64` all fail them, because each needs a non-alphanumeric on both
|
|
57
|
-
* sides of a letters-then-digits token.
|
|
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.
|
|
58
50
|
*/
|
|
59
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;
|
|
60
52
|
const PATH_VERSION_STRING = /(?<![A-Za-z0-9])v\d+\.\d+\.\d+(?![A-Za-z0-9])/gi;
|
|
61
53
|
/** Third-party names whose versions are legitimate prose. Reticle is deliberately NOT here. */
|
|
62
54
|
const THIRD_PARTY = /\b(React|Vite|Node|TypeScript|Playwright|Chrome|Chromium|Firefox|Safari|Zod|Vitest|ESLint|pnpm|MCP)\b[^v]{0,12}$/i;
|
|
63
|
-
/** Tokens that look like codes but are established technical terms. */
|
|
64
55
|
/** Test-block callees whose first argument is a human-readable description. */
|
|
65
56
|
const TEST_BLOCKS = new Set(['describe', 'it', 'test', 'suite', 'bench']);
|
|
66
57
|
/**
|
|
@@ -86,13 +77,11 @@ const ALLOWED = new Set([
|
|
|
86
77
|
'Q4', // quarters
|
|
87
78
|
'L1',
|
|
88
79
|
'L2', // cache levels
|
|
89
|
-
// The JavaScript engine
|
|
90
|
-
// GC in a JavaScript codebase — the most likely place this rule runs, and the fastest way to get it
|
|
91
|
-
// 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.
|
|
92
81
|
'V8',
|
|
93
82
|
'H4',
|
|
94
83
|
'H5',
|
|
95
|
-
'H6', // heading levels
|
|
84
|
+
'H6', // heading levels
|
|
96
85
|
]);
|
|
97
86
|
export const noInternalTags = createRule({
|
|
98
87
|
name: 'no-internal-tags',
|
|
@@ -180,9 +169,8 @@ export const noInternalTags = createRule({
|
|
|
180
169
|
}
|
|
181
170
|
},
|
|
182
171
|
/**
|
|
183
|
-
* TEST DESCRIPTIONS are string literals, not comments, so a comment-only rule
|
|
184
|
-
*
|
|
185
|
-
* 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.
|
|
186
174
|
*/
|
|
187
175
|
CallExpression(node) {
|
|
188
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"
|