@hublo/sentinel 1.5.0-alpha.8 → 1.5.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 (41) hide show
  1. package/README.md +9 -7
  2. package/dist/bin/sentinel.js +61 -16
  3. package/dist/{chunk-KLEXJFJ6.js → chunk-43HQ6BVJ.js} +61 -15
  4. package/dist/{chunk-YQRDKWSH.js → chunk-IL5PPLXE.js} +11 -1
  5. package/dist/chunk-IL5PPLXE.js.map +1 -0
  6. package/dist/chunk-MOELAQPC.js +10365 -0
  7. package/dist/chunk-WBDBRGLV.js +214 -0
  8. package/dist/chunk-WBDBRGLV.js.map +1 -0
  9. package/dist/{chunk-AMCGMLPS.js → chunk-Y57KA4QS.js} +2 -2
  10. package/dist/index.d.ts +28 -1
  11. package/dist/index.js +4 -4
  12. package/dist/roles/build/nest/toolchain.js +68 -18
  13. package/dist/roles/build/nest/toolchain.js.map +1 -1
  14. package/dist/roles/static-analysis/config.d.ts +15 -1
  15. package/dist/roles/static-analysis/config.js +1 -1
  16. package/dist/roles/test/setup/jest-parity.js +2 -2
  17. package/dist/roles/test/setup/workspace-entry.js +2 -2
  18. package/dist/{validate-AWWMBSH5.js → validate-FRMENKHM.js} +2 -2
  19. package/docs/bundle-analysis-spec.md +272 -0
  20. package/docs/sql-verification-study.md +401 -0
  21. package/docs/static-analysis-adoption.md +285 -0
  22. package/docs/static-analysis-study.md +824 -18
  23. package/docs/typescript-adoption.md +1 -1
  24. package/docs/typescript-traces.md +27 -12
  25. package/docs/using-sentinel.md +22 -12
  26. package/docs/validating-a-change.md +185 -0
  27. package/lint/nest.json +98 -18
  28. package/lint/node.json +83 -3
  29. package/lint/react-lib.json +121 -31
  30. package/lint/react.json +101 -2
  31. package/package.json +27 -6
  32. package/plugins/hublo/no-palette-common.js +76 -0
  33. package/plugins/hublo/no-regex-lookbehind.js +70 -0
  34. package/plugins/hublo/require-reload-document.js +70 -0
  35. package/plugins/hublo.js +20 -145
  36. package/plugins/sonar-cognitive.js +108 -0
  37. package/dist/chunk-DKLQYGPI.js +0 -6466
  38. package/dist/chunk-EDFIVLUA.js +0 -100
  39. package/dist/chunk-EDFIVLUA.js.map +0 -1
  40. package/dist/chunk-YQRDKWSH.js.map +0 -1
  41. /package/dist/{chunk-AMCGMLPS.js.map → chunk-Y57KA4QS.js.map} +0 -0
package/plugins/hublo.js CHANGED
@@ -1,156 +1,31 @@
1
1
  /**
2
2
  * Local oxlint plugin: Hublo house rules that have no equivalent anywhere else.
3
3
  *
4
- * oxlint has no `no-restricted-syntax`, so the eight AST selectors that used to live in
5
- * `restricted-syntax.cjs` cannot be carried across as configuration. They are expressed
6
- * here as a real rule instead, which is what oxlint's JS plugin API is for.
4
+ * It ships INSIDE sentinel rather than being copied into each module. A house rule is code, not
5
+ * configuration: a per-module copy drifts, and drifting house rules are the problem sentinel
6
+ * exists to remove. The cost is that `@hublo/sentinel` carries a rule about Hublo's colour
7
+ * palette, which is honest for a package named after the company.
7
8
  *
8
- * It ships INSIDE sentinel rather than being copied into each module. A house rule is code,
9
- * not configuration: a per-module copy drifts, and drifting house rules are the problem
10
- * sentinel exists to remove. The cost is that `@hublo/sentinel` carries a rule about Hublo's
11
- * colour palette, which is honest for a package named after the company.
9
+ * ⛔ One file per rule, assembled here, and this file holds NO rule logic. It was one 220-line
10
+ * object with three rules inside it, which is the shape that invites a fourth: the rules share
11
+ * nothing, each carries its own constants and its own reasoning, and a reader looking for one of
12
+ * them had to scroll past the other two. Composition is the point here rather than speed: this
13
+ * plugin has no dependencies and loads in about 1 ms, so there is no sonarjs-style cost to cut.
12
14
  *
13
- * It now hosts a SECOND, unrelated rule. `no-regex-lookbehind` is not a carried-over selector
14
- * but a browser-support rule: regex lookbehind is Safari 16.4+, and unlike everything else a
15
- * preset controls it can be neither transpiled nor polyfilled, because no bundler rewrites the
16
- * contents of a regex literal. It is here for the same reason as the palette rule — a house
17
- * rule is code, not configuration — and because lint is the only layer left that can catch it.
18
- *
19
- * The original selectors, and where each is handled below:
20
- * 1-2 MemberExpression[object.property.name='palette'][property.name='common'] theme.palette.common
21
- * …[computed=true][property.value='common'] theme.palette['common']
22
- * 3-4 MemberExpression[object.name='palette']… palette.common
23
- * 5-6 Property[key.name|key.value=/<colour props>/] > Literal[value=/^common\./] { color: 'common.white' }
24
- * 7-8 JSXAttribute[name.name=/<colour props>/] > Literal | JSXExpressionContainer color="common.white"
25
- */
26
-
27
- const MESSAGE =
28
- 'Do not use palette.common or common.* color paths in product code. Use semantic palette tokens instead.'
29
-
30
- /** The MUI props that take a colour path, so `common.*` in them is the same mistake. */
31
- const COLOUR_PROPS =
32
- /^(color|bgcolor|background|backgroundColor|borderColor|borderTopColor|borderRightColor|borderBottomColor|borderLeftColor|outlineColor|fill|stroke|textDecorationColor|caretColor|accentColor)$/
33
-
34
- /**
35
- * Lookbehind, positive or negative. NOT named groups `(?<name>)`, which Safari has had since
36
- * 11.3: the class is exactly `(?<` followed by `=` or `!`.
15
+ * ⚠️ The build copies a role's whole `plugins/` DIRECTORY, so `plugins/hublo/` ships beside
16
+ * `plugins/hublo.js` and the relative imports below resolve the same way in this repository and
17
+ * in the published package. The generated preset still names `../plugins/hublo.js`, so nothing an
18
+ * adopted module extends has moved.
37
19
  */
38
- const LOOKBEHIND = /\(\?<[=!]/
39
-
40
- const LOOKBEHIND_MESSAGE =
41
- 'Regex lookbehind ((?<=) / (?<!)) is Safari 16.4+ and can be neither transpiled nor polyfilled: on older iOS the literal is a parse-time SyntaxError that takes the whole chunk with it. Rewrite the pattern. Named groups (?<name>) are fine.'
42
-
43
- /** `common.something` as a string value, e.g. `{ color: 'common.white' }`. */
44
- const isCommonPath = (node) =>
45
- node?.type === 'Literal' && typeof node.value === 'string' && /^common\./.test(node.value)
20
+ import { rule as noPaletteCommon } from './hublo/no-palette-common.js'
21
+ import { rule as noRegexLookbehind } from './hublo/no-regex-lookbehind.js'
22
+ import { rule as requireReloadDocument } from './hublo/require-reload-document.js'
46
23
 
47
- const plugin = {
24
+ export default {
48
25
  meta: { name: 'hublo' },
49
26
  rules: {
50
- 'no-palette-common': {
51
- meta: {
52
- docs: {
53
- description: 'Ban palette.common and common.* colour paths in product code.',
54
- },
55
- },
56
- create(context) {
57
- const report = (node) => context.report({ node, message: MESSAGE })
58
-
59
- return {
60
- // 1-4: reaching `common` off a palette, by property or by computed key.
61
- MemberExpression(node) {
62
- const onPalette =
63
- node.object?.property?.name === 'palette' || node.object?.name === 'palette'
64
- if (!onPalette) {
65
- return
66
- }
67
- const reachesCommon =
68
- node.property?.name === 'common' ||
69
- (node.computed && node.property?.value === 'common')
70
- if (reachesCommon) {
71
- report(node)
72
- }
73
- },
74
-
75
- // 5-6: `{ color: 'common.white' }` in an object literal, key written either way.
76
- Property(node) {
77
- const key = node.key?.name ?? node.key?.value
78
- if (typeof key === 'string' && COLOUR_PROPS.test(key) && isCommonPath(node.value)) {
79
- report(node.value)
80
- }
81
- },
82
-
83
- // 7-8: `color="common.white"` and `color={'common.white'}` on a JSX element.
84
- JSXAttribute(node) {
85
- const name = node.name?.name
86
- if (typeof name !== 'string' || !COLOUR_PROPS.test(name)) {
87
- return
88
- }
89
- const value = node.value
90
- if (isCommonPath(value)) {
91
- return report(value)
92
- }
93
- if (value?.type === 'JSXExpressionContainer' && isCommonPath(value.expression)) {
94
- report(value.expression)
95
- }
96
- },
97
- }
98
- },
99
- },
100
-
101
- 'no-regex-lookbehind': {
102
- meta: {
103
- docs: {
104
- description: 'Ban regex lookbehind, unsupported by Safari below 16.4.',
105
- },
106
- },
107
- create(context) {
108
- const report = (node) => context.report({ node, message: LOOKBEHIND_MESSAGE })
109
-
110
- /**
111
- * `new RegExp(...)` and `RegExp(...)`, when the pattern is readable statically: a
112
- * string, or a template whose fixed parts carry the lookbehind.
113
- *
114
- * Each template chunk is tested on its own rather than joined. Joining invents a
115
- * `(?<=` that the runtime string never has, across an interpolation boundary:
116
- * `` `(?<${op}=x)` `` would read as one.
117
- */
118
- const isStaticLookbehind = (pattern) => {
119
- if (pattern?.type === 'Literal') {
120
- return typeof pattern.value === 'string' && LOOKBEHIND.test(pattern.value)
121
- }
122
- if (pattern?.type === 'TemplateLiteral') {
123
- return (pattern.quasis ?? []).some((quasi) =>
124
- LOOKBEHIND.test(quasi.value?.cooked ?? quasi.value?.raw ?? ''),
125
- )
126
- }
127
- return false
128
- }
129
-
130
- const checkRegExpCall = (node) => {
131
- if (node.callee?.name !== 'RegExp') {
132
- return
133
- }
134
- const pattern = node.arguments?.[0]
135
- if (isStaticLookbehind(pattern)) {
136
- report(pattern)
137
- }
138
- }
139
-
140
- return {
141
- // The case that actually breaks a page: a literal is an EARLY error, so the whole
142
- // chunk fails to parse rather than the one call site failing.
143
- Literal(node) {
144
- if (node.regex && LOOKBEHIND.test(node.regex.pattern)) {
145
- report(node)
146
- }
147
- },
148
- NewExpression: checkRegExpCall,
149
- CallExpression: checkRegExpCall,
150
- }
151
- },
152
- },
27
+ 'no-palette-common': noPaletteCommon,
28
+ 'no-regex-lookbehind': noRegexLookbehind,
29
+ 'require-reload-document': requireReloadDocument,
153
30
  },
154
31
  }
155
-
156
- export default plugin
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Cognitive complexity, measured by Sonar's own rule, loaded into oxlint.
3
+ *
4
+ * ⚠️ Source file. It is COPIED to `plugins/` at build time by `scripts/build-presets.ts`, which
5
+ * wipes that directory first: `plugins/` is git-ignored, so a file written straight into it lasts
6
+ * until the next build. Edit this one.
7
+ *
8
+ * ## Why a bridge and not an implementation
9
+ *
10
+ * Cognitive complexity is Campbell 2018, and its author works at SonarSource, so their
11
+ * implementation is not one reading of the specification among several: it is the specification's
12
+ * own house, with the language-specific decisions that come with it. The clearest of those is that
13
+ * `||` and `??` sequences do NOT increment in JavaScript and TypeScript, which looks like a defect
14
+ * against the generic paper and is deliberate: in this language those two operators are
15
+ * overwhelmingly default-value shorthand rather than control flow. SonarSource say so themselves,
16
+ * "we tried to adjust the cognitive complexity rule to accommodate common idioms", and made the
17
+ * exclusion universal in SonarQube 10.8 after it had been inconsistent between contexts.
18
+ *
19
+ * Measured here before it was believed, same file and same parser on both:
20
+ *
21
+ * | | sonarjs 0.25.1 | sonarjs 4.2.2 |
22
+ * | `if (a \|\| b \|\| c)` | 2 | 1 |
23
+ * | `if (a && b && c)` | 2 | 2 |
24
+ * | `const x = a \|\| b \|\| c`| 1 | 0 |
25
+ *
26
+ * ⛔ **The rule is USED, never copied.** `eslint-plugin-sonarjs` ships under the Sonar
27
+ * Source-Available License, whose grant is limited to a "Non-competitive Purpose", and sentinel is
28
+ * published under MIT. Depending on a package is not preparing a derivative work of it; copying its
29
+ * code into this repository would be, and would also mean granting rights we never received. So
30
+ * this file re-exposes their rule and contains none of their code.
31
+ *
32
+ * ## Why it works at all, which is not obvious
33
+ *
34
+ * This is an ESLint rule running inside oxlint. Measured before it was adopted:
35
+ *
36
+ * | | |
37
+ * | the same values as ESLint, on `.js` and `.ts`| identical on every fixture, including the 7 of Campbell's own published example |
38
+ * | on a real module, `contract`, 516 files | 765 ms, exit 0, no error |
39
+ * | `eslint` itself | NOT installed: pnpm leaves the peer unmet and oxlint provides the context |
40
+ * | advisories the dependency adds | 0 at every level, 94 packages |
41
+ *
42
+ * ⚠️ It does pull `typescript` as a hard dependency of the plugin, which nothing of ours imports.
43
+ */
44
+ import { createRequire } from 'node:module'
45
+
46
+ const require = createRequire(import.meta.url)
47
+
48
+ /**
49
+ * ⛔ The deep path, and not the package name.
50
+ *
51
+ * `eslint-plugin-sonarjs` declares `main: ./cjs/plugin.js` and no `exports` map, so a bare ESM
52
+ * `import` of the package name fails to resolve under Node's ESM rules. Measured: it throws
53
+ * `ERR_MODULE_NOT_FOUND` before oxlint has read a single file, and oxlint reports it as
54
+ * "Failed to parse oxlint configuration file", which points at the wrong thing entirely.
55
+ */
56
+ /*
57
+ * ## ⛔ The ONE rule, not the 295 the package ships
58
+ *
59
+ * `cjs/plugin.js` is the public door and it loads every rule to hand back one. Measured on this
60
+ * machine, cold, three runs each:
61
+ *
62
+ * cjs/plugin.js 399 ms 295 rules we use 1
63
+ * cjs/S3776/index.js 159 ms 1 rule the same object
64
+ *
65
+ * That difference is paid ONCE PER MODULE, because oxlint runs as a process per invocation. Over
66
+ * the whole repository, 441 modules, it is the difference between a sweep that is mostly loading
67
+ * rules nobody asked for and one that is not. The rule's own work is untouched: same `create`,
68
+ * same `meta.schema`, same figures.
69
+ *
70
+ * ⚠️ S3776 is Sonar's own identifier for cognitive complexity, and the package lays out ONE
71
+ * directory per rule, 295 of them. It also declares no `exports` field, so this path is reachable
72
+ * by the package's own layout rather than by accident.
73
+ *
74
+ * ⛔ It falls BACK rather than failing, and says so on stderr. A future version that moves the
75
+ * file must not cost the figure, and must not cost it silently either: a fallback nobody can see
76
+ * is how a 240 ms regression per module comes back unnoticed.
77
+ */
78
+ function cognitiveRule() {
79
+ try {
80
+ const only = require('eslint-plugin-sonarjs/cjs/S3776/index.js')
81
+ const rule = only.rule ?? only.default?.rule
82
+ if (typeof rule?.create === 'function') return rule
83
+ } catch {
84
+ // falls through to the public door below
85
+ }
86
+
87
+ const sonarjs = require('eslint-plugin-sonarjs/cjs/plugin.js')
88
+ /** CJS interop: the plugin is reached either directly or behind `default`. */
89
+ const rule = (sonarjs.rules ?? sonarjs.default?.rules)?.['cognitive-complexity']
90
+ if (typeof rule?.create !== 'function') {
91
+ throw new Error(
92
+ 'sentinel: eslint-plugin-sonarjs exposes `cognitive-complexity` from neither ' +
93
+ 'cjs/S3776/index.js nor cjs/plugin.js. The cognitive complexity figure cannot be ' +
94
+ 'produced; see this file.',
95
+ )
96
+ }
97
+ process.stderr.write(
98
+ 'sentinel static-analysis: eslint-plugin-sonarjs no longer exposes cjs/S3776/index.js, so the ' +
99
+ 'whole package was loaded for one rule. The figures are unchanged and the size pass is about ' +
100
+ '240 ms slower per module; see plugins/sonar-cognitive.js.\n',
101
+ )
102
+ return rule
103
+ }
104
+
105
+ export default {
106
+ meta: { name: 'sonar' },
107
+ rules: { 'cognitive-complexity': cognitiveRule() },
108
+ }