@ethisyscore/eslint-plugin-coreconnect 1.71.5 → 1.72.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethisyscore/eslint-plugin-coreconnect",
3
- "version": "1.71.5",
3
+ "version": "1.72.0",
4
4
  "description": "ESLint rules enforcing EthisysCore plugin frontend conventions. Published so a new rule reaches every plugin on a version bump, rather than being copied into each scaffold and drifting.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -9,6 +9,18 @@
9
9
  */
10
10
  const COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}(?:[0-9a-fA-F]{2})?$|^rgba?\(|^hsla?\(/;
11
11
 
12
+ /**
13
+ * A colour function whose channels come from CSS custom properties - `hsl(var(--border))`,
14
+ * `rgb(var(--cc-accent) / 0.5)`.
15
+ *
16
+ * This is not a hardcoded colour, it is the exact thing the rule's own message asks for, and
17
+ * flagging it was a false positive: `COLOR_REGEX` matches on the `hsl(` prefix alone and never
18
+ * looked inside. Measured on `finance`, whose `src/styles/tailwind.config.ts` is written entirely in
19
+ * this form - 48 findings, every one of them already themed, which is enough noise to teach a reader
20
+ * that the rule is wrong rather than that the file is.
21
+ */
22
+ const TOKENISED_COLOR_REGEX = /^(?:rgba?|hsla?)\(\s*var\(\s*--/;
23
+
12
24
  export default {
13
25
  meta: {
14
26
  type: "suggestion",
@@ -28,7 +40,15 @@ export default {
28
40
  if (typeof node.value !== "string") {
29
41
  return;
30
42
  }
31
- if (COLOR_REGEX.test(node.value.trim())) {
43
+ const value = node.value.trim();
44
+
45
+ // Order matters: a tokenised colour matches COLOR_REGEX too, so the exemption has to be
46
+ // checked first.
47
+ if (TOKENISED_COLOR_REGEX.test(value)) {
48
+ return;
49
+ }
50
+
51
+ if (COLOR_REGEX.test(value)) {
32
52
  context.report({ node, messageId: "noHardcodedColor", data: { value: node.value } });
33
53
  }
34
54
  },
@@ -13,6 +13,8 @@
13
13
  * @type {import("eslint").Rule.RuleModule}
14
14
  */
15
15
 
16
+ import { isTestFile } from "./testFiles.js";
17
+
16
18
  const FUNCTION_LIKE = new Set([
17
19
  "FunctionDeclaration",
18
20
  "FunctionExpression",
@@ -96,6 +98,11 @@ export default {
96
98
  schema: [],
97
99
  },
98
100
  create(context) {
101
+ // Test scaffolding is exempt: see isTestFile for why this rule's rationale does not reach it.
102
+ if (isTestFile(context.filename ?? context.getFilename?.())) {
103
+ return {};
104
+ }
105
+
99
106
  return {
100
107
  TSTypeLiteral(node) {
101
108
  const position = positionOf(node);
@@ -12,6 +12,8 @@
12
12
  * @type {import("eslint").Rule.RuleModule}
13
13
  */
14
14
 
15
+ import { isTestFile } from "./testFiles.js";
16
+
15
17
  const MINIMUM_MEMBERS = 3;
16
18
 
17
19
  /** A `"literal"` type node, across the parser versions that spell it differently. */
@@ -37,6 +39,11 @@ export default {
37
39
  schema: [],
38
40
  },
39
41
  create(context) {
42
+ // Test scaffolding is exempt: see isTestFile for why this rule's rationale does not reach it.
43
+ if (isTestFile(context.filename ?? context.getFilename?.())) {
44
+ return {};
45
+ }
46
+
40
47
  return {
41
48
  TSUnionType(node) {
42
49
  const members = node.types ?? [];
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Whether a file is test scaffolding rather than shipped code.
3
+ *
4
+ * Used by the two STRUCTURAL rules - `no-inline-object-type` and `no-inline-string-union` - and by
5
+ * nothing else. Both exist to stop an unnamed shape being re-written slightly differently by the
6
+ * next person who needs it, and that argument does not reach a test file: the shape belongs to one
7
+ * mock in one file, nobody imports it, and it is deleted when the test is.
8
+ *
9
+ * Measured before adding this: 80 of the estate's convention findings sit in test files, 74 of them
10
+ * `no-inline-object-type`, and almost all are the prop type of a hand-rolled MUI stub -
11
+ * `Alert: ({ children, action }: { children?: ReactNode; action?: ReactNode }) => ...`. Naming those
12
+ * 74 would add 74 aliases that exist only to satisfy a rule, in files where the rule's own rationale
13
+ * does not apply. A rule that demands ceremony gets switched off, which costs the 679 findings in
14
+ * shipped code that it is genuinely right about.
15
+ *
16
+ * ## What counts, and why a basename never can
17
+ *
18
+ * Only the three markers below, all of which say "this file exists to test something":
19
+ * a `__tests__/` or `__mocks__/` directory, or a `.test.` / `.spec.` suffix. They are conventions a
20
+ * bundler, a test runner and a reader all already agree on.
21
+ *
22
+ * A fourth marker was tried and WITHDRAWN: a basename match on `Mock[A-Z]*`, meant to catch "a
23
+ * hand-rolled stub kept beside the code it doubles rather than under `__tests__`". It was wrong,
24
+ * and wrong in the direction that costs most - it exempted SHIPPED code.
25
+ *
26
+ * Every plugin in this estate has `src/services/implementations/mock/Mock<Domain>Service.ts`, and
27
+ * those are not scaffolding. `useProjectService.ts` imports `MockProjectService` statically and
28
+ * picks between it and the real service at runtime on `IS_MOCK_MODE`, so the class is in the
29
+ * production bundle and its shapes are read by the same hooks that read the real service's. The
30
+ * rule's rationale reaches them exactly as it reaches any other source file.
31
+ *
32
+ * Measured on 2026-08-29 across the eight lint-adopting plugins: the basename branch was the ONLY
33
+ * thing exempting 17 findings, and every one of them was in a shipped `Mock*Service.ts` under a
34
+ * `mock/` directory. Nothing legitimate depended on it - the hand-rolled MUI stubs the exemption
35
+ * was written for all sit under `__tests__/` or carry a `.test.` suffix, and stay exempt.
36
+ *
37
+ * Two lessons worth keeping: a heuristic on a NAME cannot tell a test double from a shipped
38
+ * implementation of the same interface, because both are named after what they double; and an
39
+ * exemption that silently widens is worse than a finding, because nothing reports it.
40
+ *
41
+ * ## Why the check lives here
42
+ *
43
+ * The glob-shaped alternative - a `files` override in `configs.recommended` - would require that
44
+ * config to become an array, and every consumer spreads it as an object
45
+ * (`{ files: [...], ...coreconnect.configs.recommended }`). Changing that shape would break nine
46
+ * repos at once, so the check lives in the rules, which is also how this package delivers everything
47
+ * else: on a version bump, with no consumer edit.
48
+ *
49
+ * Deliberately NOT applied to `no-hardcoded-colors` or to any of the `no-local-*` rules. A hardcoded
50
+ * colour is trivially fixable anywhere and there is exactly one in a test file estate-wide; and a
51
+ * test file that re-declares the shared data grid is exactly the drift those rules exist to catch.
52
+ *
53
+ * @param {string} filename Absolute or relative path, as ESLint reports it.
54
+ * @returns {boolean}
55
+ */
56
+ export function isTestFile(filename) {
57
+ if (!filename) {
58
+ return false;
59
+ }
60
+
61
+ // Normalise Windows separators: ESLint reports native paths, and the estate is developed on both.
62
+ const path = filename.replace(/\\/g, "/");
63
+
64
+ return (
65
+ /(^|\/)__tests__\//.test(path) ||
66
+ /(^|\/)__mocks__\//.test(path) ||
67
+ /\.(test|spec)\.[cm]?[jt]sx?$/.test(path)
68
+ );
69
+ }