@comity-dev/eslint-plugin 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Reusable ESLint rules for Comity-wide source-code policy. This package owns the
6
6
 
7
7
  ## Scope
8
8
 
9
- Six rules, each addressing a Comity-wide concern:
9
+ Seven rules, each addressing a Comity-wide concern:
10
10
 
11
11
  | Rule | Concern |
12
12
  | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -16,6 +16,7 @@ Six rules, each addressing a Comity-wide concern:
16
16
  | `no-crypto-in-core` | Forbid `crypto` in Core Modules (domain must be deterministic). |
17
17
  | `no-forbidden-subpath-import` | Disallow imports from forbidden package subpaths (`utils`, `helpers`, `shared`, `internal`, `lazy`). |
18
18
  | `no-render-core-http-runtime` | Forbid render Core Modules from depending on HTTP runtime. |
19
+ | `require-internal-marker` | Require an `@internal` JSDoc tag on production sources under `src/internal/` and `src/lazy/` (owner-approved Contract B). |
19
20
 
20
21
  ## Ownership
21
22
 
package/dist/plugin.js CHANGED
@@ -4,6 +4,7 @@ import noForbiddenDeepImport from "./rules/no-forbidden-deep-import.js";
4
4
  import noForbiddenSubpathImport from "./rules/no-forbidden-subpath-import.js";
5
5
  import noLayerViolation from "./rules/no-layer-violation.js";
6
6
  import noRenderCoreHttpRuntime from "./rules/no-render-core-http-runtime.js";
7
+ import requireInternalMarker from "./rules/require-internal-marker.js";
7
8
  export const BUILTIN_RULES = [
8
9
  { name: "no-forbidden-deep-import", rule: noForbiddenDeepImport },
9
10
  { name: "no-layer-violation", rule: noLayerViolation },
@@ -11,6 +12,7 @@ export const BUILTIN_RULES = [
11
12
  { name: "no-crypto-in-core", rule: noCryptoInCore },
12
13
  { name: "no-forbidden-subpath-import", rule: noForbiddenSubpathImport },
13
14
  { name: "no-render-core-http-runtime", rule: noRenderCoreHttpRuntime },
15
+ { name: "require-internal-marker", rule: requireInternalMarker },
14
16
  ];
15
17
  export const plugin = {
16
18
  meta: {
@@ -6,5 +6,6 @@ export const recommended = {
6
6
  "@comity-dev/no-crypto-in-core": "error",
7
7
  "@comity-dev/no-forbidden-subpath-import": "error",
8
8
  "@comity-dev/no-render-core-http-runtime": "error",
9
+ "@comity-dev/require-internal-marker": "error",
9
10
  },
10
11
  };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @comity-dev/require-internal-marker
3
+ *
4
+ * Contract B: production TypeScript sources under `packages/<name>/src/internal/`
5
+ * or `packages/<name>/src/lazy/` MUST declare an `@internal` JSDoc tag.
6
+ *
7
+ * Both accepted marker forms are equivalent: the inline form (the tag on the
8
+ * same line as the JSDoc opener) and the conventional multiline form (the tag
9
+ * on its own line inside the block).
10
+ *
11
+ * The marker must be an actual JSDoc tag, so it must occupy a JSDoc tag
12
+ * position — immediately after the block opener, or at the start of a line
13
+ * inside the block after the optional leading asterisk. Consequences:
14
+ *
15
+ * - A prose mention of `@internal` inside a JSDoc description is not a tag.
16
+ * - `@internalOnly` and `@internal-extra` are different tag names.
17
+ * - Line comments, ordinary block comments, and marker-like text inside
18
+ * strings, template literals, tagged templates, or regular-expression
19
+ * literals never satisfy the rule, because the parser only produces
20
+ * comment tokens for real comments.
21
+ *
22
+ * The requirement is file-level: a file satisfies it when it contains a valid
23
+ * `@internal` JSDoc block followed by code, since the marker annotates the
24
+ * file's declarations. A marker attached to any declaration — top-level,
25
+ * class member, or nested — satisfies it, and a trailing marker with no
26
+ * following code does not. The repository's internal sources attach the
27
+ * docblock to the declaration it documents, which is the narrowest reading
28
+ * consistent with the approved contract.
29
+ *
30
+ * Scope: production TypeScript sources under the two internal directories
31
+ * only. Test files, mocks, fixtures, declaration/generated output, build and
32
+ * coverage output, and dependency directories are excluded, matching the
33
+ * repository's established ESLint conventions.
34
+ *
35
+ * Traceability: the marker policy is an owner-approved contract decision. It
36
+ * is NOT defined by public-api.md §3.2, which forbids public `/internal` and
37
+ * `/lazy` sub-entrypoints. This rule replaces the retired Semgrep rule
38
+ * `comity-module-must-have-internal-marker`, which could not inspect real
39
+ * JSDoc syntax (Semgrep strips comments from structural patterns).
40
+ */
41
+ type Rule = unknown;
42
+ declare const rule: Rule;
43
+ export default rule;
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @comity-dev/require-internal-marker
3
+ *
4
+ * Contract B: production TypeScript sources under `packages/<name>/src/internal/`
5
+ * or `packages/<name>/src/lazy/` MUST declare an `@internal` JSDoc tag.
6
+ *
7
+ * Both accepted marker forms are equivalent: the inline form (the tag on the
8
+ * same line as the JSDoc opener) and the conventional multiline form (the tag
9
+ * on its own line inside the block).
10
+ *
11
+ * The marker must be an actual JSDoc tag, so it must occupy a JSDoc tag
12
+ * position — immediately after the block opener, or at the start of a line
13
+ * inside the block after the optional leading asterisk. Consequences:
14
+ *
15
+ * - A prose mention of `@internal` inside a JSDoc description is not a tag.
16
+ * - `@internalOnly` and `@internal-extra` are different tag names.
17
+ * - Line comments, ordinary block comments, and marker-like text inside
18
+ * strings, template literals, tagged templates, or regular-expression
19
+ * literals never satisfy the rule, because the parser only produces
20
+ * comment tokens for real comments.
21
+ *
22
+ * The requirement is file-level: a file satisfies it when it contains a valid
23
+ * `@internal` JSDoc block followed by code, since the marker annotates the
24
+ * file's declarations. A marker attached to any declaration — top-level,
25
+ * class member, or nested — satisfies it, and a trailing marker with no
26
+ * following code does not. The repository's internal sources attach the
27
+ * docblock to the declaration it documents, which is the narrowest reading
28
+ * consistent with the approved contract.
29
+ *
30
+ * Scope: production TypeScript sources under the two internal directories
31
+ * only. Test files, mocks, fixtures, declaration/generated output, build and
32
+ * coverage output, and dependency directories are excluded, matching the
33
+ * repository's established ESLint conventions.
34
+ *
35
+ * Traceability: the marker policy is an owner-approved contract decision. It
36
+ * is NOT defined by public-api.md §3.2, which forbids public `/internal` and
37
+ * `/lazy` sub-entrypoints. This rule replaces the retired Semgrep rule
38
+ * `comity-module-must-have-internal-marker`, which could not inspect real
39
+ * JSDoc syntax (Semgrep strips comments from structural patterns).
40
+ */
41
+ const messages = {
42
+ missing: "Production sources under `src/internal/` or `src/lazy/` MUST declare an `@internal` JSDoc tag (inline or multiline).",
43
+ };
44
+ /** File extensions treated as production TypeScript sources. */
45
+ const TYPESCRIPT_FILE = /\.(?:ts|tsx|mts|cts)$/;
46
+ /** Test files: excluded by the validator ESLint engine and the consumer config. */
47
+ const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
48
+ /** Declaration output is generated by `tsc`; the consumer config ignores it. */
49
+ const DECLARATION_FILE = /\.d\.[cm]?ts$/;
50
+ /** Generated sources are excluded by the consumer's ESLint ignore list. */
51
+ const GENERATED_FILE = /\.generated\./;
52
+ /**
53
+ * Path segments excluded by repository convention: dependency directories,
54
+ * build/coverage output, and test/mock/fixture trees (the consumer keeps real
55
+ * `src/__mocks__` directories, and the retired Semgrep rule excluded
56
+ * `__fixtures__`).
57
+ */
58
+ const EXCLUDED_SEGMENTS = new Set([
59
+ "node_modules",
60
+ "dist",
61
+ "build",
62
+ "out",
63
+ "coverage",
64
+ "__tests__",
65
+ "__fixtures__",
66
+ "__mocks__",
67
+ ]);
68
+ /** `packages/<name>/src/internal/` or `packages/<name>/src/lazy/` */
69
+ const SCOPED_PATH = /(?:^|\/)packages\/[^/]+\/src\/(?:internal|lazy)\//;
70
+ function normalizeFilename(filename) {
71
+ return filename.replace(/\\/g, "/");
72
+ }
73
+ function isScopedProductionFile(filename) {
74
+ if (!filename)
75
+ return false;
76
+ const normalized = normalizeFilename(filename);
77
+ if (!TYPESCRIPT_FILE.test(normalized))
78
+ return false;
79
+ if (TEST_FILE.test(normalized))
80
+ return false;
81
+ if (DECLARATION_FILE.test(normalized))
82
+ return false;
83
+ if (GENERATED_FILE.test(normalized))
84
+ return false;
85
+ if (normalized.split("/").some((segment) => EXCLUDED_SEGMENTS.has(segment))) {
86
+ return false;
87
+ }
88
+ return SCOPED_PATH.test(normalized);
89
+ }
90
+ /** A JSDoc tag must not be followed by more tag-name characters. */
91
+ const INTERNAL_TAG = /^[ \t]*@internal(?![A-Za-z0-9_-])/;
92
+ /**
93
+ * Whether a JSDoc block comment body declares the `@internal` tag.
94
+ *
95
+ * @param value - The comment value. A JSDoc block starts with `*`.
96
+ *
97
+ * @remarks
98
+ * The tag must begin a line inside the block (after the optional leading
99
+ * asterisk) or immediately follow the block opener, which is where JSDoc
100
+ * allows tags. Prose that merely mentions `@internal` is therefore not a tag.
101
+ */
102
+ function declaresInternalTag(value) {
103
+ const lines = value.split(/\r?\n/);
104
+ for (let index = 0; index < lines.length; index++) {
105
+ const raw = lines[index] ?? "";
106
+ const content = index === 0 ? raw.slice(1) : raw.replace(/^[ \t]*/, "");
107
+ if (index > 0 && content.startsWith("*")) {
108
+ if (INTERNAL_TAG.test(content.slice(1)))
109
+ return true;
110
+ continue;
111
+ }
112
+ if (INTERNAL_TAG.test(content))
113
+ return true;
114
+ }
115
+ return false;
116
+ }
117
+ const rule = {
118
+ meta: {
119
+ type: "problem",
120
+ docs: {
121
+ description: "Require an `@internal` JSDoc marker on production sources under `src/internal/` and `src/lazy/`.",
122
+ category: "Comity Architecture",
123
+ recommended: true,
124
+ },
125
+ schema: [],
126
+ messages,
127
+ },
128
+ create(context) {
129
+ const filename = context.filename ?? context.getFilename();
130
+ if (!isScopedProductionFile(String(filename ?? "")))
131
+ return {};
132
+ return {
133
+ Program(node) {
134
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
135
+ const lastToken = sourceCode.getLastToken(node);
136
+ for (const comment of sourceCode.getAllComments()) {
137
+ if (comment.type !== "Block")
138
+ continue;
139
+ const value = String(comment.value ?? "");
140
+ // JSDoc block comments start with `*`; `/* ... */` does not.
141
+ if (!value.startsWith("*"))
142
+ continue;
143
+ if (!declaresInternalTag(value))
144
+ continue;
145
+ // The marker must annotate following code, not trail the file.
146
+ if (lastToken && lastToken.range[0] > comment.range[1])
147
+ return;
148
+ }
149
+ context.report({ node, messageId: "missing" });
150
+ },
151
+ };
152
+ },
153
+ };
154
+ export default rule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comity-dev/eslint-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable ESLint rules for Comity source-code policy. Development tooling.",
5
5
  "type": "module",
6
6
  "private": false,