eslint-plugin-kerfjs 5.0.0-beta.91 → 5.0.0-beta.92

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
@@ -80,9 +80,17 @@ settings: {
80
80
  For deterministic generated configs, the same settings accept parsed `catalog`, `selectionCatalog`, and `profile` objects. File-based precedence is package defaults, then workspace profile, then root-to-leaf directory profiles. The rules load `application-ui-profile-sync.cjs` from the installed `@kerfjs/ui` package by default because ESLint rule creation is synchronous; `profileContractPath` is only needed when artifacts are vendored, and then its directory must also hold `application-ui-diagnostic-ids-v1.json` and `component-class-owners.cjs` (the class-to-component rule `KUI-L103` shares with `@kerfjs/ui`'s generated component pages) from the same `@kerfjs/ui` release. Every raw layer is schema- and catalog-reference-validated before merge, so a child cannot hide malformed or stale parent policy. Exceptions use exact `KUI-L*` diagnostic ids and repository-relative file or directory targets.
81
81
 
82
82
  `ui-css-values` consumes each catalog entry's property-specific grammar and
83
- emits `KUI-L013`–`KUI-L017` with a preferred shorthand or helper. It covers JSX,
83
+ emits `KUI-L013`–`KUI-L016` with a preferred shorthand or helper. It covers JSX,
84
84
  direct component calls, nested catalog paths, and explicitly configured
85
- third-party catalogs; dynamic values remain governed by TypeScript.
85
+ third-party catalogs; dynamic values remain governed by TypeScript. Its
86
+ companion `ui-exceptional-shorthands` walks the same contracts and emits the
87
+ review finding `KUI-L017` for a valid but exceptional off-scale shorthand. It
88
+ is `warn` in both `recommended-ui` and `strict-ui`, matching the analyzer's
89
+ `review` classification, so `kerf-ui-doctor` merges the ESLint and analyzer
90
+ reports into one review finding.
91
+
92
+ UI rule severities follow the analyzer's classification of the codes each rule
93
+ reports: an `error` code ships as `error`, and a `review` code ships as `warn`.
86
94
 
87
95
  `ui-component-ownership` keeps code out of another package's component
88
96
  internals: `KUI-L020` reports a reference to a component's private `--_*`
@@ -115,6 +123,7 @@ Use flat config (`eslint.config.js`), shown above. The `legacy-recommended` expo
115
123
  | [`ui-component-ownership`](docs/rules/ui-component-ownership.md) | — (configure components, never override them) | `error` |
116
124
  | [`ui-composition`](docs/rules/ui-composition.md) | — (cataloged parents and zones) | `error` |
117
125
  | [`ui-css-values`](docs/rules/ui-css-values.md) | — (cataloged property-specific value grammar) | `error` |
126
+ | [`ui-exceptional-shorthands`](docs/rules/ui-exceptional-shorthands.md) | — (review deliberate off-scale shorthands) | `warn` |
118
127
  | [`ui-preferences`](docs/rules/ui-preferences.md) | — (application component choices) | `warn` |
119
128
  | [`ui-wiring`](docs/rules/ui-wiring.md) | — (required setup and cleanup) | `warn` |
120
129
 
@@ -7,9 +7,11 @@ names their accepted helpers, finite shorthands, and raw-value policy.
7
7
 
8
8
  Stable diagnostics are `KUI-L013` (raw or unknown shorthand), `KUI-L014`
9
9
  (wrong property helper), `KUI-L015` (non-standalone expression), `KUI-L016`
10
- (forbidden declaration list), and review-oriented `KUI-L017` (exceptional
11
- spacing shorthand). Messages name the preferred shorthand or helper. Dynamic
12
- values are left to TypeScript rather than guessed.
10
+ (forbidden declaration list). Messages name the preferred shorthand or helper.
11
+ Dynamic values are left to TypeScript rather than guessed. The review finding
12
+ `KUI-L017` (exceptional spacing shorthand) is reported by the companion
13
+ [`ui-exceptional-shorthands`](ui-exceptional-shorthands.md) rule so it can carry
14
+ the `warn` severity that matches the analyzer's `review` classification.
13
15
 
14
16
  Only component exports carry an entry's contracts. A catalog entry's
15
17
  `publicExports` also lists the helpers its props consume (`Select` ships
@@ -0,0 +1,22 @@
1
+ # `ui-exceptional-shorthands`
2
+
3
+ Reports `KUI-L017` when a Kerf UI component prop uses a valid but exceptional
4
+ off-scale shorthand — for example `<List gap="s" />` or `<Row gap="xl" />`,
5
+ where the catalog lists `s` and `xl` under the prop's `exceptionalShorthands`.
6
+ The value renders correctly; the finding asks whether the off-scale choice is
7
+ deliberate. The message names the canonical shorthands and helpers to prefer.
8
+
9
+ This is a review finding, not a contract violation. The analyzer classifies
10
+ `KUI-L017` as `review`, so the rule is `warn` in both `recommended-ui` and
11
+ `strict-ui`, and `kerf-ui-doctor` merges the ESLint and analyzer reports into a
12
+ single review finding. To make review findings fail a run, use the doctor's
13
+ `failOn: 'review'` rather than promoting this rule to `error`.
14
+
15
+ Keep a deliberate choice by recording a profile exception for `KUI-L017` with a
16
+ rationale, or move to a canonical shorthand or a typed helper such as
17
+ `space('s')`.
18
+
19
+ The rule shares its catalog walk with [`ui-css-values`](ui-css-values.md),
20
+ which reports the `KUI-L013`–`KUI-L016` contract violations at `error`. It
21
+ covers JSX and direct component calls against the bundled `@kerfjs/ui` catalog
22
+ and any third-party catalog configured through `settings.kerfjs.ui.catalog`.
package/index.js CHANGED
@@ -9,12 +9,13 @@ import requireDelegateDisposer from './lib/rules/require-delegate-disposer.js';
9
9
  import uiComponentOwnership from './lib/rules/ui-component-ownership.js';
10
10
  import uiComposition from './lib/rules/ui-composition.js';
11
11
  import uiCssValues from './lib/rules/ui-css-values.js';
12
+ import uiExceptionalShorthands from './lib/rules/ui-exceptional-shorthands.js';
12
13
  import uiPreferences from './lib/rules/ui-preferences.js';
13
14
  import uiPublicBoundaries from './lib/rules/ui-public-boundaries.js';
14
15
  import uiWiring from './lib/rules/ui-wiring.js';
15
16
 
16
17
  const plugin = {
17
- meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.91' },
18
+ meta: { name: 'eslint-plugin-kerfjs', version: '5.0.0-beta.92' },
18
19
  rules: {
19
20
  'no-inline-jsx-event-handlers': noInlineJsxEventHandlers,
20
21
  'no-raw-with-dynamic-arg': noRawWithDynamicArg,
@@ -27,6 +28,7 @@ const plugin = {
27
28
  'ui-component-ownership': uiComponentOwnership,
28
29
  'ui-composition': uiComposition,
29
30
  'ui-css-values': uiCssValues,
31
+ 'ui-exceptional-shorthands': uiExceptionalShorthands,
30
32
  'ui-preferences': uiPreferences,
31
33
  'ui-public-boundaries': uiPublicBoundaries,
32
34
  'ui-wiring': uiWiring,
@@ -62,12 +64,21 @@ plugin.configs.recommended = {
62
64
  plugins: { kerfjs: plugin },
63
65
  rules: recommendedRules,
64
66
  };
67
+ // UI rule severities follow the analyzer's classification of the KUI codes a
68
+ // rule reports (`UI_ANALYSIS_RULES` in `@kerfjs/ui/analyzer`): an `error`
69
+ // code ships as `error`, a `review` code as `warn`. `ui-exceptional-shorthands`
70
+ // (`KUI-L017`) is a review finding — a deliberate off-scale choice to justify,
71
+ // not a defect — so it stays `warn` in strict-ui too; promoting it would
72
+ // contradict the analyzer and make kerf-ui-doctor report the same finding
73
+ // twice. Gate review findings with the doctor's `failOn: 'review'` instead.
74
+ // `@kerfjs/ui`'s eslint-severity-contract test pins this agreement.
65
75
  const recommendedUiRules = {
66
76
  ...recommendedRules,
67
77
  'kerfjs/ui-public-boundaries': 'error',
68
78
  'kerfjs/ui-component-ownership': 'error',
69
79
  'kerfjs/ui-composition': 'error',
70
80
  'kerfjs/ui-css-values': 'error',
81
+ 'kerfjs/ui-exceptional-shorthands': 'warn',
71
82
  'kerfjs/ui-preferences': 'warn',
72
83
  'kerfjs/ui-wiring': 'warn',
73
84
  };
@@ -1,243 +1,3 @@
1
- import {
2
- helperCall,
3
- importRegistry,
4
- isExcepted,
5
- jsxKey,
6
- loadUiContract,
7
- UI_CONTRACT_LOAD_CODE,
8
- UI_RULE_SCHEMA,
9
- } from '../ui-contract.js';
1
+ import { createCssValueRule } from '../ui-css-value-contracts.js';
10
2
 
11
- const CODES = {
12
- raw: 'KUI-L013',
13
- helper: 'KUI-L014',
14
- expression: 'KUI-L015',
15
- declarations: 'KUI-L016',
16
- exceptional: 'KUI-L017',
17
- };
18
-
19
- function propertyName(node) {
20
- if (!node || node.computed) return undefined;
21
- return node.key?.name ?? node.key?.value;
22
- }
23
-
24
- function objectProperty(object, name) {
25
- return object?.properties?.find(
26
- (property) =>
27
- property.type === 'Property' && propertyName(property) === name,
28
- );
29
- }
30
-
31
- function jsxAttribute(opening, name) {
32
- return opening.attributes.find(
33
- (attribute) =>
34
- attribute.type === 'JSXAttribute' && attribute.name.name === name,
35
- );
36
- }
37
-
38
- function jsxValue(attribute) {
39
- if (!attribute?.value) return undefined;
40
- if (attribute.value.type === 'Literal') return attribute.value;
41
- if (attribute.value.type === 'JSXExpressionContainer')
42
- return attribute.value.expression;
43
- return undefined;
44
- }
45
-
46
- function nestedValues(value, tail) {
47
- if (!tail) return [value];
48
- if (value?.type !== 'ArrayExpression') return [];
49
- return value.elements.flatMap((element) => {
50
- if (element?.type !== 'ObjectExpression') return [];
51
- const property = objectProperty(element, tail);
52
- return property ? [property.value] : [];
53
- });
54
- }
55
-
56
- function contractValuesFromJsx(opening, path) {
57
- const [head, tail] = path.split('[].');
58
- return nestedValues(jsxValue(jsxAttribute(opening, head)), tail);
59
- }
60
-
61
- function contractValuesFromCall(call, path) {
62
- const options = call.arguments[0];
63
- if (options?.type !== 'ObjectExpression') return [];
64
- const [head, tail] = path.split('[].');
65
- const property = objectProperty(options, head);
66
- return property ? nestedValues(property.value, tail) : [];
67
- }
68
-
69
- export default {
70
- meta: {
71
- type: 'problem',
72
- docs: {
73
- description:
74
- 'Enforce cataloged property-specific CSS value contracts for Kerf UI components.',
75
- url: 'https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/docs/rules/ui-css-values.md',
76
- },
77
- schema: UI_RULE_SCHEMA,
78
- messages: {
79
- config: `${UI_CONTRACT_LOAD_CODE}: Kerf UI catalog/profile could not be loaded: {{error}}`,
80
- raw: '{{code}}: `{{component}}.{{path}}` uses {{grammar}} grammar; replace raw `{{value}}` with {{preferred}}.',
81
- helper:
82
- '{{code}}: `{{helper}}()` produces the wrong grammar for `{{component}}.{{path}}`; use {{preferred}}.',
83
- expression:
84
- '{{code}}: `{{helper}}()` is not a standalone CSS value for `{{component}}.{{path}}`; wrap it with an accepted composer such as `calc()`.',
85
- declarations:
86
- '{{code}}: `{{component}}.{{path}}` is a forbidden declaration-list escape hatch; use `className`, public tokens, or cataloged props.',
87
- exceptional:
88
- '{{code}}: `{{value}}` is an exceptional shorthand for `{{component}}.{{path}}`; prefer {{preferred}} unless the off-scale choice is deliberate.',
89
- },
90
- },
91
- create(context) {
92
- const loaded = loadUiContract(context);
93
- const filename = context.filename ?? context.getFilename();
94
- let registry;
95
-
96
- const inspect = (node, entry, valuesFor) => {
97
- for (const cssContract of entry.cssValueProps ?? []) {
98
- const preferred =
99
- [
100
- ...(cssContract.canonicalShorthands ?? []).map(
101
- (item) => `\`${item}\``,
102
- ),
103
- ...(cssContract.helpers ?? []).map((item) => `\`${item}()\``),
104
- ].join(' or ') || 'a cataloged component prop';
105
- for (const value of valuesFor(cssContract.path)) {
106
- if (!value) continue;
107
- if (
108
- cssContract.grammar === 'declarations' &&
109
- cssContract.rawPolicy !== 'allow'
110
- ) {
111
- if (!isExcepted(loaded, CODES.declarations, filename))
112
- context.report({
113
- node: value,
114
- messageId: 'declarations',
115
- data: {
116
- code: CODES.declarations,
117
- component: entry.name,
118
- path: cssContract.path,
119
- },
120
- });
121
- continue;
122
- }
123
- const literal =
124
- value.type === 'Literal' && typeof value.value === 'string'
125
- ? value.value
126
- : value.type === 'TemplateLiteral' &&
127
- value.expressions.length === 0
128
- ? value.quasis[0].value.cooked
129
- : undefined;
130
- if (literal !== undefined) {
131
- if (cssContract.exceptionalShorthands?.includes(literal)) {
132
- if (!isExcepted(loaded, CODES.exceptional, filename))
133
- context.report({
134
- node: value,
135
- messageId: 'exceptional',
136
- data: {
137
- code: CODES.exceptional,
138
- component: entry.name,
139
- path: cssContract.path,
140
- value: literal,
141
- preferred,
142
- },
143
- });
144
- } else if (
145
- !cssContract.shorthands?.includes(literal) &&
146
- cssContract.rawPolicy !== 'allow'
147
- ) {
148
- const code = CODES.raw;
149
- if (!isExcepted(loaded, code, filename))
150
- context.report({
151
- node: value,
152
- messageId: 'raw',
153
- data: {
154
- code,
155
- component: entry.name,
156
- path: cssContract.path,
157
- grammar: cssContract.grammar,
158
- value: literal,
159
- preferred,
160
- },
161
- });
162
- }
163
- continue;
164
- }
165
- if (value.type !== 'CallExpression') continue;
166
- const helper = helperCall(value.callee, registry, loaded);
167
- if (!helper) continue;
168
- if (cssContract.nonStandaloneHelpers?.includes(helper.imported)) {
169
- if (!isExcepted(loaded, CODES.expression, filename))
170
- context.report({
171
- node: value,
172
- messageId: 'expression',
173
- data: {
174
- code: CODES.expression,
175
- component: entry.name,
176
- path: cssContract.path,
177
- helper: helper.imported,
178
- },
179
- });
180
- } else if (
181
- !cssContract.helpers?.includes(helper.imported) &&
182
- cssContract.unsafeHelper !== helper.imported
183
- ) {
184
- if (!isExcepted(loaded, CODES.helper, filename))
185
- context.report({
186
- node: value,
187
- messageId: 'helper',
188
- data: {
189
- code: CODES.helper,
190
- component: entry.name,
191
- path: cssContract.path,
192
- helper: helper.imported,
193
- preferred,
194
- },
195
- });
196
- }
197
- }
198
- }
199
- };
200
-
201
- return {
202
- Program(node) {
203
- if (loaded.error)
204
- context.report({
205
- node,
206
- messageId: 'config',
207
- data: { error: loaded.error },
208
- });
209
- else registry = importRegistry(node, loaded, filename);
210
- },
211
- JSXOpeningElement(node) {
212
- if (!registry) return;
213
- const key = jsxKey(node.name, registry, loaded);
214
- const entry = loaded.entries.get(key);
215
- if (entry)
216
- inspect(node, entry, (path) => contractValuesFromJsx(node, path));
217
- },
218
- CallExpression(node) {
219
- if (!registry) return;
220
- let key;
221
- if (node.callee.type === 'Identifier')
222
- key = registry.locals.get(node.callee.name);
223
- else if (
224
- node.callee.type === 'MemberExpression' &&
225
- !node.callee.computed &&
226
- node.callee.object.type === 'Identifier' &&
227
- node.callee.property.type === 'Identifier'
228
- ) {
229
- const source = registry.namespaces.get(node.callee.object.name);
230
- const exportedKey = loaded.exports.get(node.callee.property.name);
231
- if (
232
- source === loaded.package ||
233
- loaded.imports.get(source) === exportedKey
234
- )
235
- key = exportedKey;
236
- }
237
- const entry = loaded.entries.get(key);
238
- if (entry)
239
- inspect(node, entry, (path) => contractValuesFromCall(node, path));
240
- },
241
- };
242
- },
243
- };
3
+ export default createCssValueRule({ review: false });
@@ -0,0 +1,3 @@
1
+ import { createCssValueRule } from '../ui-css-value-contracts.js';
2
+
3
+ export default createCssValueRule({ review: true });
@@ -0,0 +1,261 @@
1
+ import {
2
+ helperCall,
3
+ importRegistry,
4
+ isExcepted,
5
+ jsxKey,
6
+ loadUiContract,
7
+ UI_CONTRACT_LOAD_CODE,
8
+ UI_RULE_SCHEMA,
9
+ } from './ui-contract.js';
10
+
11
+ const CODES = {
12
+ raw: 'KUI-L013',
13
+ helper: 'KUI-L014',
14
+ expression: 'KUI-L015',
15
+ declarations: 'KUI-L016',
16
+ exceptional: 'KUI-L017',
17
+ };
18
+
19
+ function propertyName(node) {
20
+ if (!node || node.computed) return undefined;
21
+ return node.key?.name ?? node.key?.value;
22
+ }
23
+
24
+ function objectProperty(object, name) {
25
+ return object?.properties?.find(
26
+ (property) =>
27
+ property.type === 'Property' && propertyName(property) === name,
28
+ );
29
+ }
30
+
31
+ function jsxAttribute(opening, name) {
32
+ return opening.attributes.find(
33
+ (attribute) =>
34
+ attribute.type === 'JSXAttribute' && attribute.name.name === name,
35
+ );
36
+ }
37
+
38
+ function jsxValue(attribute) {
39
+ if (!attribute?.value) return undefined;
40
+ if (attribute.value.type === 'Literal') return attribute.value;
41
+ if (attribute.value.type === 'JSXExpressionContainer')
42
+ return attribute.value.expression;
43
+ return undefined;
44
+ }
45
+
46
+ function nestedValues(value, tail) {
47
+ if (!tail) return [value];
48
+ if (value?.type !== 'ArrayExpression') return [];
49
+ return value.elements.flatMap((element) => {
50
+ if (element?.type !== 'ObjectExpression') return [];
51
+ const property = objectProperty(element, tail);
52
+ return property ? [property.value] : [];
53
+ });
54
+ }
55
+
56
+ function contractValuesFromJsx(opening, path) {
57
+ const [head, tail] = path.split('[].');
58
+ return nestedValues(jsxValue(jsxAttribute(opening, head)), tail);
59
+ }
60
+
61
+ function contractValuesFromCall(call, path) {
62
+ const options = call.arguments[0];
63
+ if (options?.type !== 'ObjectExpression') return [];
64
+ const [head, tail] = path.split('[].');
65
+ const property = objectProperty(options, head);
66
+ return property ? nestedValues(property.value, tail) : [];
67
+ }
68
+
69
+ const CONTRACT_MESSAGES = {
70
+ raw: 'KUI-L013: `{{component}}.{{path}}` uses {{grammar}} grammar; replace raw `{{value}}` with {{preferred}}.',
71
+ helper:
72
+ 'KUI-L014: `{{helper}}()` produces the wrong grammar for `{{component}}.{{path}}`; use {{preferred}}.',
73
+ expression:
74
+ 'KUI-L015: `{{helper}}()` is not a standalone CSS value for `{{component}}.{{path}}`; wrap it with an accepted composer such as `calc()`.',
75
+ declarations:
76
+ 'KUI-L016: `{{component}}.{{path}}` is a forbidden declaration-list escape hatch; use `className`, public tokens, or cataloged props.',
77
+ };
78
+
79
+ const REVIEW_MESSAGES = {
80
+ exceptional:
81
+ 'KUI-L017: `{{value}}` is an exceptional shorthand for `{{component}}.{{path}}`; prefer {{preferred}} unless the off-scale choice is deliberate.',
82
+ };
83
+
84
+ // One catalog walk backs two rules because ESLint assigns severity per rule,
85
+ // not per message. `KUI-L013`–`KUI-L016` are contract violations (analyzer
86
+ // severity `error`); `KUI-L017` is a review finding (analyzer severity
87
+ // `review`), so it lives in its own rule that the presets configure as `warn`.
88
+ // Reporting all five from one `error` rule made a deliberate off-scale choice
89
+ // fail lint while the analyzer and doctor queued it for review.
90
+ export function createCssValueRule({ review }) {
91
+ const slug = review ? 'ui-exceptional-shorthands' : 'ui-css-values';
92
+ const reported = review ? REVIEW_MESSAGES : CONTRACT_MESSAGES;
93
+ return {
94
+ meta: {
95
+ type: review ? 'suggestion' : 'problem',
96
+ docs: {
97
+ description: review
98
+ ? 'Flag valid but exceptional off-scale shorthands for Kerf UI components for review.'
99
+ : 'Enforce cataloged property-specific CSS value contracts for Kerf UI components.',
100
+ url: `https://github.com/brianwestphal/kerf/blob/main/eslint-plugin/docs/rules/${slug}.md`,
101
+ },
102
+ schema: UI_RULE_SCHEMA,
103
+ messages: {
104
+ config: `${UI_CONTRACT_LOAD_CODE}: Kerf UI catalog/profile could not be loaded: {{error}}`,
105
+ ...reported,
106
+ },
107
+ },
108
+ create(context) {
109
+ return createVisitors(context, (descriptor) => {
110
+ if (descriptor.messageId in reported) context.report(descriptor);
111
+ });
112
+ },
113
+ };
114
+ }
115
+
116
+ function createVisitors(context, report) {
117
+ const loaded = loadUiContract(context);
118
+ const filename = context.filename ?? context.getFilename();
119
+ let registry;
120
+
121
+ const inspect = (node, entry, valuesFor) => {
122
+ for (const cssContract of entry.cssValueProps ?? []) {
123
+ const preferred =
124
+ [
125
+ ...(cssContract.canonicalShorthands ?? []).map(
126
+ (item) => `\`${item}\``,
127
+ ),
128
+ ...(cssContract.helpers ?? []).map((item) => `\`${item}()\``),
129
+ ].join(' or ') || 'a cataloged component prop';
130
+ for (const value of valuesFor(cssContract.path)) {
131
+ if (!value) continue;
132
+ if (
133
+ cssContract.grammar === 'declarations' &&
134
+ cssContract.rawPolicy !== 'allow'
135
+ ) {
136
+ if (!isExcepted(loaded, CODES.declarations, filename))
137
+ report({
138
+ node: value,
139
+ messageId: 'declarations',
140
+ data: {
141
+ component: entry.name,
142
+ path: cssContract.path,
143
+ },
144
+ });
145
+ continue;
146
+ }
147
+ const literal =
148
+ value.type === 'Literal' && typeof value.value === 'string'
149
+ ? value.value
150
+ : value.type === 'TemplateLiteral' && value.expressions.length === 0
151
+ ? value.quasis[0].value.cooked
152
+ : undefined;
153
+ if (literal !== undefined) {
154
+ if (cssContract.exceptionalShorthands?.includes(literal)) {
155
+ if (!isExcepted(loaded, CODES.exceptional, filename))
156
+ report({
157
+ node: value,
158
+ messageId: 'exceptional',
159
+ data: {
160
+ component: entry.name,
161
+ path: cssContract.path,
162
+ value: literal,
163
+ preferred,
164
+ },
165
+ });
166
+ } else if (
167
+ !cssContract.shorthands?.includes(literal) &&
168
+ cssContract.rawPolicy !== 'allow'
169
+ ) {
170
+ const code = CODES.raw;
171
+ if (!isExcepted(loaded, code, filename))
172
+ report({
173
+ node: value,
174
+ messageId: 'raw',
175
+ data: {
176
+ component: entry.name,
177
+ path: cssContract.path,
178
+ grammar: cssContract.grammar,
179
+ value: literal,
180
+ preferred,
181
+ },
182
+ });
183
+ }
184
+ continue;
185
+ }
186
+ if (value.type !== 'CallExpression') continue;
187
+ const helper = helperCall(value.callee, registry, loaded);
188
+ if (!helper) continue;
189
+ if (cssContract.nonStandaloneHelpers?.includes(helper.imported)) {
190
+ if (!isExcepted(loaded, CODES.expression, filename))
191
+ report({
192
+ node: value,
193
+ messageId: 'expression',
194
+ data: {
195
+ component: entry.name,
196
+ path: cssContract.path,
197
+ helper: helper.imported,
198
+ },
199
+ });
200
+ } else if (
201
+ !cssContract.helpers?.includes(helper.imported) &&
202
+ cssContract.unsafeHelper !== helper.imported
203
+ ) {
204
+ if (!isExcepted(loaded, CODES.helper, filename))
205
+ report({
206
+ node: value,
207
+ messageId: 'helper',
208
+ data: {
209
+ component: entry.name,
210
+ path: cssContract.path,
211
+ helper: helper.imported,
212
+ preferred,
213
+ },
214
+ });
215
+ }
216
+ }
217
+ }
218
+ };
219
+
220
+ return {
221
+ Program(node) {
222
+ if (loaded.error)
223
+ context.report({
224
+ node,
225
+ messageId: 'config',
226
+ data: { error: loaded.error },
227
+ });
228
+ else registry = importRegistry(node, loaded, filename);
229
+ },
230
+ JSXOpeningElement(node) {
231
+ if (!registry) return;
232
+ const key = jsxKey(node.name, registry, loaded);
233
+ const entry = loaded.entries.get(key);
234
+ if (entry)
235
+ inspect(node, entry, (path) => contractValuesFromJsx(node, path));
236
+ },
237
+ CallExpression(node) {
238
+ if (!registry) return;
239
+ let key;
240
+ if (node.callee.type === 'Identifier')
241
+ key = registry.locals.get(node.callee.name);
242
+ else if (
243
+ node.callee.type === 'MemberExpression' &&
244
+ !node.callee.computed &&
245
+ node.callee.object.type === 'Identifier' &&
246
+ node.callee.property.type === 'Identifier'
247
+ ) {
248
+ const source = registry.namespaces.get(node.callee.object.name);
249
+ const exportedKey = loaded.exports.get(node.callee.property.name);
250
+ if (
251
+ source === loaded.package ||
252
+ loaded.imports.get(source) === exportedKey
253
+ )
254
+ key = exportedKey;
255
+ }
256
+ const entry = loaded.entries.get(key);
257
+ if (entry)
258
+ inspect(node, entry, (path) => contractValuesFromCall(node, path));
259
+ },
260
+ };
261
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-kerfjs",
3
- "version": "5.0.0-beta.91",
3
+ "version": "5.0.0-beta.92",
4
4
  "description": "ESLint rules that enforce kerf's hard rules — catches AI-shaped bugs at edit time.",
5
5
  "type": "module",
6
6
  "license": "MIT",