@gost-kit/eslint-plugin 0.1.0 → 0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @gost-kit/eslint-plugin
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9f59da3: Add the opt-in ready-boolean-condition rule for Svelte if/else-if conditions.
8
+ Accept ready boolean reads and literals, report computations/truthiness, and
9
+ require typed parser services with strictNullChecks for reads. The Svelte config
10
+ registers the rule off; projects select business/UI-Kit scopes explicitly.
11
+
3
12
  ## 0.1.0
4
13
 
5
14
  ### Minor Changes
package/README.md CHANGED
@@ -8,7 +8,8 @@ ESLint ^10.14.0 and TypeScript ^5.9.3.
8
8
 
9
9
  `@gost-kit/eslint-config-svelte` installs and enables the plugin for typed Svelte
10
10
  components and `.svelte.ts` modules. The selector rule is enabled by default;
11
- `gost/no-component-state` and `gost/single-intent-dispatch` require explicit
11
+ `gost/no-component-state`, `gost/single-intent-dispatch` and
12
+ `gost/ready-boolean-condition` require explicit
12
13
  consumer overrides for business component paths. UI-Kit paths can opt out.
13
14
  No separate plugin registration is needed.
14
15
  Other consumers may register the named public export in their flat config:
@@ -32,8 +33,8 @@ Nested common installations are supported; a direct common dependency of the
32
33
  application is not required for inferred Store detection.
33
34
  Local `$state.raw` bindings are recognized from their original lexical scope.
34
35
  For `require-feature-state-selector`, type information is required; an enabled typed rule without parser services
35
- reports a configuration error. Stateless and JavaScript-script components are
36
- skipped. Mixed components are checked per script: a typed module script does
36
+ reports a configuration error. For the selector rule, stateless and JavaScript-script
37
+ components are skipped. Mixed components are checked per script: a typed module script does
37
38
  not enable the rule for a JavaScript instance script. The Svelte config does not enable this typed rule for `.svelte.js`.
38
39
 
39
40
  `single-intent-dispatch` uses an `on*` name heuristic, including function-valued
@@ -44,3 +45,9 @@ ordinary optional data reads remain outside that gate check.
44
45
 
45
46
  See [RULES.md](RULES.md) for diagnostics, autofix boundaries and examples.
46
47
  Tests and test helpers are excluded from published artifacts.
48
+
49
+ `ready-boolean-condition` checks Svelte if/else-if conditions: true/false or
50
+ ready boolean identifier/static field reads. Computations and truthiness are
51
+ reported; typed reads require strictNullChecks and parser services. JS/stateless
52
+ reads are checked too when services are available. No autofix or presenter
53
+ provenance guarantee is provided. See the inventory for opt-in boundaries.
package/RULES.md CHANGED
@@ -247,3 +247,66 @@ a dispatch diagnostic, although it violates the business-component boundary.
247
247
  Event adaptation belongs in UI-Kit. Effects outside discovered UI callbacks may
248
248
  perform several dispatches. Presentation computations (including templates) are
249
249
  #19, boolean template conditions #18, and testable local UI-state #27.
250
+
251
+ ## gost/ready-boolean-condition
252
+
253
+ Opt in for business `.svelte` files. `{#if}` and `{:else if}` read a ready
254
+ boolean; ordinary else and conditional rendering remain available.
255
+
256
+ ```svelte
257
+ {#if view.isLoaded}
258
+ <p>{view.name}</p>
259
+ {:else if isFailed}
260
+ <p>Failed</p>
261
+ {/if}
262
+ ```
263
+
264
+ Allowed: boolean literals true/false, identifiers and static field chains rooted
265
+ at an identifier (for example view.ready), including quoted keys and number-valued
266
+ AST literal indexes such as flags[0] or flags[0x10]. The final type must be
267
+ definitely boolean. this.isReady is outside the supported root form. Signed keys
268
+ such as flags[-1] are unary expressions, and bigint keys such as flags[10n] are
269
+ not number-valued literals; both are outside this first-version syntax boundary.
270
+ Optional chains are accepted only when the complete expression excludes
271
+ undefined/null. Boolean aliases, boolean-only unions and proven boolean generic
272
+ constraints are accepted. TypeScript flow narrowing applies at the expression.
273
+
274
+ Reported with `readyBoolean`: comparisons, logical operations, negation, calls,
275
+ ternaries, dynamic keys, assertions/non-null assertions/satisfies wrappers,
276
+ object/string/number truthiness, nullable booleans and any/unknown/never. Move
277
+ the calculation to a pure presenter and read its named boolean result. There
278
+ is no autofix or suggestion that invents a presenter/field name.
279
+
280
+ Identifier/member reads require actual parser type services and strictNullChecks,
281
+ either directly or via strict. Explicit strictNullChecks: false overrides strict.
282
+ Missing services/null checking is a configuration error, not a successful check.
283
+ Literal booleans and invalid syntax do not need services; components without
284
+ conditions do not request them. Type services also cover JS-script and stateless
285
+ components when the configured parser supplies a program. Inferred any in JS
286
+ props is rejected just like TypeScript any. Script if statements, ordinary TS and
287
+ rune modules are outside the rule.
288
+
289
+ This checks shape and type, not provenance/purity. A local boolean or boolean
290
+ getter is not proof of a presenter. Rule #19 addresses presentation calculations
291
+ outside template conditions. Static indexed reads follow consumer TypeScript
292
+ options; enable noUncheckedIndexedAccess to expose possible missing entries.
293
+ The rule does not validate runtime input that the declared types conceal.
294
+
295
+ `createSvelteConfig` registers it explicitly off. Enable with a later override:
296
+
297
+ ```ts
298
+ {
299
+ files: ['src/**/*.svelte'],
300
+ rules: { 'gost/ready-boolean-condition': 'error' },
301
+ }
302
+ ```
303
+
304
+ Independent UI-Kit components may use a later matching off override while
305
+ retaining the other TypeScript/Svelte checks.
306
+
307
+ The current svelte-eslint-parser 1.10.1 models component generics as aliases to
308
+ their constraints (unknown when unconstrained). Conditional types over those
309
+ component generics may therefore resolve differently from a particular caller's
310
+ instantiation. This rule checks the parser-provided program, not every generic
311
+ instantiation; use an explicitly boolean view field for that boundary. It cannot
312
+ recover source type distinctions already erased by the parser.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { readyBooleanCondition } from './rules/ready-boolean-condition.js';
1
2
  import { singleIntentDispatch } from './rules/single-intent-dispatch.js';
2
3
  import { noComponentState } from './rules/no-component-state.js';
3
4
  import { requireFeatureStateSelector } from './rules/require-feature-state-selector.js';
@@ -6,6 +7,7 @@ export const plugin = {
6
7
  // typescript-eslint still types the legacy context members removed in ESLint 10.
7
8
  // At this public boundary the implementation uses only the current APIs.
8
9
  rules: {
10
+ 'ready-boolean-condition': readyBooleanCondition,
9
11
  'single-intent-dispatch': singleIntentDispatch,
10
12
  'no-component-state': noComponentState,
11
13
  'require-feature-state-selector': requireFeatureStateSelector,
@@ -0,0 +1,2 @@
1
+ import ts from 'typescript';
2
+ export declare const booleanType: (checker: ts.TypeChecker, type: ts.Type) => boolean;
@@ -0,0 +1,14 @@
1
+ import ts from 'typescript';
2
+ export const booleanType = (checker, type) => {
3
+ if (type.flags &
4
+ (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never)) {
5
+ return false;
6
+ }
7
+ if (type.flags & ts.TypeFlags.TypeParameter) {
8
+ const constraint = checker.getBaseConstraintOfType(type);
9
+ return (constraint !== undefined &&
10
+ constraint !== type &&
11
+ booleanType(checker, constraint));
12
+ }
13
+ return checker.isTypeAssignableTo(type, checker.getBooleanType());
14
+ };
@@ -1,2 +1,4 @@
1
1
  import { type TSESTree } from '@typescript-eslint/utils';
2
- export declare const memberKey: (node: TSESTree.MemberExpression) => string | undefined;
2
+ export declare const memberKey: (node: TSESTree.MemberExpression, { numeric }?: {
3
+ numeric?: boolean;
4
+ }) => string | undefined;
@@ -1,13 +1,18 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
- export const memberKey = (node) => {
2
+ export const memberKey = (node, { numeric = false } = {}) => {
3
3
  if (!node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
4
4
  return node.property.name;
5
5
  }
6
6
  if (node.computed && node.property.type === AST_NODE_TYPES.Literal) {
7
- return node.property.raw.startsWith("'") ||
8
- node.property.raw.startsWith('"')
9
- ? String(node.property.value)
10
- : undefined;
7
+ if (node.property.raw.startsWith("'") ||
8
+ node.property.raw.startsWith('"')) {
9
+ return String(node.property.value);
10
+ }
11
+ // Match numeric AST literals, including values such as 1e999; finiteness
12
+ // would change syntax classification. Existing callers remain names-only.
13
+ if (numeric && typeof node.property.value === 'number') {
14
+ return String(node.property.value);
15
+ }
11
16
  }
12
17
  return undefined;
13
18
  };
@@ -0,0 +1,2 @@
1
+ import { type TSESTree } from '@typescript-eslint/utils';
2
+ export declare const readyBooleanExpression: (node: TSESTree.Expression) => boolean;
@@ -0,0 +1,17 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { memberKey } from './member-key.js';
3
+ export const readyBooleanExpression = (node) => {
4
+ if (node.type === AST_NODE_TYPES.Identifier) {
5
+ return true;
6
+ }
7
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
8
+ return readyBooleanExpression(node.expression);
9
+ }
10
+ if (node.type !== AST_NODE_TYPES.MemberExpression) {
11
+ return false;
12
+ }
13
+ const staticKey = memberKey(node, { numeric: true }) !== undefined;
14
+ return (staticKey &&
15
+ node.object.type !== AST_NODE_TYPES.Super &&
16
+ readyBooleanExpression(node.object));
17
+ };
@@ -0,0 +1,2 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const readyBooleanCondition: ESLintUtils.RuleModule<"readyBoolean", [], unknown, ESLintUtils.RuleListener>;
@@ -0,0 +1,49 @@
1
+ import { AST_NODE_TYPES, ESLintUtils, } from '@typescript-eslint/utils';
2
+ import { readyBooleanExpression } from '../lib/ready-boolean-expression.js';
3
+ import { booleanType } from '../lib/boolean-type.js';
4
+ export const readyBooleanCondition = ESLintUtils.RuleCreator.withoutDocs({
5
+ meta: {
6
+ type: 'suggestion',
7
+ docs: {
8
+ description: 'Require ready boolean reads in Svelte if/else-if conditions.',
9
+ },
10
+ schema: [],
11
+ messages: {
12
+ readyBoolean: 'Prepare a named boolean in a presenter and read it here instead of computing a condition or using truthiness.',
13
+ },
14
+ },
15
+ defaultOptions: [],
16
+ create(context) {
17
+ if (!context.filename.endsWith('.svelte')) {
18
+ return {};
19
+ }
20
+ let checkType;
21
+ return {
22
+ SvelteIfBlock(node) {
23
+ const expression = node.expression;
24
+ if (expression.type === AST_NODE_TYPES.Literal &&
25
+ (expression.value === true || expression.value === false)) {
26
+ return;
27
+ }
28
+ if (!readyBooleanExpression(expression)) {
29
+ context.report({ node: expression, messageId: 'readyBoolean' });
30
+ return;
31
+ }
32
+ if (!checkType) {
33
+ const services = ESLintUtils.getParserServices(context);
34
+ const options = services.program.getCompilerOptions();
35
+ if (!(options.strictNullChecks ?? options.strict ?? false)) {
36
+ throw new Error('gost/ready-boolean-condition requires strictNullChecks (or strict) for typed conditions.');
37
+ }
38
+ const checker = services.program.getTypeChecker();
39
+ checkType = (condition) => {
40
+ return booleanType(checker, services.getTypeAtLocation(condition));
41
+ };
42
+ }
43
+ if (!checkType(expression)) {
44
+ context.report({ node: expression, messageId: 'readyBoolean' });
45
+ }
46
+ },
47
+ };
48
+ },
49
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gost-kit/eslint-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "GOST architecture rules for agent-assisted frontend development.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,8 +43,8 @@
43
43
  "svelte-eslint-parser": "1.10.1",
44
44
  "typescript-eslint": "8.72.1",
45
45
  "svelte": "5.57.2",
46
- "@gost-kit/common": "0.2.1",
47
- "@gost-kit/svelte": "0.1.1",
46
+ "@gost-kit/common": "0.3.0",
47
+ "@gost-kit/svelte": "0.2.0",
48
48
  "@typescript-eslint/rule-tester": "8.72.1"
49
49
  },
50
50
  "dependencies": {