@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.
- package/README.md +9 -7
- package/dist/bin/sentinel.js +61 -16
- package/dist/{chunk-KLEXJFJ6.js → chunk-43HQ6BVJ.js} +61 -15
- package/dist/{chunk-YQRDKWSH.js → chunk-IL5PPLXE.js} +11 -1
- package/dist/chunk-IL5PPLXE.js.map +1 -0
- package/dist/chunk-MOELAQPC.js +10365 -0
- package/dist/chunk-WBDBRGLV.js +214 -0
- package/dist/chunk-WBDBRGLV.js.map +1 -0
- package/dist/{chunk-AMCGMLPS.js → chunk-Y57KA4QS.js} +2 -2
- package/dist/index.d.ts +28 -1
- package/dist/index.js +4 -4
- package/dist/roles/build/nest/toolchain.js +68 -18
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/static-analysis/config.d.ts +15 -1
- package/dist/roles/static-analysis/config.js +1 -1
- package/dist/roles/test/setup/jest-parity.js +2 -2
- package/dist/roles/test/setup/workspace-entry.js +2 -2
- package/dist/{validate-AWWMBSH5.js → validate-FRMENKHM.js} +2 -2
- package/docs/bundle-analysis-spec.md +272 -0
- package/docs/sql-verification-study.md +401 -0
- package/docs/static-analysis-adoption.md +285 -0
- package/docs/static-analysis-study.md +824 -18
- package/docs/typescript-adoption.md +1 -1
- package/docs/typescript-traces.md +27 -12
- package/docs/using-sentinel.md +22 -12
- package/docs/validating-a-change.md +185 -0
- package/lint/nest.json +98 -18
- package/lint/node.json +83 -3
- package/lint/react-lib.json +121 -31
- package/lint/react.json +101 -2
- package/package.json +27 -6
- package/plugins/hublo/no-palette-common.js +76 -0
- package/plugins/hublo/no-regex-lookbehind.js +70 -0
- package/plugins/hublo/require-reload-document.js +70 -0
- package/plugins/hublo.js +20 -145
- package/plugins/sonar-cognitive.js +108 -0
- package/dist/chunk-DKLQYGPI.js +0 -6466
- package/dist/chunk-EDFIVLUA.js +0 -100
- package/dist/chunk-EDFIVLUA.js.map +0 -1
- package/dist/chunk-YQRDKWSH.js.map +0 -1
- /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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
24
|
+
export default {
|
|
48
25
|
meta: { name: 'hublo' },
|
|
49
26
|
rules: {
|
|
50
|
-
'no-palette-common':
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
}
|