@llui/compiler 0.3.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/LICENSE +21 -0
- package/dist/accessor-resolver.d.ts +58 -0
- package/dist/accessor-resolver.d.ts.map +1 -0
- package/dist/accessor-resolver.js +119 -0
- package/dist/accessor-resolver.js.map +1 -0
- package/dist/binding-descriptors.d.ts +105 -0
- package/dist/binding-descriptors.d.ts.map +1 -0
- package/dist/binding-descriptors.js +340 -0
- package/dist/binding-descriptors.js.map +1 -0
- package/dist/collect-deps.d.ts +49 -0
- package/dist/collect-deps.d.ts.map +1 -0
- package/dist/collect-deps.js +444 -0
- package/dist/collect-deps.js.map +1 -0
- package/dist/compiler-cache.d.ts +20 -0
- package/dist/compiler-cache.d.ts.map +1 -0
- package/dist/compiler-cache.js +20 -0
- package/dist/compiler-cache.js.map +1 -0
- package/dist/cross-file-resolver.d.ts +109 -0
- package/dist/cross-file-resolver.d.ts.map +1 -0
- package/dist/cross-file-resolver.js +530 -0
- package/dist/cross-file-resolver.js.map +1 -0
- package/dist/cross-file-walker.d.ts +63 -0
- package/dist/cross-file-walker.d.ts.map +1 -0
- package/dist/cross-file-walker.js +516 -0
- package/dist/cross-file-walker.js.map +1 -0
- package/dist/diagnostic.d.ts +76 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +59 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/introspection-factory.d.ts +54 -0
- package/dist/introspection-factory.d.ts.map +1 -0
- package/dist/introspection-factory.js +46 -0
- package/dist/introspection-factory.js.map +1 -0
- package/dist/manifest.d.ts +144 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +209 -0
- package/dist/manifest.js.map +1 -0
- package/dist/module.d.ts +222 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +256 -0
- package/dist/module.js.map +1 -0
- package/dist/modules/_element-helpers.d.ts +4 -0
- package/dist/modules/_element-helpers.d.ts.map +1 -0
- package/dist/modules/_element-helpers.js +138 -0
- package/dist/modules/_element-helpers.js.map +1 -0
- package/dist/modules/_msg-variants.d.ts +10 -0
- package/dist/modules/_msg-variants.d.ts.map +1 -0
- package/dist/modules/_msg-variants.js +97 -0
- package/dist/modules/_msg-variants.js.map +1 -0
- package/dist/modules/_shared.d.ts +16 -0
- package/dist/modules/_shared.d.ts.map +1 -0
- package/dist/modules/_shared.js +30 -0
- package/dist/modules/_shared.js.map +1 -0
- package/dist/modules/accessibility.d.ts +3 -0
- package/dist/modules/accessibility.d.ts.map +1 -0
- package/dist/modules/accessibility.js +82 -0
- package/dist/modules/accessibility.js.map +1 -0
- package/dist/modules/accessor-side-effect.d.ts +3 -0
- package/dist/modules/accessor-side-effect.d.ts.map +1 -0
- package/dist/modules/accessor-side-effect.js +113 -0
- package/dist/modules/accessor-side-effect.js.map +1 -0
- package/dist/modules/agent-emits-drift.d.ts +3 -0
- package/dist/modules/agent-emits-drift.d.ts.map +1 -0
- package/dist/modules/agent-emits-drift.js +158 -0
- package/dist/modules/agent-emits-drift.js.map +1 -0
- package/dist/modules/agent-example-on-payload.d.ts +3 -0
- package/dist/modules/agent-example-on-payload.d.ts.map +1 -0
- package/dist/modules/agent-example-on-payload.js +53 -0
- package/dist/modules/agent-example-on-payload.js.map +1 -0
- package/dist/modules/agent-exclusive-annotations.d.ts +3 -0
- package/dist/modules/agent-exclusive-annotations.d.ts.map +1 -0
- package/dist/modules/agent-exclusive-annotations.js +68 -0
- package/dist/modules/agent-exclusive-annotations.js.map +1 -0
- package/dist/modules/agent-missing-intent.d.ts +3 -0
- package/dist/modules/agent-missing-intent.d.ts.map +1 -0
- package/dist/modules/agent-missing-intent.js +47 -0
- package/dist/modules/agent-missing-intent.js.map +1 -0
- package/dist/modules/agent-msg-resolvable.d.ts +3 -0
- package/dist/modules/agent-msg-resolvable.d.ts.map +1 -0
- package/dist/modules/agent-msg-resolvable.js +161 -0
- package/dist/modules/agent-msg-resolvable.js.map +1 -0
- package/dist/modules/agent-nonextractable-handler.d.ts +3 -0
- package/dist/modules/agent-nonextractable-handler.d.ts.map +1 -0
- package/dist/modules/agent-nonextractable-handler.js +127 -0
- package/dist/modules/agent-nonextractable-handler.js.map +1 -0
- package/dist/modules/agent-optional-field-undocumented.d.ts +3 -0
- package/dist/modules/agent-optional-field-undocumented.d.ts.map +1 -0
- package/dist/modules/agent-optional-field-undocumented.js +67 -0
- package/dist/modules/agent-optional-field-undocumented.js.map +1 -0
- package/dist/modules/agent-tagsend-translator-missing.d.ts +3 -0
- package/dist/modules/agent-tagsend-translator-missing.d.ts.map +1 -0
- package/dist/modules/agent-tagsend-translator-missing.js +58 -0
- package/dist/modules/agent-tagsend-translator-missing.js.map +1 -0
- package/dist/modules/agent-warning-on-confirm.d.ts +3 -0
- package/dist/modules/agent-warning-on-confirm.d.ts.map +1 -0
- package/dist/modules/agent-warning-on-confirm.js +46 -0
- package/dist/modules/agent-warning-on-confirm.js.map +1 -0
- package/dist/modules/async-update.d.ts +3 -0
- package/dist/modules/async-update.d.ts.map +1 -0
- package/dist/modules/async-update.js +86 -0
- package/dist/modules/async-update.js.map +1 -0
- package/dist/modules/binding-descriptors.d.ts +4 -0
- package/dist/modules/binding-descriptors.d.ts.map +1 -0
- package/dist/modules/binding-descriptors.js +48 -0
- package/dist/modules/binding-descriptors.js.map +1 -0
- package/dist/modules/bitmask-overflow.d.ts +3 -0
- package/dist/modules/bitmask-overflow.d.ts.map +1 -0
- package/dist/modules/bitmask-overflow.js +152 -0
- package/dist/modules/bitmask-overflow.js.map +1 -0
- package/dist/modules/compiler-stamp.d.ts +3 -0
- package/dist/modules/compiler-stamp.d.ts.map +1 -0
- package/dist/modules/compiler-stamp.js +44 -0
- package/dist/modules/compiler-stamp.js.map +1 -0
- package/dist/modules/component-meta.d.ts +3 -0
- package/dist/modules/component-meta.d.ts.map +1 -0
- package/dist/modules/component-meta.js +44 -0
- package/dist/modules/component-meta.js.map +1 -0
- package/dist/modules/controlled-input.d.ts +3 -0
- package/dist/modules/controlled-input.d.ts.map +1 -0
- package/dist/modules/controlled-input.js +68 -0
- package/dist/modules/controlled-input.js.map +1 -0
- package/dist/modules/core-synthesis.d.ts +18 -0
- package/dist/modules/core-synthesis.d.ts.map +1 -0
- package/dist/modules/core-synthesis.js +748 -0
- package/dist/modules/core-synthesis.js.map +1 -0
- package/dist/modules/direct-state-in-view.d.ts +3 -0
- package/dist/modules/direct-state-in-view.d.ts.map +1 -0
- package/dist/modules/direct-state-in-view.js +103 -0
- package/dist/modules/direct-state-in-view.js.map +1 -0
- package/dist/modules/each-closure-violation.d.ts +3 -0
- package/dist/modules/each-closure-violation.d.ts.map +1 -0
- package/dist/modules/each-closure-violation.js +255 -0
- package/dist/modules/each-closure-violation.js.map +1 -0
- package/dist/modules/each-memo.d.ts +15 -0
- package/dist/modules/each-memo.d.ts.map +1 -0
- package/dist/modules/each-memo.js +115 -0
- package/dist/modules/each-memo.js.map +1 -0
- package/dist/modules/effect-without-handler.d.ts +3 -0
- package/dist/modules/effect-without-handler.d.ts.map +1 -0
- package/dist/modules/effect-without-handler.js +92 -0
- package/dist/modules/effect-without-handler.js.map +1 -0
- package/dist/modules/element-rewrite.d.ts +22 -0
- package/dist/modules/element-rewrite.d.ts.map +1 -0
- package/dist/modules/element-rewrite.js +1017 -0
- package/dist/modules/element-rewrite.js.map +1 -0
- package/dist/modules/empty-props.d.ts +3 -0
- package/dist/modules/empty-props.d.ts.map +1 -0
- package/dist/modules/empty-props.js +50 -0
- package/dist/modules/empty-props.js.map +1 -0
- package/dist/modules/exhaustive-effect-handling.d.ts +3 -0
- package/dist/modules/exhaustive-effect-handling.d.ts.map +1 -0
- package/dist/modules/exhaustive-effect-handling.js +61 -0
- package/dist/modules/exhaustive-effect-handling.js.map +1 -0
- package/dist/modules/exhaustive-update.d.ts +3 -0
- package/dist/modules/exhaustive-update.d.ts.map +1 -0
- package/dist/modules/exhaustive-update.js +146 -0
- package/dist/modules/exhaustive-update.js.map +1 -0
- package/dist/modules/forgotten-spread.d.ts +3 -0
- package/dist/modules/forgotten-spread.d.ts.map +1 -0
- package/dist/modules/forgotten-spread.js +51 -0
- package/dist/modules/forgotten-spread.js.map +1 -0
- package/dist/modules/form-boilerplate.d.ts +3 -0
- package/dist/modules/form-boilerplate.d.ts.map +1 -0
- package/dist/modules/form-boilerplate.js +101 -0
- package/dist/modules/form-boilerplate.js.map +1 -0
- package/dist/modules/imperative-dom-in-view.d.ts +3 -0
- package/dist/modules/imperative-dom-in-view.d.ts.map +1 -0
- package/dist/modules/imperative-dom-in-view.js +123 -0
- package/dist/modules/imperative-dom-in-view.js.map +1 -0
- package/dist/modules/item-dedup.d.ts +7 -0
- package/dist/modules/item-dedup.d.ts.map +1 -0
- package/dist/modules/item-dedup.js +204 -0
- package/dist/modules/item-dedup.js.map +1 -0
- package/dist/modules/map-on-state-array.d.ts +3 -0
- package/dist/modules/map-on-state-array.d.ts.map +1 -0
- package/dist/modules/map-on-state-array.js +84 -0
- package/dist/modules/map-on-state-array.js.map +1 -0
- package/dist/modules/mask-legend.d.ts +10 -0
- package/dist/modules/mask-legend.d.ts.map +1 -0
- package/dist/modules/mask-legend.js +50 -0
- package/dist/modules/mask-legend.js.map +1 -0
- package/dist/modules/missing-memo.d.ts +3 -0
- package/dist/modules/missing-memo.d.ts.map +1 -0
- package/dist/modules/missing-memo.js +114 -0
- package/dist/modules/missing-memo.js.map +1 -0
- package/dist/modules/msg-annotations.d.ts +9 -0
- package/dist/modules/msg-annotations.d.ts.map +1 -0
- package/dist/modules/msg-annotations.js +54 -0
- package/dist/modules/msg-annotations.js.map +1 -0
- package/dist/modules/msg-schema.d.ts +10 -0
- package/dist/modules/msg-schema.d.ts.map +1 -0
- package/dist/modules/msg-schema.js +70 -0
- package/dist/modules/msg-schema.js.map +1 -0
- package/dist/modules/namespace-import.d.ts +3 -0
- package/dist/modules/namespace-import.d.ts.map +1 -0
- package/dist/modules/namespace-import.js +80 -0
- package/dist/modules/namespace-import.js.map +1 -0
- package/dist/modules/nested-send-in-update.d.ts +3 -0
- package/dist/modules/nested-send-in-update.d.ts.map +1 -0
- package/dist/modules/nested-send-in-update.js +77 -0
- package/dist/modules/nested-send-in-update.js.map +1 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.d.ts +3 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.d.ts.map +1 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.js +100 -0
- package/dist/modules/no-barrel-import-when-subpath-exists.js.map +1 -0
- package/dist/modules/no-eager-item-accessor.d.ts +3 -0
- package/dist/modules/no-eager-item-accessor.d.ts.map +1 -0
- package/dist/modules/no-eager-item-accessor.js +74 -0
- package/dist/modules/no-eager-item-accessor.js.map +1 -0
- package/dist/modules/no-let-reactive-accessor.d.ts +3 -0
- package/dist/modules/no-let-reactive-accessor.d.ts.map +1 -0
- package/dist/modules/no-let-reactive-accessor.js +227 -0
- package/dist/modules/no-let-reactive-accessor.js.map +1 -0
- package/dist/modules/no-list-render-in-sample.d.ts +3 -0
- package/dist/modules/no-list-render-in-sample.d.ts.map +1 -0
- package/dist/modules/no-list-render-in-sample.js +89 -0
- package/dist/modules/no-list-render-in-sample.js.map +1 -0
- package/dist/modules/no-sample-in-accessor.d.ts +3 -0
- package/dist/modules/no-sample-in-accessor.d.ts.map +1 -0
- package/dist/modules/no-sample-in-accessor.js +141 -0
- package/dist/modules/no-sample-in-accessor.js.map +1 -0
- package/dist/modules/no-sample-in-reactive-position.d.ts +3 -0
- package/dist/modules/no-sample-in-reactive-position.d.ts.map +1 -0
- package/dist/modules/no-sample-in-reactive-position.js +72 -0
- package/dist/modules/no-sample-in-reactive-position.js.map +1 -0
- package/dist/modules/pure-update-function.d.ts +3 -0
- package/dist/modules/pure-update-function.d.ts.map +1 -0
- package/dist/modules/pure-update-function.js +127 -0
- package/dist/modules/pure-update-function.js.map +1 -0
- package/dist/modules/reactive-paths.d.ts +3 -0
- package/dist/modules/reactive-paths.d.ts.map +1 -0
- package/dist/modules/reactive-paths.js +77 -0
- package/dist/modules/reactive-paths.js.map +1 -0
- package/dist/modules/row-factory.d.ts +12 -0
- package/dist/modules/row-factory.d.ts.map +1 -0
- package/dist/modules/row-factory.js +385 -0
- package/dist/modules/row-factory.js.map +1 -0
- package/dist/modules/schema-hash.d.ts +15 -0
- package/dist/modules/schema-hash.d.ts.map +1 -0
- package/dist/modules/schema-hash.js +70 -0
- package/dist/modules/schema-hash.js.map +1 -0
- package/dist/modules/spread-in-children.d.ts +3 -0
- package/dist/modules/spread-in-children.d.ts.map +1 -0
- package/dist/modules/spread-in-children.js +144 -0
- package/dist/modules/spread-in-children.js.map +1 -0
- package/dist/modules/state-mutation.d.ts +3 -0
- package/dist/modules/state-mutation.d.ts.map +1 -0
- package/dist/modules/state-mutation.js +138 -0
- package/dist/modules/state-mutation.js.map +1 -0
- package/dist/modules/state-schema.d.ts +8 -0
- package/dist/modules/state-schema.d.ts.map +1 -0
- package/dist/modules/state-schema.js +55 -0
- package/dist/modules/state-schema.js.map +1 -0
- package/dist/modules/static-items.d.ts +3 -0
- package/dist/modules/static-items.d.ts.map +1 -0
- package/dist/modules/static-items.js +125 -0
- package/dist/modules/static-items.js.map +1 -0
- package/dist/modules/static-on.d.ts +3 -0
- package/dist/modules/static-on.d.ts.map +1 -0
- package/dist/modules/static-on.js +100 -0
- package/dist/modules/static-on.js.map +1 -0
- package/dist/modules/string-effect-callback.d.ts +3 -0
- package/dist/modules/string-effect-callback.d.ts.map +1 -0
- package/dist/modules/string-effect-callback.js +50 -0
- package/dist/modules/string-effect-callback.js.map +1 -0
- package/dist/modules/structural-mask.d.ts +8 -0
- package/dist/modules/structural-mask.d.ts.map +1 -0
- package/dist/modules/structural-mask.js +76 -0
- package/dist/modules/structural-mask.js.map +1 -0
- package/dist/modules/subapp-requires-reason.d.ts +3 -0
- package/dist/modules/subapp-requires-reason.d.ts.map +1 -0
- package/dist/modules/subapp-requires-reason.js +129 -0
- package/dist/modules/subapp-requires-reason.js.map +1 -0
- package/dist/modules/text-mask.d.ts +12 -0
- package/dist/modules/text-mask.d.ts.map +1 -0
- package/dist/modules/text-mask.js +63 -0
- package/dist/modules/text-mask.js.map +1 -0
- package/dist/modules/view-bag-import.d.ts +3 -0
- package/dist/modules/view-bag-import.d.ts.map +1 -0
- package/dist/modules/view-bag-import.js +80 -0
- package/dist/modules/view-bag-import.js.map +1 -0
- package/dist/msg-annotations.d.ts +104 -0
- package/dist/msg-annotations.d.ts.map +1 -0
- package/dist/msg-annotations.js +242 -0
- package/dist/msg-annotations.js.map +1 -0
- package/dist/msg-schema.d.ts +130 -0
- package/dist/msg-schema.d.ts.map +1 -0
- package/dist/msg-schema.js +770 -0
- package/dist/msg-schema.js.map +1 -0
- package/dist/schema-hash.d.ts +16 -0
- package/dist/schema-hash.d.ts.map +1 -0
- package/dist/schema-hash.js +31 -0
- package/dist/schema-hash.js.map +1 -0
- package/dist/state-schema.d.ts +41 -0
- package/dist/state-schema.d.ts.map +1 -0
- package/dist/state-schema.js +156 -0
- package/dist/state-schema.js.map +1 -0
- package/dist/transform.d.ts +109 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/transform.js +1390 -0
- package/dist/transform.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-barrel-import-when-subpath-exists.d.ts","sourceRoot":"","sources":["../../src/modules/no-barrel-import-when-subpath-exists.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA+BlD,wBAAgB,qCAAqC,IAAI,cAAc,CAqDtE"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// `no-barrel-import-when-subpath-exists` — errors when a name is imported
|
|
2
|
+
// from an `@llui/*` package's barrel and the package ships a sub-path
|
|
3
|
+
// export for that name. Sub-paths skip the barrel parse — smaller
|
|
4
|
+
// bundle, faster cold builds. Migrated from
|
|
5
|
+
// `@llui/eslint-plugin/src/rules/no-barrel-import-when-subpath-exists.ts`.
|
|
6
|
+
// Autofix dropped per the migration plan; the error includes the exact
|
|
7
|
+
// split-import replacement.
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { dirname } from 'node:path';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
12
|
+
const TARGETS = new Set(['@llui/components']);
|
|
13
|
+
const subpathCache = new Map();
|
|
14
|
+
function loadSubpaths(packageName, fromDir) {
|
|
15
|
+
const cacheKey = `${packageName}@${fromDir}`;
|
|
16
|
+
const cached = subpathCache.get(cacheKey);
|
|
17
|
+
if (cached !== undefined)
|
|
18
|
+
return cached;
|
|
19
|
+
let result = null;
|
|
20
|
+
try {
|
|
21
|
+
const req = createRequire(`${fromDir}/index.js`);
|
|
22
|
+
const pkgPath = req.resolve(`${packageName}/package.json`);
|
|
23
|
+
const pkg = req(pkgPath);
|
|
24
|
+
if (pkg.exports && typeof pkg.exports === 'object') {
|
|
25
|
+
const subs = new Set();
|
|
26
|
+
for (const key of Object.keys(pkg.exports)) {
|
|
27
|
+
if (key === '.' || key.includes('*'))
|
|
28
|
+
continue;
|
|
29
|
+
if (!key.startsWith('./'))
|
|
30
|
+
continue;
|
|
31
|
+
subs.add(key.slice(2));
|
|
32
|
+
}
|
|
33
|
+
result = subs;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
result = null;
|
|
38
|
+
}
|
|
39
|
+
subpathCache.set(cacheKey, result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
export function noBarrelImportWhenSubpathExistsModule() {
|
|
43
|
+
return {
|
|
44
|
+
name: 'no-barrel-import-when-subpath-exists',
|
|
45
|
+
compilerVersion: '^0.3.0',
|
|
46
|
+
diagnostics: [
|
|
47
|
+
{
|
|
48
|
+
id: 'llui/no-barrel-import-when-subpath-exists',
|
|
49
|
+
description: 'Barrel import has a sub-path export — use the sub-path for smaller bundles.',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
visitors: {
|
|
53
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
54
|
+
const visited = node;
|
|
55
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
56
|
+
// Look up sub-paths relative to the file's directory. If the
|
|
57
|
+
// package isn't installed (test fixture, sandboxed env), the
|
|
58
|
+
// helper returns null and the rule silently skips.
|
|
59
|
+
const fromDir = sf.fileName ? dirname(sf.fileName) : process.cwd();
|
|
60
|
+
for (const stmt of sf.statements) {
|
|
61
|
+
if (!ts.isImportDeclaration(stmt))
|
|
62
|
+
continue;
|
|
63
|
+
if (!ts.isStringLiteral(stmt.moduleSpecifier))
|
|
64
|
+
continue;
|
|
65
|
+
const src = stmt.moduleSpecifier.text;
|
|
66
|
+
if (!TARGETS.has(src))
|
|
67
|
+
continue;
|
|
68
|
+
const clause = stmt.importClause;
|
|
69
|
+
if (!clause || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
|
|
70
|
+
continue;
|
|
71
|
+
const subpaths = loadSubpaths(src, fromDir);
|
|
72
|
+
if (!subpaths)
|
|
73
|
+
continue;
|
|
74
|
+
for (const spec of clause.namedBindings.elements) {
|
|
75
|
+
const importedName = (spec.propertyName ?? spec.name).text;
|
|
76
|
+
if (!subpaths.has(importedName))
|
|
77
|
+
continue;
|
|
78
|
+
const localName = spec.name.text;
|
|
79
|
+
const fixLine = importedName === localName
|
|
80
|
+
? `import { ${localName} } from '${src}/${importedName}'`
|
|
81
|
+
: `import { ${importedName} as ${localName} } from '${src}/${importedName}'`;
|
|
82
|
+
ctx.reportDiagnostic({
|
|
83
|
+
id: 'llui/no-barrel-import-when-subpath-exists',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
category: 'perf',
|
|
86
|
+
message: `Import \`${importedName}\` from the sub-path '${src}/${importedName}' instead of ` +
|
|
87
|
+
`the barrel '${src}'. Sub-path imports skip the barrel parse — smaller bundle, ` +
|
|
88
|
+
`faster cold builds. Fix: \`${fixLine}\` (split off from the existing barrel import).`,
|
|
89
|
+
location: {
|
|
90
|
+
file: sf.fileName,
|
|
91
|
+
range: rangeFromOffsets(sf.text, spec.getStart(sf), spec.getEnd()),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=no-barrel-import-when-subpath-exists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-barrel-import-when-subpath-exists.js","sourceRoot":"","sources":["../../src/modules/no-barrel-import-when-subpath-exists.ts"],"names":[],"mappings":"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","sourcesContent":["// `no-barrel-import-when-subpath-exists` — errors when a name is imported\n// from an `@llui/*` package's barrel and the package ships a sub-path\n// export for that name. Sub-paths skip the barrel parse — smaller\n// bundle, faster cold builds. Migrated from\n// `@llui/eslint-plugin/src/rules/no-barrel-import-when-subpath-exists.ts`.\n// Autofix dropped per the migration plan; the error includes the exact\n// split-import replacement.\n\nimport { createRequire } from 'node:module'\nimport { dirname } from 'node:path'\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\n\nconst TARGETS = new Set(['@llui/components'])\ntype SubpathSet = Set<string> | null\nconst subpathCache = new Map<string, SubpathSet>()\n\nfunction loadSubpaths(packageName: string, fromDir: string): SubpathSet {\n const cacheKey = `${packageName}@${fromDir}`\n const cached = subpathCache.get(cacheKey)\n if (cached !== undefined) return cached\n let result: SubpathSet = null\n try {\n const req = createRequire(`${fromDir}/index.js`)\n const pkgPath = req.resolve(`${packageName}/package.json`)\n const pkg = req(pkgPath) as { exports?: Record<string, unknown> }\n if (pkg.exports && typeof pkg.exports === 'object') {\n const subs = new Set<string>()\n for (const key of Object.keys(pkg.exports)) {\n if (key === '.' || key.includes('*')) continue\n if (!key.startsWith('./')) continue\n subs.add(key.slice(2))\n }\n result = subs\n }\n } catch {\n result = null\n }\n subpathCache.set(cacheKey, result)\n return result\n}\n\nexport function noBarrelImportWhenSubpathExistsModule(): CompilerModule {\n return {\n name: 'no-barrel-import-when-subpath-exists',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-barrel-import-when-subpath-exists',\n description: 'Barrel import has a sub-path export — use the sub-path for smaller bundles.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n // Look up sub-paths relative to the file's directory. If the\n // package isn't installed (test fixture, sandboxed env), the\n // helper returns null and the rule silently skips.\n const fromDir = sf.fileName ? dirname(sf.fileName) : process.cwd()\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt)) continue\n if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue\n const src = stmt.moduleSpecifier.text\n if (!TARGETS.has(src)) continue\n const clause = stmt.importClause\n if (!clause || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue\n const subpaths = loadSubpaths(src, fromDir)\n if (!subpaths) continue\n for (const spec of clause.namedBindings.elements) {\n const importedName = (spec.propertyName ?? spec.name).text\n if (!subpaths.has(importedName)) continue\n const localName = spec.name.text\n const fixLine =\n importedName === localName\n ? `import { ${localName} } from '${src}/${importedName}'`\n : `import { ${importedName} as ${localName} } from '${src}/${importedName}'`\n ctx.reportDiagnostic({\n id: 'llui/no-barrel-import-when-subpath-exists',\n severity: 'error',\n category: 'perf',\n message:\n `Import \\`${importedName}\\` from the sub-path '${src}/${importedName}' instead of ` +\n `the barrel '${src}'. Sub-path imports skip the barrel parse — smaller bundle, ` +\n `faster cold builds. Fix: \\`${fixLine}\\` (split off from the existing barrel import).`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, spec.getStart(sf), spec.getEnd()),\n },\n })\n }\n }\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-eager-item-accessor.d.ts","sourceRoot":"","sources":["../../src/modules/no-eager-item-accessor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAYlD,wBAAgB,yBAAyB,IAAI,cAAc,CAyD1D"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// `no-eager-item-accessor` — errors when an each() `item.<prop>()` is
|
|
2
|
+
// invoked eagerly inside `text(...)` / `unsafeHtml(...)`. The
|
|
3
|
+
// ItemAccessor returns a `() => V`; passing the call result captures
|
|
4
|
+
// a static value, so the cell never updates when the row data
|
|
5
|
+
// changes in place. Pass the accessor itself instead (drop the `()`).
|
|
6
|
+
// Migrated from `@llui/eslint-plugin/src/rules/no-eager-item-accessor.ts`.
|
|
7
|
+
import ts from 'typescript';
|
|
8
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
9
|
+
const EAGER_TARGETS = new Set(['text', 'unsafeHtml']);
|
|
10
|
+
/** True when `node` is `item.<prop>()` — bare `item` identifier root. */
|
|
11
|
+
function isItemMemberCall(node) {
|
|
12
|
+
if (!ts.isCallExpression(node))
|
|
13
|
+
return false;
|
|
14
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
15
|
+
return false;
|
|
16
|
+
const obj = node.expression.expression;
|
|
17
|
+
return ts.isIdentifier(obj) && obj.text === 'item';
|
|
18
|
+
}
|
|
19
|
+
export function noEagerItemAccessorModule() {
|
|
20
|
+
return {
|
|
21
|
+
name: 'no-eager-item-accessor',
|
|
22
|
+
compilerVersion: '^0.3.0',
|
|
23
|
+
diagnostics: [
|
|
24
|
+
{
|
|
25
|
+
id: 'llui/no-eager-item-accessor',
|
|
26
|
+
description: 'Eager call of an ItemAccessor (`text(item.x())`) captures a static value — drop the `()` to pass the accessor.',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
visitors: {
|
|
30
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
31
|
+
const visited = node;
|
|
32
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
33
|
+
const walk = (n) => {
|
|
34
|
+
if (ts.isCallExpression(n)) {
|
|
35
|
+
// Bare `text(...)` or member-form `h.text(...)`.
|
|
36
|
+
let calleeName;
|
|
37
|
+
if (ts.isIdentifier(n.expression) && EAGER_TARGETS.has(n.expression.text)) {
|
|
38
|
+
calleeName = n.expression.text;
|
|
39
|
+
}
|
|
40
|
+
else if (ts.isPropertyAccessExpression(n.expression) &&
|
|
41
|
+
ts.isIdentifier(n.expression.name) &&
|
|
42
|
+
EAGER_TARGETS.has(n.expression.name.text)) {
|
|
43
|
+
calleeName = n.expression.name.text;
|
|
44
|
+
}
|
|
45
|
+
if (calleeName) {
|
|
46
|
+
const arg = n.arguments[0];
|
|
47
|
+
if (arg && isItemMemberCall(arg)) {
|
|
48
|
+
const memberArg = arg;
|
|
49
|
+
const member = memberArg.expression;
|
|
50
|
+
const propText = ts.isIdentifier(member.name)
|
|
51
|
+
? `item.${member.name.text}`
|
|
52
|
+
: 'item.<prop>';
|
|
53
|
+
ctx.reportDiagnostic({
|
|
54
|
+
id: 'llui/no-eager-item-accessor',
|
|
55
|
+
severity: 'error',
|
|
56
|
+
category: 'reactivity',
|
|
57
|
+
message: `\`${calleeName}(${propText}())\` reads the item value once at view-construction and never updates when the row data changes in place. ` +
|
|
58
|
+
`Drop the \`()\` to pass the accessor itself: \`${calleeName}(${propText})\`. The runtime detects the zero-arg form and re-reads on every commit.`,
|
|
59
|
+
location: {
|
|
60
|
+
file: sf.fileName,
|
|
61
|
+
range: rangeFromOffsets(sf.text, memberArg.getStart(sf), memberArg.getEnd()),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
ts.forEachChild(n, walk);
|
|
68
|
+
};
|
|
69
|
+
walk(sf);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=no-eager-item-accessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-eager-item-accessor.js","sourceRoot":"","sources":["../../src/modules/no-eager-item-accessor.ts"],"names":[],"mappings":"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","sourcesContent":["// `no-eager-item-accessor` — errors when an each() `item.<prop>()` is\n// invoked eagerly inside `text(...)` / `unsafeHtml(...)`. The\n// ItemAccessor returns a `() => V`; passing the call result captures\n// a static value, so the cell never updates when the row data\n// changes in place. Pass the accessor itself instead (drop the `()`).\n// Migrated from `@llui/eslint-plugin/src/rules/no-eager-item-accessor.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\n\nconst EAGER_TARGETS = new Set(['text', 'unsafeHtml'])\n\n/** True when `node` is `item.<prop>()` — bare `item` identifier root. */\nfunction isItemMemberCall(node: ts.Node): node is ts.CallExpression {\n if (!ts.isCallExpression(node)) return false\n if (!ts.isPropertyAccessExpression(node.expression)) return false\n const obj = node.expression.expression\n return ts.isIdentifier(obj) && obj.text === 'item'\n}\n\nexport function noEagerItemAccessorModule(): CompilerModule {\n return {\n name: 'no-eager-item-accessor',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-eager-item-accessor',\n description:\n 'Eager call of an ItemAccessor (`text(item.x())`) captures a static value — drop the `()` to pass the accessor.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n const walk = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n // Bare `text(...)` or member-form `h.text(...)`.\n let calleeName: string | undefined\n if (ts.isIdentifier(n.expression) && EAGER_TARGETS.has(n.expression.text)) {\n calleeName = n.expression.text\n } else if (\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n EAGER_TARGETS.has(n.expression.name.text)\n ) {\n calleeName = n.expression.name.text\n }\n if (calleeName) {\n const arg = n.arguments[0]\n if (arg && isItemMemberCall(arg)) {\n const memberArg = arg\n const member = memberArg.expression as ts.PropertyAccessExpression\n const propText = ts.isIdentifier(member.name)\n ? `item.${member.name.text}`\n : 'item.<prop>'\n ctx.reportDiagnostic({\n id: 'llui/no-eager-item-accessor',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`${calleeName}(${propText}())\\` reads the item value once at view-construction and never updates when the row data changes in place. ` +\n `Drop the \\`()\\` to pass the accessor itself: \\`${calleeName}(${propText})\\`. The runtime detects the zero-arg form and re-reads on every commit.`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, memberArg.getStart(sf), memberArg.getEnd()),\n },\n })\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-let-reactive-accessor.d.ts","sourceRoot":"","sources":["../../src/modules/no-let-reactive-accessor.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAwHlD,wBAAgB,2BAA2B,IAAI,cAAc,CAuG5D"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// `no-let-reactive-accessor` — errors when a top-level `let` / `var`
|
|
2
|
+
// binding with a callable initializer is referenced at a reactive
|
|
3
|
+
// accessor position. The compiler's `resolveAccessorBody` refuses to
|
|
4
|
+
// follow `let` / `var` bindings (reassignment would invalidate any
|
|
5
|
+
// compile-time mask analysis), so the binding silently falls back to
|
|
6
|
+
// FULL_MASK at runtime — correct but suboptimal. Forcing `const`
|
|
7
|
+
// preserves the precise-mask optimization. Migrated from
|
|
8
|
+
// `@llui/eslint-plugin/src/rules/no-let-reactive-accessor.ts`.
|
|
9
|
+
//
|
|
10
|
+
// Note: autofix dropped (per the lint→compiler migration plan); the
|
|
11
|
+
// error message includes the exact `let`/`var` → `const` substitution
|
|
12
|
+
// so an LLM consuming the error can apply it directly.
|
|
13
|
+
import ts from 'typescript';
|
|
14
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
15
|
+
import { ELEMENT_HELPERS } from './_element-helpers.js';
|
|
16
|
+
const REACTIVE_API_NAMES = new Set([
|
|
17
|
+
...ELEMENT_HELPERS,
|
|
18
|
+
'each',
|
|
19
|
+
'branch',
|
|
20
|
+
'scope',
|
|
21
|
+
'show',
|
|
22
|
+
'memo',
|
|
23
|
+
'portal',
|
|
24
|
+
'foreign',
|
|
25
|
+
'child',
|
|
26
|
+
'errorBoundary',
|
|
27
|
+
]);
|
|
28
|
+
const FIRST_ARG_BINDING_HELPERS = new Set(['text', 'unsafeHtml', 'memo']);
|
|
29
|
+
function isAtReactivePosition(node) {
|
|
30
|
+
const parent = node.parent;
|
|
31
|
+
if (!parent)
|
|
32
|
+
return false;
|
|
33
|
+
if (ts.isCallExpression(parent) && parent.arguments[0] === node) {
|
|
34
|
+
if (ts.isIdentifier(parent.expression)) {
|
|
35
|
+
const name = parent.expression.text;
|
|
36
|
+
if (name === 'item' || name === 'sample')
|
|
37
|
+
return false;
|
|
38
|
+
return FIRST_ARG_BINDING_HELPERS.has(name) || REACTIVE_API_NAMES.has(name);
|
|
39
|
+
}
|
|
40
|
+
if (ts.isPropertyAccessExpression(parent.expression) &&
|
|
41
|
+
ts.isIdentifier(parent.expression.name)) {
|
|
42
|
+
return FIRST_ARG_BINDING_HELPERS.has(parent.expression.name.text);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (ts.isPropertyAssignment(parent) && parent.initializer === node) {
|
|
47
|
+
if (!ts.isIdentifier(parent.name))
|
|
48
|
+
return false;
|
|
49
|
+
const key = parent.name.text;
|
|
50
|
+
if (/^on[A-Z]/.test(key))
|
|
51
|
+
return false;
|
|
52
|
+
if (key === 'key' || key === 'name')
|
|
53
|
+
return false;
|
|
54
|
+
let ancestor = parent.parent;
|
|
55
|
+
while (ancestor && !ts.isCallExpression(ancestor))
|
|
56
|
+
ancestor = ancestor.parent;
|
|
57
|
+
if (!ancestor)
|
|
58
|
+
return false;
|
|
59
|
+
const callExpr = ancestor;
|
|
60
|
+
if (!ts.isIdentifier(callExpr.expression))
|
|
61
|
+
return false;
|
|
62
|
+
return REACTIVE_API_NAMES.has(callExpr.expression.text);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
function describeReactiveContext(id) {
|
|
67
|
+
const parent = id.parent;
|
|
68
|
+
if (!parent)
|
|
69
|
+
return 'reactive position';
|
|
70
|
+
if (ts.isCallExpression(parent) && parent.arguments[0] === id) {
|
|
71
|
+
if (ts.isIdentifier(parent.expression))
|
|
72
|
+
return `${parent.expression.text}(…)`;
|
|
73
|
+
if (ts.isPropertyAccessExpression(parent.expression) &&
|
|
74
|
+
ts.isIdentifier(parent.expression.name)) {
|
|
75
|
+
return `…${parent.expression.name.text}(…)`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (ts.isPropertyAssignment(parent) &&
|
|
79
|
+
parent.initializer === id &&
|
|
80
|
+
ts.isIdentifier(parent.name)) {
|
|
81
|
+
let ancestor = parent.parent;
|
|
82
|
+
while (ancestor && !ts.isCallExpression(ancestor))
|
|
83
|
+
ancestor = ancestor.parent;
|
|
84
|
+
const calleeName = ancestor && ts.isCallExpression(ancestor) && ts.isIdentifier(ancestor.expression)
|
|
85
|
+
? ancestor.expression.text
|
|
86
|
+
: null;
|
|
87
|
+
return calleeName ? `${calleeName}({ ${parent.name.text}: … })` : `{ ${parent.name.text}: … }`;
|
|
88
|
+
}
|
|
89
|
+
return 'reactive position';
|
|
90
|
+
}
|
|
91
|
+
function isCallableInitializer(init) {
|
|
92
|
+
if (!init)
|
|
93
|
+
return false;
|
|
94
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
|
|
95
|
+
return true;
|
|
96
|
+
if (ts.isCallExpression(init) &&
|
|
97
|
+
ts.isIdentifier(init.expression) &&
|
|
98
|
+
init.expression.text === 'memo' &&
|
|
99
|
+
init.arguments.length >= 1) {
|
|
100
|
+
const inner = init.arguments[0];
|
|
101
|
+
return ts.isArrowFunction(inner) || ts.isFunctionExpression(inner);
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* True when `n` is the target of a write (assignment LHS or
|
|
107
|
+
* pre/postfix increment). We don't try to detect destructuring or
|
|
108
|
+
* compound forms exhaustively — the common case (`foo = …`,
|
|
109
|
+
* `foo += …`, `foo++`) covers the practical reassignment patterns.
|
|
110
|
+
*/
|
|
111
|
+
function isWriteTarget(n) {
|
|
112
|
+
const parent = n.parent;
|
|
113
|
+
if (!parent)
|
|
114
|
+
return false;
|
|
115
|
+
if (ts.isBinaryExpression(parent) && parent.left === n) {
|
|
116
|
+
const op = parent.operatorToken.kind;
|
|
117
|
+
return (op === ts.SyntaxKind.EqualsToken ||
|
|
118
|
+
op === ts.SyntaxKind.PlusEqualsToken ||
|
|
119
|
+
op === ts.SyntaxKind.MinusEqualsToken ||
|
|
120
|
+
op === ts.SyntaxKind.AsteriskEqualsToken ||
|
|
121
|
+
op === ts.SyntaxKind.SlashEqualsToken);
|
|
122
|
+
}
|
|
123
|
+
if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {
|
|
124
|
+
return (parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
125
|
+
parent.operator === ts.SyntaxKind.MinusMinusToken);
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
export function noLetReactiveAccessorModule() {
|
|
130
|
+
return {
|
|
131
|
+
name: 'no-let-reactive-accessor',
|
|
132
|
+
compilerVersion: '^0.3.0',
|
|
133
|
+
diagnostics: [
|
|
134
|
+
{
|
|
135
|
+
id: 'llui/no-let-reactive-accessor',
|
|
136
|
+
description: '`let` / `var` accessor referenced at a reactive position — compiler falls back to FULL_MASK. Use `const`.',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
visitors: {
|
|
140
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
141
|
+
const visited = node;
|
|
142
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
143
|
+
const decls = [];
|
|
144
|
+
for (const stmt of sf.statements) {
|
|
145
|
+
if (!ts.isVariableStatement(stmt))
|
|
146
|
+
continue;
|
|
147
|
+
const flags = stmt.declarationList.flags;
|
|
148
|
+
let kind = null;
|
|
149
|
+
if (flags & ts.NodeFlags.Let)
|
|
150
|
+
kind = 'let';
|
|
151
|
+
else if (!(flags & ts.NodeFlags.Const))
|
|
152
|
+
kind = 'var';
|
|
153
|
+
if (!kind)
|
|
154
|
+
continue;
|
|
155
|
+
if (stmt.declarationList.declarations.length !== 1)
|
|
156
|
+
continue;
|
|
157
|
+
const decl = stmt.declarationList.declarations[0];
|
|
158
|
+
if (!ts.isIdentifier(decl.name))
|
|
159
|
+
continue;
|
|
160
|
+
if (!isCallableInitializer(decl.initializer))
|
|
161
|
+
continue;
|
|
162
|
+
decls.push({ name: decl.name.text, kind, stmt });
|
|
163
|
+
}
|
|
164
|
+
if (decls.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
// For each declaration, scan all identifier references in
|
|
167
|
+
// the file. Track reads at reactive positions and any
|
|
168
|
+
// write (reassignment). The declarator's own identifier is
|
|
169
|
+
// excluded.
|
|
170
|
+
const declMap = new Map(decls.map((d) => [d.name, d]));
|
|
171
|
+
const state = new Map();
|
|
172
|
+
for (const d of decls) {
|
|
173
|
+
state.set(d.name, { hasReactive: false, reactiveContext: null, hasReassign: false });
|
|
174
|
+
}
|
|
175
|
+
// We don't track shadowing rigorously — a nested `const foo`
|
|
176
|
+
// would still let the outer `let foo`'s name fire. The
|
|
177
|
+
// common case (top-level let, no inner shadowing) is what we
|
|
178
|
+
// care about; shadowing is rare and an inner const would
|
|
179
|
+
// change behavior anyway.
|
|
180
|
+
const walk = (n) => {
|
|
181
|
+
if (ts.isIdentifier(n) && declMap.has(n.text)) {
|
|
182
|
+
const decl = declMap.get(n.text);
|
|
183
|
+
const slot = state.get(n.text);
|
|
184
|
+
// Skip the declarator's own identifier node — that's not
|
|
185
|
+
// a reference, it's the declaration itself.
|
|
186
|
+
const declIdent = decl.stmt.declarationList.declarations[0].name;
|
|
187
|
+
if (n !== declIdent) {
|
|
188
|
+
if (isWriteTarget(n))
|
|
189
|
+
slot.hasReassign = true;
|
|
190
|
+
if (!slot.hasReactive && isAtReactivePosition(n)) {
|
|
191
|
+
slot.hasReactive = true;
|
|
192
|
+
slot.reactiveContext = describeReactiveContext(n);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
ts.forEachChild(n, walk);
|
|
197
|
+
};
|
|
198
|
+
walk(sf);
|
|
199
|
+
for (const d of decls) {
|
|
200
|
+
const s = state.get(d.name);
|
|
201
|
+
if (!s.hasReactive)
|
|
202
|
+
continue;
|
|
203
|
+
const reactiveCtx = s.reactiveContext ?? 'reactive position';
|
|
204
|
+
const fixHint = s.hasReassign
|
|
205
|
+
? `Either avoid reassignment (use \`const\` and a different binding for the new value) or accept the FULL_MASK fallback.`
|
|
206
|
+
: `Fix: change \`${d.kind} ${d.name} = …\` to \`const ${d.name} = …\`.`;
|
|
207
|
+
const reasonClause = s.hasReassign
|
|
208
|
+
? `, but it's reassigned later in the file — the compiler can't follow \`${d.kind}\` bindings`
|
|
209
|
+
: '';
|
|
210
|
+
ctx.reportDiagnostic({
|
|
211
|
+
id: 'llui/no-let-reactive-accessor',
|
|
212
|
+
severity: 'error',
|
|
213
|
+
category: 'reactivity',
|
|
214
|
+
message: `\`${d.name}\` is a \`${d.kind}\`-bound accessor used at a reactive position ` +
|
|
215
|
+
`(${reactiveCtx})${reasonClause}. The compiler only follows \`const\` bindings; ` +
|
|
216
|
+
`\`${d.kind}\` falls back to FULL_MASK at runtime. ${fixHint}`,
|
|
217
|
+
location: {
|
|
218
|
+
file: sf.fileName,
|
|
219
|
+
range: rangeFromOffsets(sf.text, d.stmt.getStart(sf), d.stmt.getEnd()),
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=no-let-reactive-accessor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-let-reactive-accessor.js","sourceRoot":"","sources":["../../src/modules/no-let-reactive-accessor.ts"],"names":[],"mappings":"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","sourcesContent":["// `no-let-reactive-accessor` — errors when a top-level `let` / `var`\n// binding with a callable initializer is referenced at a reactive\n// accessor position. The compiler's `resolveAccessorBody` refuses to\n// follow `let` / `var` bindings (reassignment would invalidate any\n// compile-time mask analysis), so the binding silently falls back to\n// FULL_MASK at runtime — correct but suboptimal. Forcing `const`\n// preserves the precise-mask optimization. Migrated from\n// `@llui/eslint-plugin/src/rules/no-let-reactive-accessor.ts`.\n//\n// Note: autofix dropped (per the lint→compiler migration plan); the\n// error message includes the exact `let`/`var` → `const` substitution\n// so an LLM consuming the error can apply it directly.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { ELEMENT_HELPERS } from './_element-helpers.js'\n\nconst REACTIVE_API_NAMES = new Set<string>([\n ...ELEMENT_HELPERS,\n 'each',\n 'branch',\n 'scope',\n 'show',\n 'memo',\n 'portal',\n 'foreign',\n 'child',\n 'errorBoundary',\n])\nconst FIRST_ARG_BINDING_HELPERS = new Set(['text', 'unsafeHtml', 'memo'])\n\nfunction isAtReactivePosition(node: ts.Identifier): boolean {\n const parent = node.parent\n if (!parent) return false\n if (ts.isCallExpression(parent) && parent.arguments[0] === node) {\n if (ts.isIdentifier(parent.expression)) {\n const name = parent.expression.text\n if (name === 'item' || name === 'sample') return false\n return FIRST_ARG_BINDING_HELPERS.has(name) || REACTIVE_API_NAMES.has(name)\n }\n if (\n ts.isPropertyAccessExpression(parent.expression) &&\n ts.isIdentifier(parent.expression.name)\n ) {\n return FIRST_ARG_BINDING_HELPERS.has(parent.expression.name.text)\n }\n return false\n }\n if (ts.isPropertyAssignment(parent) && parent.initializer === node) {\n if (!ts.isIdentifier(parent.name)) return false\n const key = parent.name.text\n if (/^on[A-Z]/.test(key)) return false\n if (key === 'key' || key === 'name') return false\n let ancestor: ts.Node | undefined = parent.parent\n while (ancestor && !ts.isCallExpression(ancestor)) ancestor = ancestor.parent\n if (!ancestor) return false\n const callExpr = ancestor as ts.CallExpression\n if (!ts.isIdentifier(callExpr.expression)) return false\n return REACTIVE_API_NAMES.has(callExpr.expression.text)\n }\n return false\n}\n\nfunction describeReactiveContext(id: ts.Identifier): string {\n const parent = id.parent\n if (!parent) return 'reactive position'\n if (ts.isCallExpression(parent) && parent.arguments[0] === id) {\n if (ts.isIdentifier(parent.expression)) return `${parent.expression.text}(…)`\n if (\n ts.isPropertyAccessExpression(parent.expression) &&\n ts.isIdentifier(parent.expression.name)\n ) {\n return `…${parent.expression.name.text}(…)`\n }\n }\n if (\n ts.isPropertyAssignment(parent) &&\n parent.initializer === id &&\n ts.isIdentifier(parent.name)\n ) {\n let ancestor: ts.Node | undefined = parent.parent\n while (ancestor && !ts.isCallExpression(ancestor)) ancestor = ancestor.parent\n const calleeName =\n ancestor && ts.isCallExpression(ancestor) && ts.isIdentifier(ancestor.expression)\n ? ancestor.expression.text\n : null\n return calleeName ? `${calleeName}({ ${parent.name.text}: … })` : `{ ${parent.name.text}: … }`\n }\n return 'reactive position'\n}\n\nfunction isCallableInitializer(init: ts.Expression | undefined): boolean {\n if (!init) return false\n if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) return true\n if (\n ts.isCallExpression(init) &&\n ts.isIdentifier(init.expression) &&\n init.expression.text === 'memo' &&\n init.arguments.length >= 1\n ) {\n const inner = init.arguments[0]!\n return ts.isArrowFunction(inner) || ts.isFunctionExpression(inner)\n }\n return false\n}\n\n/**\n * True when `n` is the target of a write (assignment LHS or\n * pre/postfix increment). We don't try to detect destructuring or\n * compound forms exhaustively — the common case (`foo = …`,\n * `foo += …`, `foo++`) covers the practical reassignment patterns.\n */\nfunction isWriteTarget(n: ts.Identifier): boolean {\n const parent = n.parent\n if (!parent) return false\n if (ts.isBinaryExpression(parent) && parent.left === n) {\n const op = parent.operatorToken.kind\n return (\n op === ts.SyntaxKind.EqualsToken ||\n op === ts.SyntaxKind.PlusEqualsToken ||\n op === ts.SyntaxKind.MinusEqualsToken ||\n op === ts.SyntaxKind.AsteriskEqualsToken ||\n op === ts.SyntaxKind.SlashEqualsToken\n )\n }\n if (ts.isPostfixUnaryExpression(parent) || ts.isPrefixUnaryExpression(parent)) {\n return (\n parent.operator === ts.SyntaxKind.PlusPlusToken ||\n parent.operator === ts.SyntaxKind.MinusMinusToken\n )\n }\n return false\n}\n\nexport function noLetReactiveAccessorModule(): CompilerModule {\n return {\n name: 'no-let-reactive-accessor',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-let-reactive-accessor',\n description:\n '`let` / `var` accessor referenced at a reactive position — compiler falls back to FULL_MASK. Use `const`.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n // Collect single-declarator top-level `let foo = …` /\n // `var foo = …` declarations with a callable initializer.\n // Nested let/var are out of scope — the compiler resolver\n // only follows top-level bindings anyway.\n type Decl = {\n name: string\n kind: 'let' | 'var'\n stmt: ts.VariableStatement\n }\n const decls: Decl[] = []\n for (const stmt of sf.statements) {\n if (!ts.isVariableStatement(stmt)) continue\n const flags = stmt.declarationList.flags\n let kind: 'let' | 'var' | null = null\n if (flags & ts.NodeFlags.Let) kind = 'let'\n else if (!(flags & ts.NodeFlags.Const)) kind = 'var'\n if (!kind) continue\n if (stmt.declarationList.declarations.length !== 1) continue\n const decl = stmt.declarationList.declarations[0]!\n if (!ts.isIdentifier(decl.name)) continue\n if (!isCallableInitializer(decl.initializer)) continue\n decls.push({ name: decl.name.text, kind, stmt })\n }\n if (decls.length === 0) return\n\n // For each declaration, scan all identifier references in\n // the file. Track reads at reactive positions and any\n // write (reassignment). The declarator's own identifier is\n // excluded.\n const declMap = new Map(decls.map((d) => [d.name, d] as const))\n const state = new Map<\n string,\n { hasReactive: boolean; reactiveContext: string | null; hasReassign: boolean }\n >()\n for (const d of decls) {\n state.set(d.name, { hasReactive: false, reactiveContext: null, hasReassign: false })\n }\n // We don't track shadowing rigorously — a nested `const foo`\n // would still let the outer `let foo`'s name fire. The\n // common case (top-level let, no inner shadowing) is what we\n // care about; shadowing is rare and an inner const would\n // change behavior anyway.\n const walk = (n: ts.Node): void => {\n if (ts.isIdentifier(n) && declMap.has(n.text)) {\n const decl = declMap.get(n.text)!\n const slot = state.get(n.text)!\n // Skip the declarator's own identifier node — that's not\n // a reference, it's the declaration itself.\n const declIdent = decl.stmt.declarationList.declarations[0]!.name\n if (n !== declIdent) {\n if (isWriteTarget(n)) slot.hasReassign = true\n if (!slot.hasReactive && isAtReactivePosition(n)) {\n slot.hasReactive = true\n slot.reactiveContext = describeReactiveContext(n)\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n\n for (const d of decls) {\n const s = state.get(d.name)!\n if (!s.hasReactive) continue\n const reactiveCtx = s.reactiveContext ?? 'reactive position'\n const fixHint = s.hasReassign\n ? `Either avoid reassignment (use \\`const\\` and a different binding for the new value) or accept the FULL_MASK fallback.`\n : `Fix: change \\`${d.kind} ${d.name} = …\\` to \\`const ${d.name} = …\\`.`\n const reasonClause = s.hasReassign\n ? `, but it's reassigned later in the file — the compiler can't follow \\`${d.kind}\\` bindings`\n : ''\n ctx.reportDiagnostic({\n id: 'llui/no-let-reactive-accessor',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`${d.name}\\` is a \\`${d.kind}\\`-bound accessor used at a reactive position ` +\n `(${reactiveCtx})${reasonClause}. The compiler only follows \\`const\\` bindings; ` +\n `\\`${d.kind}\\` falls back to FULL_MASK at runtime. ${fixHint}`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, d.stmt.getStart(sf), d.stmt.getEnd()),\n },\n })\n }\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-list-render-in-sample.d.ts","sourceRoot":"","sources":["../../src/modules/no-list-render-in-sample.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AA4BlD,wBAAgB,0BAA0B,IAAI,cAAc,CAqD3D"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// `no-list-render-in-sample` — errors when `sample()` wraps a `.map()`
|
|
2
|
+
// over state-derived items. `sample` is a one-shot imperative read; the
|
|
3
|
+
// rendered cells go stale on in-place row updates. Use `each + ItemAccessor`.
|
|
4
|
+
// Migrated from
|
|
5
|
+
// `@llui/eslint-plugin/src/rules/no-list-render-in-sample.ts`.
|
|
6
|
+
import ts from 'typescript';
|
|
7
|
+
import { rangeFromOffsets } from '../diagnostic.js';
|
|
8
|
+
const STATE_PARAM_NAMES = new Set(['s', 'state', 'props']);
|
|
9
|
+
function isStateRootedMemberMap(n) {
|
|
10
|
+
if (!ts.isCallExpression(n))
|
|
11
|
+
return false;
|
|
12
|
+
if (!ts.isPropertyAccessExpression(n.expression))
|
|
13
|
+
return false;
|
|
14
|
+
if (!ts.isIdentifier(n.expression.name) || n.expression.name.text !== 'map')
|
|
15
|
+
return false;
|
|
16
|
+
let cursor = n.expression.expression;
|
|
17
|
+
while (ts.isPropertyAccessExpression(cursor))
|
|
18
|
+
cursor = cursor.expression;
|
|
19
|
+
if (!ts.isIdentifier(cursor))
|
|
20
|
+
return false;
|
|
21
|
+
return STATE_PARAM_NAMES.has(cursor.text);
|
|
22
|
+
}
|
|
23
|
+
function bodyContainsStateMap(body) {
|
|
24
|
+
let found = false;
|
|
25
|
+
const walk = (n) => {
|
|
26
|
+
if (found)
|
|
27
|
+
return;
|
|
28
|
+
if (isStateRootedMemberMap(n)) {
|
|
29
|
+
found = true;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(n, walk);
|
|
33
|
+
};
|
|
34
|
+
walk(body);
|
|
35
|
+
return found;
|
|
36
|
+
}
|
|
37
|
+
export function noListRenderInSampleModule() {
|
|
38
|
+
return {
|
|
39
|
+
name: 'no-list-render-in-sample',
|
|
40
|
+
compilerVersion: '^0.3.0',
|
|
41
|
+
diagnostics: [
|
|
42
|
+
{
|
|
43
|
+
id: 'llui/no-list-render-in-sample',
|
|
44
|
+
description: '`sample()` wrapping `.map()` over state-derived items — cells go stale.',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
visitors: {
|
|
48
|
+
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
49
|
+
const visited = node;
|
|
50
|
+
const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
51
|
+
const walk = (n) => {
|
|
52
|
+
if (ts.isCallExpression(n)) {
|
|
53
|
+
let isSample = false;
|
|
54
|
+
if (ts.isIdentifier(n.expression) && n.expression.text === 'sample')
|
|
55
|
+
isSample = true;
|
|
56
|
+
else if (ts.isPropertyAccessExpression(n.expression) &&
|
|
57
|
+
ts.isIdentifier(n.expression.name) &&
|
|
58
|
+
n.expression.name.text === 'sample')
|
|
59
|
+
isSample = true;
|
|
60
|
+
if (isSample) {
|
|
61
|
+
const arg = n.arguments[0];
|
|
62
|
+
if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) && arg.body) {
|
|
63
|
+
if (bodyContainsStateMap(arg.body)) {
|
|
64
|
+
ctx.reportDiagnostic({
|
|
65
|
+
id: 'llui/no-list-render-in-sample',
|
|
66
|
+
severity: 'error',
|
|
67
|
+
category: 'reactivity',
|
|
68
|
+
message: `\`sample()\` is a one-shot read — \`.map()\` over state-derived items inside ` +
|
|
69
|
+
`it captures the rows at view-construction and the cells go stale when row ` +
|
|
70
|
+
`data updates in place. Use \`each({ items: (s) => s.<list>, key, render })\` ` +
|
|
71
|
+
`and bind cells reactively via \`text(item.field)\` / ` +
|
|
72
|
+
`\`show({ when: () => item.flag() })\`. See cookbook recipe "List of editable rows."`,
|
|
73
|
+
location: {
|
|
74
|
+
file: sf.fileName,
|
|
75
|
+
range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ts.forEachChild(n, walk);
|
|
83
|
+
};
|
|
84
|
+
walk(sf);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=no-list-render-in-sample.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-list-render-in-sample.js","sourceRoot":"","sources":["../../src/modules/no-list-render-in-sample.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,wEAAwE;AACxE,8EAA8E;AAC9E,gBAAgB;AAChB,+DAA+D;AAE/D,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAGnD,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AAE1D,SAAS,sBAAsB,CAAC,CAAU;IACxC,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAA;IACzC,IAAI,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,CAAC,UAAU,CAAC;QAAE,OAAO,KAAK,CAAA;IAC9D,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IACzF,IAAI,MAAM,GAAkB,CAAC,CAAC,UAAU,CAAC,UAAU,CAAA;IACnD,OAAO,EAAE,CAAC,0BAA0B,CAAC,MAAM,CAAC;QAAE,MAAM,GAAG,MAAM,CAAC,UAAU,CAAA;IACxE,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAA;IAC1C,OAAO,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AAC3C,CAAC;AAED,SAAS,oBAAoB,CAAC,IAAa;IACzC,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,MAAM,IAAI,GAAG,CAAC,CAAU,EAAQ,EAAE;QAChC,IAAI,KAAK;YAAE,OAAM;QACjB,IAAI,sBAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,KAAK,GAAG,IAAI,CAAA;YACZ,OAAM;QACR,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAC1B,CAAC,CAAA;IACD,IAAI,CAAC,IAAI,CAAC,CAAA;IACV,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,0BAA0B;IACxC,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,eAAe,EAAE,QAAQ;QACzB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,+BAA+B;gBACnC,WAAW,EAAE,yEAAyE;aACvF;SACF;QACD,QAAQ,EAAE;YACR,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;gBACxC,MAAM,OAAO,GAAG,IAAqB,CAAA;gBACrC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;gBAC5F,MAAM,IAAI,GAAG,CAAC,CAAU,EAAQ,EAAE;oBAChC,IAAI,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC3B,IAAI,QAAQ,GAAG,KAAK,CAAA;wBACpB,IAAI,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,KAAK,QAAQ;4BAAE,QAAQ,GAAG,IAAI,CAAA;6BAC/E,IACH,EAAE,CAAC,0BAA0B,CAAC,CAAC,CAAC,UAAU,CAAC;4BAC3C,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;4BAClC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ;4BAEnC,QAAQ,GAAG,IAAI,CAAA;wBACjB,IAAI,QAAQ,EAAE,CAAC;4BACb,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;4BAC1B,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;gCACjF,IAAI,oBAAoB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;oCACnC,GAAG,CAAC,gBAAgB,CAAC;wCACnB,EAAE,EAAE,+BAA+B;wCACnC,QAAQ,EAAE,OAAO;wCACjB,QAAQ,EAAE,YAAY;wCACtB,OAAO,EACL,+EAA+E;4CAC/E,4EAA4E;4CAC5E,+EAA+E;4CAC/E,uDAAuD;4CACvD,qFAAqF;wCACvF,QAAQ,EAAE;4CACR,IAAI,EAAE,EAAE,CAAC,QAAQ;4CACjB,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;yCAC7D;qCACF,CAAC,CAAA;gCACJ,CAAC;4BACH,CAAC;wBACH,CAAC;oBACH,CAAC;oBACD,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;gBAC1B,CAAC,CAAA;gBACD,IAAI,CAAC,EAAE,CAAC,CAAA;YACV,CAAC;SACF;KACF,CAAA;AACH,CAAC","sourcesContent":["// `no-list-render-in-sample` — errors when `sample()` wraps a `.map()`\n// over state-derived items. `sample` is a one-shot imperative read; the\n// rendered cells go stale on in-place row updates. Use `each + ItemAccessor`.\n// Migrated from\n// `@llui/eslint-plugin/src/rules/no-list-render-in-sample.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\n\nconst STATE_PARAM_NAMES = new Set(['s', 'state', 'props'])\n\nfunction isStateRootedMemberMap(n: ts.Node): boolean {\n if (!ts.isCallExpression(n)) return false\n if (!ts.isPropertyAccessExpression(n.expression)) return false\n if (!ts.isIdentifier(n.expression.name) || n.expression.name.text !== 'map') return false\n let cursor: ts.Expression = n.expression.expression\n while (ts.isPropertyAccessExpression(cursor)) cursor = cursor.expression\n if (!ts.isIdentifier(cursor)) return false\n return STATE_PARAM_NAMES.has(cursor.text)\n}\n\nfunction bodyContainsStateMap(body: ts.Node): boolean {\n let found = false\n const walk = (n: ts.Node): void => {\n if (found) return\n if (isStateRootedMemberMap(n)) {\n found = true\n return\n }\n ts.forEachChild(n, walk)\n }\n walk(body)\n return found\n}\n\nexport function noListRenderInSampleModule(): CompilerModule {\n return {\n name: 'no-list-render-in-sample',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/no-list-render-in-sample',\n description: '`sample()` wrapping `.map()` over state-derived items — cells go stale.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n const walk = (n: ts.Node): void => {\n if (ts.isCallExpression(n)) {\n let isSample = false\n if (ts.isIdentifier(n.expression) && n.expression.text === 'sample') isSample = true\n else if (\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n n.expression.name.text === 'sample'\n )\n isSample = true\n if (isSample) {\n const arg = n.arguments[0]\n if (arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) && arg.body) {\n if (bodyContainsStateMap(arg.body)) {\n ctx.reportDiagnostic({\n id: 'llui/no-list-render-in-sample',\n severity: 'error',\n category: 'reactivity',\n message:\n `\\`sample()\\` is a one-shot read — \\`.map()\\` over state-derived items inside ` +\n `it captures the rows at view-construction and the cells go stale when row ` +\n `data updates in place. Use \\`each({ items: (s) => s.<list>, key, render })\\` ` +\n `and bind cells reactively via \\`text(item.field)\\` / ` +\n `\\`show({ when: () => item.flag() })\\`. See cookbook recipe \"List of editable rows.\"`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),\n },\n })\n }\n }\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-sample-in-accessor.d.ts","sourceRoot":"","sources":["../../src/modules/no-sample-in-accessor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAqDlD,wBAAgB,wBAAwB,IAAI,cAAc,CAiFzD"}
|