@starci/eslint-canon-be 3.0.10 → 3.2.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/README.md +2 -1
  3. package/builders.mjs +1 -1
  4. package/comments.mjs +1 -1
  5. package/config-owner.mjs +1 -1
  6. package/index.mjs +2 -0
  7. package/lib/config.mjs +4 -1
  8. package/lib/doors.mjs +3 -1
  9. package/module-shape.mjs +1 -2
  10. package/package.json +1 -1
  11. package/resilience.mjs +1 -1
  12. package/runtime/config.example.yaml +194 -0
  13. package/runtime/engine/by-code-unit.mjs +12 -0
  14. package/runtime/engine/config.mjs +222 -239
  15. package/runtime/engine/invalid-config.mjs +5 -5
  16. package/runtime/engine/model-config.mjs +90 -0
  17. package/runtime/engine/orca-config.mjs +3 -1
  18. package/runtime/engine/release-config.mjs +28 -0
  19. package/runtime/engine/removed-vocabulary.mjs +28 -0
  20. package/runtime/engine/resources-config.mjs +52 -0
  21. package/runtime/engine/runtime-root.mjs +17 -0
  22. package/runtime/engine/secrets.mjs +35 -31
  23. package/runtime/engine/sonar-config.mjs +21 -0
  24. package/runtime/engine/temp-root.mjs +32 -0
  25. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  26. package/runtime/knowledge/hfs/rules.yaml +161 -65
  27. package/runtime/knowledge/hfs/slots.yaml +11 -14
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +126 -265
  31. package/runtime/modules/models/tiers.yaml +66 -0
  32. package/runtime/scripts/api/git/lib.mjs +34 -1
  33. package/runtime/scripts/hfs/allows.mjs +3 -3
  34. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  35. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  36. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  37. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  38. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  39. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  40. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  41. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  42. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  43. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  44. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  45. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  46. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  47. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  48. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  49. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  50. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  51. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  52. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  53. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  54. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  55. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  56. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  57. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  58. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  59. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  60. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  61. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  63. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  64. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  65. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  66. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  67. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  68. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  69. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  70. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  71. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  72. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  73. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  74. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  75. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  76. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  77. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  78. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  79. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  80. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  81. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  82. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  83. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  84. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  85. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  86. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  87. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  88. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  89. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  90. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  91. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  92. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  93. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  94. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  95. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  96. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  97. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  98. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  99. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  100. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  101. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  102. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  103. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  104. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  105. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  106. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  107. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  108. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  109. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  110. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  111. package/runtime/scripts/hfs/project-graph.mjs +4 -1
  112. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  113. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  114. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  115. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  116. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  117. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  118. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  119. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  120. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  121. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  122. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  123. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  124. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  125. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  126. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  127. package/runtime/scripts/hfs/slots.mjs +74 -695
  128. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  129. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  130. package/runtime/scripts/hfs/view.mjs +1 -1
  131. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  132. package/runtime/scripts/lib/git.mjs +2 -2
  133. package/runtime/scripts/lib/i18n.mjs +3 -2
  134. package/runtime/scripts/lib/language.mjs +3 -3
  135. package/runtime/scripts/lib/list.mjs +8 -1
  136. package/runtime/scripts/lib/path-key.mjs +38 -5
  137. package/runtime/scripts/lib/regex.mjs +1 -1
  138. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  139. package/runtime/scripts/lib/sonar-syntax-rules.mjs +225 -0
  140. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  141. package/runtime/scripts/lib/stack-services.mjs +1 -2
  142. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  143. package/runtime/scripts/lib/walk.mjs +6 -1
  144. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  145. package/schema-authority.mjs +1 -1
  146. package/service-deps.mjs +1 -1
  147. package/sonar-parity.mjs +19 -0
  148. package/suppression.mjs +2 -6
  149. package/test-world.mjs +2 -10
@@ -0,0 +1,8 @@
1
+ // Trailing-slash trimming for slot paths and generated roots, linear in the length of the text.
2
+
3
+ /** `text` without its trailing `/` characters. */
4
+ export function trimTrailingSlashes(text) {
5
+ let end = text.length;
6
+ while (end > 0 && text.codePointAt(end - 1) === 47) end -= 1;
7
+ return text.slice(0, end);
8
+ }
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { createRequire } from 'node:module';
3
2
  import path from 'node:path';
3
+ import { byCodeUnit } from '../lib/list.mjs';
4
4
 
5
5
  // One TypeScript program per (compiler, root names, compiler options, project references) inside a program run, and
6
6
  // one value per (kind, compiler, input) for what callers derive from those programs (the architecture context).
@@ -27,7 +27,7 @@ function keyOf(value) {
27
27
  }
28
28
  if (typeof value !== 'object' || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) return null;
29
29
  const entries = [];
30
- for (const name of Object.keys(value).sort()) {
30
+ for (const name of Object.keys(value).sort(byCodeUnit)) {
31
31
  const item = keyOf(value[name]);
32
32
  if (item === null) return null;
33
33
  entries.push(`${JSON.stringify(name)}:${item}`);
@@ -44,7 +44,10 @@ export function sharedInProgramRun(kind, ts, input, build) {
44
44
  const key = values ? keyOf(input) : null;
45
45
  if (key === null) return build();
46
46
  let byKey = values.get(ts);
47
- if (!byKey) values.set(ts, byKey = new Map());
47
+ if (!byKey) {
48
+ byKey = new Map();
49
+ values.set(ts, byKey);
50
+ }
48
51
  const id = `${kind}\0${key}`;
49
52
  if (!byKey.has(id)) byKey.set(id, build());
50
53
  return byKey.get(id);
@@ -74,4 +77,3 @@ export function typeScriptProjectReferencePath(ts, reference) {
74
77
  export function resolveTypeScriptModule(ts, specifier, from, options, host = ts.sys) {
75
78
  return ts.resolveModuleName(specifier, from, options, host).resolvedModule?.resolvedFileName;
76
79
  }
77
-
@@ -6,7 +6,7 @@ import path from 'node:path';
6
6
  import { allowsFile } from './allows.mjs';
7
7
  import { openHfs } from './slots.mjs';
8
8
 
9
- const posix = (p) => String(p).replace(/\\/g, '/');
9
+ const posix = (p) => String(p).replaceAll('\\', '/');
10
10
  const isAbsoluteAny = (text) => path.isAbsolute(text) || /^[A-Za-z]:\//.test(text);
11
11
 
12
12
  /**
@@ -58,8 +58,8 @@ export function shellCommands(body) {
58
58
  } else if (char === '"' || char === "'") {
59
59
  quote = char;
60
60
  current += char;
61
- } else if (char === ';' || char === '|' || (char === '&' && body[i + 1] === '&')) {
62
- if (char !== ';' && body[i + 1] === char) i += 1;
61
+ } else if (isCommandSeparator(body, i, char)) {
62
+ i += commandSeparatorAdvance(body, i, char);
63
63
  flush();
64
64
  } else if (char === '\\' && body[i + 1] === '\n') {
65
65
  i += 1;
@@ -71,43 +71,17 @@ export function shellCommands(body) {
71
71
  return commands;
72
72
  }
73
73
 
74
+ const isCommandSeparator = (body, index, char) => char === ';' || char === '|' || (char === '&' && body[index + 1] === '&');
75
+ const commandSeparatorAdvance = (body, index, char) => char !== ';' && body[index + 1] === char ? 1 : 0;
76
+
74
77
  /**
75
78
  * Parse a Dockerfile: { instructions: [{ keyword, text, line, stage }], stages: [{ index, name, image, line, instructions }],
76
79
  * comments: [{ text, line }], preamble: [instruction before the first FROM (ARG)] }. `keyword` is upper-case; `text` is the
77
80
  * argument text of the logical line; `stage` is the index of the stage the instruction belongs to (-1 before the first FROM).
78
81
  */
79
82
  export function parseDockerfile(source) {
80
- const lines = String(source).replace(/\r\n/g, '\n').split('\n');
81
- const instructions = [];
82
- const comments = [];
83
- let escape = '\\';
84
- let directives = true;
85
- for (let i = 0; i < lines.length; i += 1) {
86
- const raw = lines[i];
87
- const trimmed = raw.trim();
88
- if (trimmed === '') { directives = false; continue; }
89
- if (trimmed.startsWith('#')) {
90
- const body = trimmed.slice(1).trim();
91
- const directive = directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
92
- if (directive) { if (directive[1].toLowerCase() === 'escape') escape = directive[2]; continue; }
93
- directives = false;
94
- comments.push({ text: body, line: i + 1 });
95
- continue;
96
- }
97
- directives = false;
98
- const start = i + 1;
99
- let logical = trimmed;
100
- while (logical.endsWith(escape) && i + 1 < lines.length) {
101
- logical = logical.slice(0, -1).trimEnd();
102
- i += 1;
103
- while (i < lines.length && lines[i].trim().startsWith('#')) i += 1;
104
- if (i >= lines.length) break;
105
- logical = `${logical} ${lines[i].trim()}`;
106
- }
107
- const split = logical.search(/\s/);
108
- const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
109
- instructions.push({ keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: start, stage: -1 });
110
- }
83
+ const lines = String(source).replaceAll('\r\n', '\n').split('\n');
84
+ const { instructions, comments } = parseInstructions(lines);
111
85
  const stages = [];
112
86
  const preamble = [];
113
87
  for (const instruction of instructions) {
@@ -122,3 +96,52 @@ export function parseDockerfile(source) {
122
96
  }
123
97
  return { instructions, stages, comments, preamble };
124
98
  }
99
+
100
+ function parseInstructions(lines) {
101
+ const instructions = [];
102
+ const comments = [];
103
+ const state = { escape: '\\', directives: true };
104
+ let i = 0;
105
+ while (i < lines.length) {
106
+ const parsed = parseInstructionLine(lines, i, state);
107
+ if (parsed.comment) comments.push(parsed.comment);
108
+ if (parsed.instruction) instructions.push(parsed.instruction);
109
+ i = parsed.nextLine + 1;
110
+ }
111
+ return { instructions, comments };
112
+ }
113
+
114
+ function parseInstructionLine(lines, index, state) {
115
+ const trimmed = lines[index].trim();
116
+ if (trimmed === '') { state.directives = false; return { nextLine: index }; }
117
+ if (trimmed.startsWith('#')) return parseCommentLine(trimmed, index, state);
118
+ state.directives = false;
119
+ const { logical, nextLine } = logicalInstructionOf(lines, index, state.escape);
120
+ const split = logical.search(/\s/);
121
+ const keyword = (split < 0 ? logical : logical.slice(0, split)).toUpperCase();
122
+ return { nextLine, instruction: { keyword, text: split < 0 ? '' : logical.slice(split).trim(), line: index + 1, stage: -1 } };
123
+ }
124
+
125
+ function parseCommentLine(trimmed, index, state) {
126
+ const body = trimmed.slice(1).trim();
127
+ const directive = state.directives ? /^(syntax|escape|check)\s*=\s*(\S+)$/i.exec(body) : null;
128
+ if (directive) {
129
+ if (directive[1].toLowerCase() === 'escape') state.escape = directive[2];
130
+ return { nextLine: index };
131
+ }
132
+ state.directives = false;
133
+ return { nextLine: index, comment: { text: body, line: index + 1 } };
134
+ }
135
+
136
+ function logicalInstructionOf(lines, index, escape) {
137
+ let logical = lines[index].trim();
138
+ let nextLine = index;
139
+ while (logical.endsWith(escape) && nextLine + 1 < lines.length) {
140
+ logical = logical.slice(0, -1).trimEnd();
141
+ nextLine += 1;
142
+ while (nextLine < lines.length && lines[nextLine].trim().startsWith('#')) nextLine += 1;
143
+ if (nextLine >= lines.length) break;
144
+ logical = `${logical} ${lines[nextLine].trim()}`;
145
+ }
146
+ return { logical, nextLine };
147
+ }
@@ -7,13 +7,13 @@
7
7
  */
8
8
  export const unquoteDiffPath = (value) =>
9
9
  /^".*"$/.test(value)
10
- ? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => `\\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
10
+ ? JSON.parse(value.replace(/\\([0-7]{3})/g, (_, octal) => String.raw`\u00${Number.parseInt(octal, 8).toString(16).padStart(2, '0')}`))
11
11
  : value;
12
12
 
13
13
  // git's repository-local variables (git rev-parse --local-env-vars). A hook or alias run in a linked worktree exports
14
14
  // GIT_DIR=<main>/.git/worktrees/<wt>; a spec inheriting it pointed every fixture git at the LIVE .claude repo and re-inited
15
15
  // it core.bare=true (2026-09-29, a6f60352c). tests/setup/isolated-registry.mjs drops the same list.
16
- export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE'];
16
+ export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE', 'GIT_INTERNAL_SUPER_PREFIX'];
17
17
  /** `parent` (the caller's env) without git's repository-local variables (GIT_CONFIG_KEY_n/VALUE_n go with GIT_CONFIG_COUNT). Pure. */
18
18
  export function withoutGitLocalEnv(parent) {
19
19
  const env = { ...parent };
@@ -11,6 +11,7 @@
11
11
  // A placeholder is `{name}`; the English and the Vietnamese of one entry carry exactly the same placeholders
12
12
  // (scripts/checks/check-i18n-catalog.mjs). An English source with no entry is returned as is: a missing translation degrades
13
13
  // to English, never to a blank.
14
+ import { byCodeUnit } from './list.mjs';
14
15
  import { escapeRegExp } from './regex.mjs';
15
16
  import fs from 'node:fs';
16
17
  import path from 'node:path';
@@ -19,7 +20,7 @@ import { parseYaml } from '../../engine/yaml.mjs';
19
20
  import { DEFAULT_OWNER_LANGUAGE, loadConfig } from '../../engine/config.mjs';
20
21
 
21
22
  export const CATALOG_DIR = 'modules/i18n/messages';
22
- export const PLACEHOLDER = /\{([A-Za-z_][\w]*)\}/g;
23
+ export const PLACEHOLDER = /\{([A-Za-z_]\w*)\}/g;
23
24
 
24
25
  const cache = new Map();
25
26
 
@@ -54,7 +55,7 @@ export const resetCatalogCache = () => cache.clear();
54
55
  /** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
55
56
  export const captureNames = (text, pattern, { sort = false } = {}) => {
56
57
  const names = [...String(text).matchAll(pattern)].map((m) => m[1]);
57
- return sort ? names.sort() : names;
58
+ return sort ? names.sort(byCodeUnit) : names;
58
59
  };
59
60
 
60
61
  /** The names of the placeholders of a message. */
@@ -50,13 +50,13 @@ export const DECLARED_VIETNAMESE_FIELDS = Object.freeze({
50
50
  'modules/ops/_labels.yaml': Object.freeze({ flowFields: Object.freeze(['vi']) }),
51
51
  'modules/goal/archetypes.yaml': Object.freeze({
52
52
  fields: Object.freeze([
53
- 'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'integrationIntent', 'integrationNegation', 'brandIntent',
53
+ 'buildIntent', 'canonIntent', 'e2eIntent', 'uatIntent', 'proofNegation', 'proofStateCue', 'integrationIntent', 'brandIntent',
54
54
  'phrases', 'requires', 'excludes', 'backend', 'frontend', 'package',
55
55
  ]),
56
56
  }),
57
57
  'modules/goal/source-phrases.yaml': Object.freeze({
58
58
  fields: Object.freeze([
59
- 'prefer', 'avoid', 'negation', 'refused', 'accept', 'golden',
59
+ 'prefer', 'avoid', 'only', 'negation', 'refused', 'accept', 'golden',
60
60
  'missingPaths', 'grantTooNarrow', 'toolTimeout', 'testGap', 'checkerUnavailable',
61
61
  'supervisorVerbs', 'ownerWord', 'orWord', 'beyondAuthority', 'ownerOnly', 'workerDied', 'contractConflict', 'decision',
62
62
  'product', 'grammar', 'knowledge', 'sourceLabel', 'internalVocabulary', 'actor', 'success',
@@ -124,7 +124,7 @@ export function documentLanguageHits(rel, text) {
124
124
  if (declared.fields?.includes(keys[hit.line - 1])) return false;
125
125
  if (!declared.flowFields) return true;
126
126
  // an inline flow map: drop the declared fields' values and judge what is left of the line
127
- const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(`\\b${field}\\s*:\\s*(?:"(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
127
+ const rest = declared.flowFields.reduce((line, field) => line.replace(new RegExp(String.raw`\b${field}\s*:\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,}]*)`, 'g'), ''), lines[hit.line - 1]);
128
128
  return hasSecondLanguage(rest);
129
129
  });
130
130
  }
@@ -4,7 +4,11 @@
4
4
  export const list = (v) => (Array.isArray(v) ? v : []);
5
5
 
6
6
  /** `v` as an array: itself when already one, [] for null/undefined, else [v]. */
7
- export const asList = (v) => (Array.isArray(v) ? v : v == null ? [] : [v]);
7
+ export const asList = (v) => {
8
+ if (Array.isArray(v)) return v;
9
+ if (v == null) return [];
10
+ return [v];
11
+ };
8
12
 
9
13
  /** The object entries of `v` (a non-array reads as []). */
10
14
  export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => item && typeof item === 'object');
@@ -18,3 +22,6 @@ export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
18
22
  const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
19
23
  return dedupe ? [...new Set(items)] : items;
20
24
  };
25
+
26
+ // The comparator of a bare `sort()` (UTF-16 code-unit order) is owned by the engine, which cannot import scripts/lib.
27
+ export { byCodeUnit } from '../../engine/by-code-unit.mjs';
@@ -6,6 +6,17 @@ const WIN = process.platform === 'win32';
6
6
 
7
7
  /** `p` with every backslash turned into a forward slash; null and undefined read as ''. */
8
8
  export const slash = (p) => String(p ?? '').replaceAll('\\', '/');
9
+ /** `text` without its trailing '/' run (a loop: a trailing-run pattern backtracks super-linearly). */
10
+ export const trimTrailingSlashes = (text) => {
11
+ for (let end = text.length; ; end -= 1) if (end === 0 || text[end - 1] !== '/') return text.slice(0, end);
12
+ };
13
+ // A trailing '*' run with the one '/' before it (the glob suffix of 'dir/**'); text without a trailing '*' is unchanged.
14
+ const stripTrailingStars = (text) => {
15
+ let end = text.length;
16
+ while (end > 0 && text[end - 1] === '*') end -= 1;
17
+ if (end === text.length) return text;
18
+ return text.slice(0, end > 0 && text[end - 1] === '/' ? end - 1 : end);
19
+ };
9
20
  /** A relative path in git's spelling: forward slashes, no leading './'. */
10
21
  export const posixPath = (p) => slash(p).replace(/^\.\//, '');
11
22
  /** Whether posix `file` is `prefix` itself or under it (`file === prefix || file` starts with `prefix/`). */
@@ -19,7 +30,7 @@ export const samePath = (a, b) => foldCase(a) === foldCase(b);
19
30
  * `fold: true` folds case on every platform (a comparison against a lowercase-stored root needs it).
20
31
  */
21
32
  export const pathKey = (p, { fold = WIN } = {}) => {
22
- const key = slash(path.resolve(p)).replace(/\/+$/, '');
33
+ const key = trimTrailingSlashes(slash(path.resolve(p)));
23
34
  return fold ? key.toLowerCase() : key;
24
35
  };
25
36
  /** `a` is `b` or under it, or the reverse: two path spellings whose trees touch. */
@@ -36,19 +47,28 @@ export const normPath = (p, { trim = false, collapse = false, dot = false, glob
36
47
  s = slash(s);
37
48
  if (collapse) s = s.replace(/\/{2,}/g, '/');
38
49
  if (dot) s = s.replace(/^\.\//, '');
39
- if (glob === 'star') s = s.replace(/\/?\*+$/, '');
50
+ if (glob === 'star') s = stripTrailingStars(s);
40
51
  else if (glob === 'double') s = s.replace(/\/\*\*$/, '');
41
- s = s.replace(/\/+$/, '');
52
+ s = trimTrailingSlashes(s);
42
53
  return fold ? s.toLowerCase() : s;
43
54
  };
44
55
  /** A work-relative path in one spelling: forward slashes, no leading ./, no trailing slash. */
45
- export const normWork = (p) => String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
56
+ export const normWork = (p) => trimTrailingSlashes(String(p ?? '').trim().replaceAll('\\', '/').replace(/^\.\/+/, ''));
46
57
 
47
58
  /** `p` resolved and dereferenced (realpath where the path exists) — the canonical spelling of a checkout dir. */
48
59
  export const realPath = (p) => {
49
60
  const resolved = path.resolve(p);
50
61
  try { return fs.realpathSync.native(resolved); } catch { return resolved; }
51
62
  };
63
+ /** `p` with its nearest existing ancestor dereferenced and the not-yet-created rest appended: a root's canonical spelling before it exists. */
64
+ export const canonicalPath = (p) => {
65
+ const rest = [];
66
+ for (let at = path.resolve(p); ; at = path.dirname(at)) {
67
+ try { return path.join(fs.realpathSync.native(at), ...rest); } catch { /* not created yet */ }
68
+ if (path.dirname(at) === at) return path.resolve(p);
69
+ rest.unshift(path.basename(at));
70
+ }
71
+ };
52
72
  /** `p` resolved and case-folded where the filesystem ignores case — the Map key of an absolute path. */
53
73
  export const resolvedKey = (p) => foldCase(path.resolve(p));
54
74
  /** Whether `a` and `b` resolve to the same path (always case-folded, so differently-cased spellings count). */
@@ -61,12 +81,25 @@ export const insidePath = (root, file, { key = (p) => p, includeSelf = false } =
61
81
  const rel = path.relative(key(root), key(file));
62
82
  return (includeSelf || Boolean(rel)) && !rel.startsWith('..') && !path.isAbsolute(rel);
63
83
  };
84
+ /**
85
+ * `candidate` (absolute, or relative to `base`) resolved and dereferenced, required to sit under `base` (or be it):
86
+ * the boundary check for a path an operator, an agent or another process hands in. `base` is canonicalised once and the
87
+ * candidate is judged below it, so `..` segments and links leading out are refused; returns the canonical path, or throws
88
+ * a PATH_OUTSIDE_BASE refusal (`label` names the argument in the message).
89
+ */
90
+ export const containedPath = (base, candidate, { label = 'path' } = {}) => {
91
+ const root = canonicalPath(base);
92
+ const resolved = canonicalPath(path.resolve(root, String(candidate ?? '')));
93
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
94
+ if (!insidePath(root, resolved, { includeSelf: true }) || (resolved !== root && !resolved.startsWith(prefix))) throw Object.assign(new Error(`${label} ${String(candidate)} is outside ${root} [PATH_OUTSIDE_BASE]`), { code: 'PATH_OUTSIDE_BASE' });
95
+ return resolved;
96
+ };
64
97
  /**
65
98
  * A path-ish value (a string or a {path} entry) in relative compare form: trimmed, forward slashes,
66
99
  * no trailing '/**' glob or slashes, no leading './'; `fold` lowercases for a case-insensitive key.
67
100
  */
68
101
  export const normRel = (p, { fold = false } = {}) => {
69
- const s = String(typeof p === 'string' ? p : p?.path ?? '').trim().replace(/\\/g, '/').replace(/\/\*\*$/, '').replace(/\/+$/, '').replace(/^\.\//, '');
102
+ const s = trimTrailingSlashes(String(typeof p === 'string' ? p : p?.path ?? '').trim().replaceAll('\\', '/').replace(/\/\*\*$/, '')).replace(/^\.\//, '');
70
103
  return fold ? s.toLowerCase() : s;
71
104
  };
72
105
  /** Whether `file` equals or sits under one of `prefixes` (posix spellings; `dot` counts a '.' prefix as covering all). */
@@ -1,4 +1,4 @@
1
1
  // regex.mjs - building a RegExp from text.
2
2
 
3
3
  /** `text` with every regular-expression metacharacter escaped, so it matches itself literally. */
4
- export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4
+ export const escapeRegExp = (text) => String(text).replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
@@ -15,7 +15,7 @@ export const FORBIDDEN_FILES = [
15
15
  export const SECRET_PATTERNS = [
16
16
  { name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
17
17
  { name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
18
- { name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_[A-Za-z0-9_]{50,}\b/ },
18
+ { name: 'github-token', re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,}\b|\bgithub_pat_\w{50,}\b/ },
19
19
  { name: 'anthropic-key', re: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
20
20
  { name: 'openai-key', re: /\bsk-(?:proj-)?[A-Za-z0-9]{32,}\b/ },
21
21
  { name: 'slack-token', re: /\bxox[baprs]-[A-Za-z0-9-]{10,}/ },
@@ -28,7 +28,11 @@ export const SECRET_PATTERNS = [
28
28
  // candidate; only this heuristic takes the exemption, never a provider-shaped token above.
29
29
  // A spec file's keyword-assigned values are test inputs (a spec's sign-in
30
30
  // form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
31
- { name: 'assigned-secret', re: /\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})['"]/i, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
31
+ { name: 'assigned-secret', re: new RegExp([
32
+ String.raw`\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|client[_-]?secret)`,
33
+ String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
34
+ `['"]`,
35
+ ].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
32
36
  skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
33
37
  ];
34
38
 
@@ -0,0 +1,225 @@
1
+ // sonar-syntax-rules.mjs - the ESLint rules that keep a TypeScript app clear of the smells SonarCloud flagged in the example apps, judged on
2
+ // the syntax tree alone (no type information, no dependency). Both lint canons (@starci/eslint-canon-be and -fe) publish them from their
3
+ // bundled runtime copy under the names below, so a product inherits them and `starci app lint` refuses the finding before the scan sees it.
4
+ // Each message names the Sonar rule it stands for, and a rule judges the files the example scans judge: a unit or end-to-end spec is outside the
5
+ // scan (`sonar.exclusions`), so it is outside these rules. The rules that need the type checker (S1874, S6551, S7503) are borrowed from
6
+ // typescript-eslint by the canon factories; the one that needs a regex ambiguity analysis (S8786) runs in the runtime's own `sonar-rules` check.
7
+
8
+ const FUNCTIONS = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression']);
9
+ const REPEATED = Object.freeze({
10
+ ForStatement: ['body', 'test', 'update'],
11
+ ForInStatement: ['body'],
12
+ ForOfStatement: ['body'],
13
+ WhileStatement: ['body', 'test'],
14
+ DoWhileStatement: ['body', 'test'],
15
+ });
16
+
17
+ /** The first ancestor of `node` that is `type`-matching per `test`, stopping at a function or class boundary. */
18
+ function ancestorWithin(node, test) {
19
+ for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) {
20
+ if (FUNCTIONS.has(parent.type) || parent.type === 'StaticBlock') return null;
21
+ if (test(parent, child)) return parent;
22
+ }
23
+ return null;
24
+ }
25
+
26
+ const repeatsChild = (loop, child) => (REPEATED[loop.type] ?? []).some((key) => loop[key] === child);
27
+
28
+ /** A loop with no condition (`for (;;)`, `while (true)`) walks pages or polls: each turn needs the one before, so it cannot be run together. */
29
+ const isEndless = (loop) => (loop.type === 'ForStatement' && loop.test === null) || (['WhileStatement', 'DoWhileStatement'].includes(loop.type) && loop.test.type === 'Literal' && loop.test.value === true);
30
+
31
+ /** S9382: an `await` inside a loop body runs one step after the other; say the sequence by name or run the independent steps together. */
32
+ const noAwaitInLoop = {
33
+ meta: {
34
+ type: 'suggestion',
35
+ schema: [],
36
+ messages: { loop: 'S9382: do not `await` inside a loop. Run independent steps together with `Promise.all`, or state the sequence by name with a helper that chains the promises (`eachInOrder` of the back end\'s `platform/primitives`).' },
37
+ },
38
+ create: (context) => ({
39
+ AwaitExpression: (node) => {
40
+ if (ancestorWithin(node, (loop, child) => repeatsChild(loop, child) && !isEndless(loop))) context.report({ node, messageId: 'loop' });
41
+ },
42
+ }),
43
+ };
44
+
45
+ /** S7758: `codePointAt` and `fromCodePoint` handle every code point; `charCodeAt` and `fromCharCode` stop at 16 bits. */
46
+ const preferCodePoint = {
47
+ meta: { type: 'suggestion', schema: [], messages: { codePoint: 'S7758: use `{{ better }}` instead of `{{ worse }}`; it reads whole code points, not 16-bit halves.' } },
48
+ create: (context) => ({
49
+ CallExpression: (node) => {
50
+ const callee = node.callee;
51
+ if (callee.type !== 'MemberExpression' || callee.computed || callee.property.type !== 'Identifier') return;
52
+ const name = callee.property.name;
53
+ if (name === 'charCodeAt') context.report({ node, messageId: 'codePoint', data: { better: 'codePointAt', worse: 'charCodeAt' } });
54
+ else if (name === 'fromCharCode' && callee.object.type === 'Identifier' && callee.object.name === 'String') context.report({ node, messageId: 'codePoint', data: { better: 'String.fromCodePoint', worse: 'String.fromCharCode' } });
55
+ },
56
+ }),
57
+ };
58
+
59
+ const BACKSLASH_PAIR = '\\\\';
60
+
61
+ /** Whether the string literal only escapes backslashes: its value is its raw text with each pair read as one. */
62
+ function onlyBackslashEscapes(node) {
63
+ const raw = node.raw;
64
+ if (typeof node.value !== 'string' || !raw.includes(BACKSLASH_PAIR) || raw.at(-2) === '\\') return false;
65
+ if (node.value.includes('`') || node.value.includes('${')) return false;
66
+ return raw.slice(1, -1).replaceAll(BACKSLASH_PAIR, '\\') === node.value;
67
+ }
68
+
69
+ const KEEPS_ITS_ESCAPES = new Set(['ImportDeclaration', 'ExportAllDeclaration', 'ExportNamedDeclaration', 'TSLiteralType', 'TSExternalModuleReference', 'JSXAttribute']);
70
+
71
+ /** A property named `matcher` is read statically by Next (a template literal there kills the build), so its text keeps the escape. */
72
+ const isMatcherValue = (node) => (node.parent?.type === 'Property' && node.parent.value === node && node.parent.key?.name === 'matcher')
73
+ || (node.parent?.type === 'ArrayExpression' && isMatcherValue(node.parent));
74
+
75
+ /** S7780: a string whose only escapes are backslashes reads better as `String.raw`. */
76
+ const preferStringRaw = {
77
+ meta: { type: 'suggestion', schema: [], messages: { raw: 'S7780: write this string with `String.raw` so its backslashes are not escaped.' } },
78
+ create: (context) => ({
79
+ Literal: (node) => {
80
+ if (KEEPS_ITS_ESCAPES.has(node.parent?.type) || node.parent?.type === 'ExpressionStatement' || isMatcherValue(node)) return;
81
+ if (onlyBackslashEscapes(node)) context.report({ node, messageId: 'raw' });
82
+ },
83
+ }),
84
+ };
85
+
86
+ /** S3358: a conditional expression inside another conditional expression is read inside out; name the inner one. */
87
+ const noNestedConditional = {
88
+ meta: { type: 'suggestion', schema: [], messages: { nested: 'S3358: extract this nested conditional expression into its own statement or a named function.' } },
89
+ create: (context) => ({
90
+ ConditionalExpression: (node) => {
91
+ if (node.parent?.type === 'ConditionalExpression') context.report({ node, messageId: 'nested' });
92
+ },
93
+ }),
94
+ };
95
+
96
+ /** Whether `void` is used on a call: the explicit discard of a promise that Sonar and no-floating-promises both accept. */
97
+ const discardsACall = (argument) => argument.type === 'CallExpression' || argument.type === 'AwaitExpression' || (argument.type === 'ChainExpression' && argument.expression.type === 'CallExpression');
98
+
99
+ /** S3735: `void` on anything but a call hides what the code does; `void principal` says nothing, `undefined` or no statement says it. */
100
+ const noVoidOperator = {
101
+ meta: { type: 'suggestion', schema: [], messages: { void: 'S3735: remove this `void`; it discards a value the code never needed. Only `void call()` (an explicit discard of a promise) is kept.' } },
102
+ create: (context) => ({
103
+ UnaryExpression: (node) => {
104
+ if (node.operator === 'void' && !discardsACall(node.argument)) context.report({ node, messageId: 'void' });
105
+ },
106
+ }),
107
+ };
108
+
109
+ /** S1128: an import no reference reads is removed. */
110
+ const noUnusedImport = {
111
+ meta: { type: 'suggestion', schema: [], messages: { unused: 'S1128: remove the unused import of `{{ name }}`.' } },
112
+ create: (context) => ({
113
+ ImportDeclaration: (node) => {
114
+ for (const variable of context.sourceCode.getDeclaredVariables(node)) {
115
+ if (variable.references.length === 0) context.report({ node: variable.identifiers[0] ?? node, messageId: 'unused', data: { name: variable.name } });
116
+ }
117
+ },
118
+ }),
119
+ };
120
+
121
+ /** Whether `identifier` names an import of the file that nothing else reads. */
122
+ function onlyReExported(sourceCode, identifier) {
123
+ let scope = sourceCode.getScope(identifier);
124
+ while (scope?.set.has(identifier.name) === false) scope = scope.upper;
125
+ const variable = scope?.set.get(identifier.name);
126
+ const definition = variable?.defs[0];
127
+ return definition?.type === 'ImportBinding' && variable.references.length === 1;
128
+ }
129
+
130
+ /** S7763: an import that is only handed on is re-exported with `export ... from`. */
131
+ const preferExportFrom = {
132
+ meta: { type: 'suggestion', schema: [], messages: { reexport: 'S7763: use `export ... from` to re-export `{{ name }}` instead of importing it first.' } },
133
+ create: (context) => {
134
+ const { sourceCode } = context;
135
+ const check = (identifier, node) => {
136
+ if (identifier?.type === 'Identifier' && onlyReExported(sourceCode, identifier)) context.report({ node, messageId: 'reexport', data: { name: identifier.name } });
137
+ };
138
+ return {
139
+ ExportNamedDeclaration: (node) => {
140
+ if (node.source) return;
141
+ for (const specifier of node.specifiers) check(specifier.local, specifier);
142
+ for (const declarator of node.declaration?.declarations ?? []) if (!declarator.id.typeAnnotation) check(declarator.init, declarator);
143
+ },
144
+ ExportDefaultDeclaration: (node) => check(node.declaration, node),
145
+ };
146
+ },
147
+ };
148
+
149
+ /** The members a props type declares: [{name, node}] from a type literal, an interface body, `Readonly<...>` or an intersection of those declared in the file. */
150
+ function declaredMembers(program, type) {
151
+ if (!type) return [];
152
+ if (type.type === 'TSTypeLiteral') return type.members.filter((member) => !member.computed && member.key).map((member) => ({ name: member.key.name ?? member.key.value, node: member.key }));
153
+ if (type.type === 'TSIntersectionType') return type.types.flatMap((part) => declaredMembers(program, part));
154
+ if (type.type !== 'TSTypeReference' || type.typeName.type !== 'Identifier') return [];
155
+ if (type.typeName.name === 'Readonly') return declaredMembers(program, type.typeArguments?.params?.[0]);
156
+ const declaration = program.body.map((statement) => statement.declaration ?? statement).find((statement) => (statement.type === 'TSTypeAliasDeclaration' || statement.type === 'TSInterfaceDeclaration') && statement.id.name === type.typeName.name);
157
+ if (declaration?.type === 'TSInterfaceDeclaration') return declaredMembers(program, { type: 'TSTypeLiteral', members: declaration.body.body });
158
+ return declaredMembers(program, declaration?.typeAnnotation);
159
+ }
160
+
161
+ const keyName = (key) => (key.type === 'Identifier' ? key.name : key.value);
162
+
163
+ /** The prop names a use of the parameter variable reads, or null when it escapes whole (passed on, spread, indexed dynamically). */
164
+ function readNames(variable) {
165
+ const names = new Set();
166
+ for (const reference of variable.references) {
167
+ const parent = reference.identifier.parent;
168
+ if (parent.type === 'MemberExpression' && parent.object === reference.identifier && (!parent.computed || parent.property.type === 'Literal')) names.add(keyName(parent.property));
169
+ else if (parent.type === 'VariableDeclarator' && parent.init === reference.identifier && parent.id.type === 'ObjectPattern' && parent.id.properties.every((property) => property.type === 'Property' && !property.computed)) parent.id.properties.forEach((property) => names.add(keyName(property.key)));
170
+ else return null;
171
+ }
172
+ return names;
173
+ }
174
+
175
+ /** The names a parameter reads: an object pattern's own keys, or what its variable's uses read; null means "all of them". */
176
+ function readByParameter(sourceCode, fn, parameter) {
177
+ if (parameter.type === 'ObjectPattern') {
178
+ if (parameter.properties.some((property) => property.type !== 'Property' || property.computed)) return null;
179
+ return new Set(parameter.properties.map((property) => keyName(property.key)));
180
+ }
181
+ if (parameter.type !== 'Identifier') return null;
182
+ const variable = sourceCode.getDeclaredVariables(fn).find((candidate) => candidate.name === parameter.name);
183
+ return variable ? readNames(variable) : null;
184
+ }
185
+
186
+ /** S6767: a member of a component's props type that the component never reads is removed from the type. */
187
+ const noUnusedPropTypes = {
188
+ meta: { type: 'suggestion', schema: [], messages: { unused: 'S6767: the prop `{{ name }}` is declared in the props type but the component never reads it; remove it.' } },
189
+ create: (context) => {
190
+ const { sourceCode } = context;
191
+ const frames = [];
192
+ const enter = (node) => frames.push({ node, jsx: false });
193
+ const markJsx = () => frames.forEach((frame) => { frame.jsx = true; });
194
+ const exit = () => {
195
+ const { node, jsx } = frames.pop();
196
+ const parameter = node.params[0];
197
+ const annotation = parameter?.typeAnnotation?.typeAnnotation;
198
+ if (!jsx || !annotation) return;
199
+ const read = readByParameter(sourceCode, node, parameter);
200
+ if (read === null) return;
201
+ for (const member of declaredMembers(sourceCode.ast, annotation)) {
202
+ if (!read.has(member.name)) context.report({ node: member.node, messageId: 'unused', data: { name: member.name } });
203
+ }
204
+ };
205
+ return { FunctionDeclaration: enter, FunctionExpression: enter, ArrowFunctionExpression: enter, 'FunctionDeclaration:exit': exit, 'FunctionExpression:exit': exit, 'ArrowFunctionExpression:exit': exit, JSXElement: markJsx, JSXFragment: markJsx };
206
+ },
207
+ };
208
+
209
+ /** The files the example scans leave out: a unit spec and an end-to-end spec. */
210
+ const OUTSIDE_THE_SCAN = /\.(?:e2e-)?spec\.[cm]?[jt]sx?$/;
211
+
212
+ /** The rule judging only the files the scan judges. */
213
+ const scanned = (rule) => ({ ...rule, create: (context) => (OUTSIDE_THE_SCAN.test(context.filename) ? {} : rule.create(context)) });
214
+
215
+ /** The rules both canons publish, by published name. */
216
+ export const SONAR_SYNTAX_RULES = Object.freeze({
217
+ 'no-await-in-loop': scanned(noAwaitInLoop),
218
+ 'prefer-code-point': scanned(preferCodePoint),
219
+ 'prefer-string-raw': scanned(preferStringRaw),
220
+ 'no-nested-conditional': scanned(noNestedConditional),
221
+ 'no-void-operator': scanned(noVoidOperator),
222
+ 'no-unused-import': scanned(noUnusedImport),
223
+ 'prefer-export-from': scanned(preferExportFrom),
224
+ 'no-unused-prop-types': scanned(noUnusedPropTypes),
225
+ });