@elaraai/east-diagnostics 1.0.4
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.md +31 -0
- package/README.md +74 -0
- package/dist/src/block-builder.d.ts +16 -0
- package/dist/src/block-builder.d.ts.map +1 -0
- package/dist/src/block-builder.js +19 -0
- package/dist/src/block-builder.js.map +1 -0
- package/dist/src/block-scope.d.ts +27 -0
- package/dist/src/block-scope.d.ts.map +1 -0
- package/dist/src/block-scope.js +53 -0
- package/dist/src/block-scope.js.map +1 -0
- package/dist/src/east-type.d.ts +8 -0
- package/dist/src/east-type.d.ts.map +1 -0
- package/dist/src/east-type.js +28 -0
- package/dist/src/east-type.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rules/index.d.ts +19 -0
- package/dist/src/rules/index.d.ts.map +1 -0
- package/dist/src/rules/index.js +39 -0
- package/dist/src/rules/index.js.map +1 -0
- package/dist/src/rules/no-east-data-builder-helper.d.ts +3 -0
- package/dist/src/rules/no-east-data-builder-helper.d.ts.map +1 -0
- package/dist/src/rules/no-east-data-builder-helper.js +90 -0
- package/dist/src/rules/no-east-data-builder-helper.js.map +1 -0
- package/dist/src/rules/no-east-namespaced-type.d.ts +7 -0
- package/dist/src/rules/no-east-namespaced-type.d.ts.map +1 -0
- package/dist/src/rules/no-east-namespaced-type.js +32 -0
- package/dist/src/rules/no-east-namespaced-type.js.map +1 -0
- package/dist/src/rules/no-handrolled-variant.d.ts +3 -0
- package/dist/src/rules/no-handrolled-variant.d.ts.map +1 -0
- package/dist/src/rules/no-handrolled-variant.js +45 -0
- package/dist/src/rules/no-handrolled-variant.js.map +1 -0
- package/dist/src/rules/no-let-const-in-expression.d.ts +3 -0
- package/dist/src/rules/no-let-const-in-expression.d.ts.map +1 -0
- package/dist/src/rules/no-let-const-in-expression.js +51 -0
- package/dist/src/rules/no-let-const-in-expression.js.map +1 -0
- package/dist/src/rules/no-redundant-east-cast.d.ts +3 -0
- package/dist/src/rules/no-redundant-east-cast.d.ts.map +1 -0
- package/dist/src/rules/no-redundant-east-cast.js +44 -0
- package/dist/src/rules/no-redundant-east-cast.js.map +1 -0
- package/dist/src/rules/no-reinlined-east-binding.d.ts +3 -0
- package/dist/src/rules/no-reinlined-east-binding.d.ts.map +1 -0
- package/dist/src/rules/no-reinlined-east-binding.js +79 -0
- package/dist/src/rules/no-reinlined-east-binding.js.map +1 -0
- package/dist/src/rules/no-relative-src-import.d.ts +3 -0
- package/dist/src/rules/no-relative-src-import.d.ts.map +1 -0
- package/dist/src/rules/no-relative-src-import.js +40 -0
- package/dist/src/rules/no-relative-src-import.js.map +1 -0
- package/dist/src/rules/no-ts-helpers-in-east.d.ts +3 -0
- package/dist/src/rules/no-ts-helpers-in-east.d.ts.map +1 -0
- package/dist/src/rules/no-ts-helpers-in-east.js +50 -0
- package/dist/src/rules/no-ts-helpers-in-east.js.map +1 -0
- package/dist/src/rules/no-unexecuted-east-expression.d.ts +3 -0
- package/dist/src/rules/no-unexecuted-east-expression.d.ts.map +1 -0
- package/dist/src/rules/no-unexecuted-east-expression.js +50 -0
- package/dist/src/rules/no-unexecuted-east-expression.js.map +1 -0
- package/dist/src/rules/prefer-explicit-east-type.d.ts +3 -0
- package/dist/src/rules/prefer-explicit-east-type.d.ts.map +1 -0
- package/dist/src/rules/prefer-explicit-east-type.js +60 -0
- package/dist/src/rules/prefer-explicit-east-type.js.map +1 -0
- package/dist/src/rules/prefer-jsx-over-factory-call.d.ts +3 -0
- package/dist/src/rules/prefer-jsx-over-factory-call.d.ts.map +1 -0
- package/dist/src/rules/prefer-jsx-over-factory-call.js +141 -0
- package/dist/src/rules/prefer-jsx-over-factory-call.js.map +1 -0
- package/dist/src/rules/prefer-let-const-over-east-value.d.ts +7 -0
- package/dist/src/rules/prefer-let-const-over-east-value.d.ts.map +1 -0
- package/dist/src/rules/prefer-let-const-over-east-value.js +71 -0
- package/dist/src/rules/prefer-let-const-over-east-value.js.map +1 -0
- package/dist/src/rules/prefer-some-none.d.ts +7 -0
- package/dist/src/rules/prefer-some-none.d.ts.map +1 -0
- package/dist/src/rules/prefer-some-none.js +36 -0
- package/dist/src/rules/prefer-some-none.js.map +1 -0
- package/dist/src/run.d.ts +11 -0
- package/dist/src/run.d.ts.map +1 -0
- package/dist/src/run.js +25 -0
- package/dist/src/run.js.map +1 -0
- package/dist/src/service.d.ts +19 -0
- package/dist/src/service.d.ts.map +1 -0
- package/dist/src/service.js +149 -0
- package/dist/src/service.js.map +1 -0
- package/dist/src/types.d.ts +56 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { matchBlockBuilderCall } from "../block-builder.js";
|
|
2
|
+
const NAME = "no-let-const-in-expression";
|
|
3
|
+
const CODE = 990008;
|
|
4
|
+
// `$.let` / `$.const` declare a variable in the block (a statement-like effect)
|
|
5
|
+
// and return the expression handle. The only correct position is the initializer
|
|
6
|
+
// of a JS `const`/`let`. Used anywhere else — as an argument (`$.if($.let(...))`),
|
|
7
|
+
// a chain target (`$.let(...).add(1n)`), or an operand — it buries a declaration
|
|
8
|
+
// inside an expression. Type-checks fine, so the checker won't catch it.
|
|
9
|
+
export const noLetConstInExpression = {
|
|
10
|
+
name: NAME,
|
|
11
|
+
code: CODE,
|
|
12
|
+
description: "Require $.let/$.const to be bound to a const; disallow using the result inline in an expression.",
|
|
13
|
+
check(node, ctx) {
|
|
14
|
+
const match = matchBlockBuilderCall(node, ctx);
|
|
15
|
+
if (match === undefined)
|
|
16
|
+
return;
|
|
17
|
+
const t = ctx.ts;
|
|
18
|
+
const call = match.call;
|
|
19
|
+
// Walk out of wrapping parentheses.
|
|
20
|
+
let current = call;
|
|
21
|
+
let parent = current.parent;
|
|
22
|
+
while (parent !== undefined && t.isParenthesizedExpression(parent)) {
|
|
23
|
+
current = parent;
|
|
24
|
+
parent = parent.parent;
|
|
25
|
+
}
|
|
26
|
+
if (parent === undefined)
|
|
27
|
+
return;
|
|
28
|
+
// Flag only when the result is consumed inline: a chain target
|
|
29
|
+
// (`$.let(...).add(1n)`), an index target, an operator operand, or an
|
|
30
|
+
// argument to another call (`$.if($.let(...))`). A const/let initializer,
|
|
31
|
+
// a bare statement, `return $.const(...)`, and a concise arrow body are all
|
|
32
|
+
// valid and stay silent.
|
|
33
|
+
const usedInExpression = (t.isPropertyAccessExpression(parent) && parent.expression === current) ||
|
|
34
|
+
(t.isElementAccessExpression(parent) && parent.expression === current) ||
|
|
35
|
+
(t.isBinaryExpression(parent) && (parent.left === current || parent.right === current)) ||
|
|
36
|
+
(t.isCallExpression(parent) && parent.arguments.some((arg) => arg === current));
|
|
37
|
+
if (!usedInExpression)
|
|
38
|
+
return;
|
|
39
|
+
const sf = ctx.sourceFile;
|
|
40
|
+
const start = call.getStart(sf);
|
|
41
|
+
ctx.report({
|
|
42
|
+
ruleName: NAME,
|
|
43
|
+
code: CODE,
|
|
44
|
+
start,
|
|
45
|
+
length: call.getEnd() - start,
|
|
46
|
+
messageText: `\`$.${match.method}\` declares a variable — bind it to a \`const\` first (\`const x = $.${match.method}(value, Type)\`), don't use the result inline (e.g. \`$.if($.let(...))\` or \`$.let(...).add(...)\`).`,
|
|
47
|
+
category: "warning",
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=no-let-const-in-expression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-let-const-in-expression.js","sourceRoot":"","sources":["../../../src/rules/no-let-const-in-expression.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,IAAI,GAAG,4BAA4B,CAAC;AAC1C,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB,gFAAgF;AAChF,iFAAiF;AACjF,mFAAmF;AACnF,iFAAiF;AACjF,yEAAyE;AACzE,MAAM,CAAC,MAAM,sBAAsB,GAAa;IAC9C,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,WAAW,EACT,kGAAkG;IACpG,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAEhC,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QAExB,oCAAoC;QACpC,IAAI,OAAO,GAAY,IAAI,CAAC;QAC5B,IAAI,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC5B,OAAO,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,yBAAyB,CAAC,MAAM,CAAC,EAAE,CAAC;YACnE,OAAO,GAAG,MAAM,CAAC;YACjB,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QACzB,CAAC;QACD,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO;QAEjC,+DAA+D;QAC/D,sEAAsE;QACtE,0EAA0E;QAC1E,4EAA4E;QAC5E,yBAAyB;QACzB,MAAM,gBAAgB,GACpB,CAAC,CAAC,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC;YACvE,CAAC,CAAC,CAAC,yBAAyB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC;YACtE,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,OAAO,IAAI,MAAM,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC;YACvF,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC;QAClF,IAAI,CAAC,gBAAgB;YAAE,OAAO;QAE9B,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAChC,GAAG,CAAC,MAAM,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,IAAI;YACV,KAAK;YACL,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK;YAC7B,WAAW,EAAE,OAAO,KAAK,CAAC,MAAM,wEAAwE,KAAK,CAAC,MAAM,uGAAuG;YAC3N,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-redundant-east-cast.d.ts","sourceRoot":"","sources":["../../../src/rules/no-redundant-east-cast.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAU5C,eAAO,MAAM,mBAAmB,EAAE,QAkCjC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { matchBlockBuilderCall } from "../block-builder.js";
|
|
2
|
+
const NAME = "no-redundant-east-cast";
|
|
3
|
+
const CODE = 990001;
|
|
4
|
+
// `$.let` / `$.const` two-arg overload is `(<T>(expr: SubtypeExprOrValue<NoInfer<T>>, type: T) => ...)`:
|
|
5
|
+
// `NoInfer<T>` makes the East type argument drive inference, so a TS cast on the
|
|
6
|
+
// value is dead weight. (The one-arg overload infers from the value, where a
|
|
7
|
+
// cast *would* matter — hence we only fire when the type argument is present.)
|
|
8
|
+
export const noRedundantEastCast = {
|
|
9
|
+
name: NAME,
|
|
10
|
+
code: CODE,
|
|
11
|
+
description: "Disallow a TypeScript cast on the value argument of $.let/$.const when the East type argument is present (the type argument already drives inference).",
|
|
12
|
+
check(node, ctx) {
|
|
13
|
+
const match = matchBlockBuilderCall(node, ctx);
|
|
14
|
+
if (match === undefined || match.args.length < 2)
|
|
15
|
+
return;
|
|
16
|
+
const t = ctx.ts;
|
|
17
|
+
const value = match.args[0];
|
|
18
|
+
if (value === undefined)
|
|
19
|
+
return;
|
|
20
|
+
let inner;
|
|
21
|
+
if (t.isAsExpression(value))
|
|
22
|
+
inner = value.expression;
|
|
23
|
+
else if (t.isTypeAssertionExpression(value))
|
|
24
|
+
inner = value.expression;
|
|
25
|
+
if (inner === undefined)
|
|
26
|
+
return;
|
|
27
|
+
const sf = ctx.sourceFile;
|
|
28
|
+
const start = value.getStart(sf);
|
|
29
|
+
const length = value.getEnd() - start;
|
|
30
|
+
ctx.report({
|
|
31
|
+
ruleName: NAME,
|
|
32
|
+
code: CODE,
|
|
33
|
+
start,
|
|
34
|
+
length,
|
|
35
|
+
messageText: `Redundant cast: \`$.${match.method}\` infers the value type from the East type argument; drop the \`as …\` on the value.`,
|
|
36
|
+
category: "warning",
|
|
37
|
+
fix: {
|
|
38
|
+
description: "Remove redundant cast",
|
|
39
|
+
changes: [{ start, length, newText: inner.getText(sf) }],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=no-redundant-east-cast.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-redundant-east-cast.js","sourceRoot":"","sources":["../../../src/rules/no-redundant-east-cast.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,IAAI,GAAG,wBAAwB,CAAC;AACtC,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB,yGAAyG;AACzG,iFAAiF;AACjF,6EAA6E;AAC7E,+EAA+E;AAC/E,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,WAAW,EACT,wJAAwJ;IAC1J,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO;QAEzD,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QACjB,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAEhC,IAAI,KAAgC,CAAC;QACrC,IAAI,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC;YAAE,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC;aACjD,IAAI,CAAC,CAAC,yBAAyB,CAAC,KAAK,CAAC;YAAE,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC;QACtE,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAEhC,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC;QACtC,GAAG,CAAC,MAAM,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,IAAI;YACV,KAAK;YACL,MAAM;YACN,WAAW,EAAE,uBAAuB,KAAK,CAAC,MAAM,uFAAuF;YACvI,QAAQ,EAAE,SAAS;YACnB,GAAG,EAAE;gBACH,WAAW,EAAE,uBAAuB;gBACpC,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;aACzD;SACF,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-reinlined-east-binding.d.ts","sourceRoot":"","sources":["../../../src/rules/no-reinlined-east-binding.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAsB5C,eAAO,MAAM,sBAAsB,EAAE,QAuDpC,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isEastExprType } from "../east-type.js";
|
|
2
|
+
import { matchBlockBuilderCall } from "../block-builder.js";
|
|
3
|
+
import { enclosingBlockScope } from "../block-scope.js";
|
|
4
|
+
const NAME = "no-reinlined-east-binding";
|
|
5
|
+
const CODE = 990010;
|
|
6
|
+
// An East `Expr` is a value tree, not a slot. Bound to a JS `const`/`let` and
|
|
7
|
+
// referenced more than once inside the same East block, it is *re-inlined* at
|
|
8
|
+
// each use — the tree is duplicated, so it is re-evaluated per use and (for
|
|
9
|
+
// mutable values) gets a fresh identity each time. `$.let`/`$.const` introduce
|
|
10
|
+
// a single block binding evaluated once and referenced by name, which is the
|
|
11
|
+
// only way to get single-evaluation + shared identity. So multi-use JS bindings
|
|
12
|
+
// of an `Expr` are an error, not a style nit.
|
|
13
|
+
//
|
|
14
|
+
// Scoped to the hazard: a single use is a harmless alias / single-pass argument,
|
|
15
|
+
// and inline composition (`Stack.VStack([A(), B(), …])`) never binds, so neither
|
|
16
|
+
// trips. Re-inlining only duplicates within one East block tree, so the count is
|
|
17
|
+
// bucketed per enclosing East block scope (an `East.function` body or, for JSX
|
|
18
|
+
// authoring, a `<Reactive>{$ => …}}` / `ui(…)` BlockBuilder callback).
|
|
19
|
+
export const noReinlinedEastBinding = {
|
|
20
|
+
name: NAME,
|
|
21
|
+
code: CODE,
|
|
22
|
+
description: "An East Expr bound to a JS const/let and reused inside an East block is re-inlined per use — bind it once with $.let/$.const.",
|
|
23
|
+
check(node, ctx) {
|
|
24
|
+
const t = ctx.ts;
|
|
25
|
+
if (!t.isVariableDeclaration(node))
|
|
26
|
+
return;
|
|
27
|
+
if (!t.isIdentifier(node.name))
|
|
28
|
+
return;
|
|
29
|
+
if (node.initializer === undefined)
|
|
30
|
+
return;
|
|
31
|
+
let init = node.initializer;
|
|
32
|
+
while (t.isParenthesizedExpression(init))
|
|
33
|
+
init = init.expression;
|
|
34
|
+
// `$.let(...)` / `$.const(...)` is the correct form — never flag it.
|
|
35
|
+
if (matchBlockBuilderCall(init, ctx) !== undefined)
|
|
36
|
+
return;
|
|
37
|
+
// Aliasing an existing binding (`const a = b`) re-inlines nothing; the root
|
|
38
|
+
// binding, if unbound, is flagged at its own declaration.
|
|
39
|
+
if (t.isIdentifier(init))
|
|
40
|
+
return;
|
|
41
|
+
// Only Expr-typed bindings re-inline. Types (`StructType(...)`), plain JS
|
|
42
|
+
// objects, callbacks held for a single handoff, etc. are fine.
|
|
43
|
+
if (!isEastExprType(ctx.checker.getTypeAtLocation(init)))
|
|
44
|
+
return;
|
|
45
|
+
const declSymbol = ctx.checker.getSymbolAtLocation(node.name);
|
|
46
|
+
if (declSymbol === undefined)
|
|
47
|
+
return;
|
|
48
|
+
const name = node.name.text;
|
|
49
|
+
const perBody = new Map();
|
|
50
|
+
const visit = (n) => {
|
|
51
|
+
if (t.isIdentifier(n) && n !== node.name && n.text === name) {
|
|
52
|
+
if (ctx.checker.getSymbolAtLocation(n) === declSymbol) {
|
|
53
|
+
const body = enclosingBlockScope(n, ctx);
|
|
54
|
+
if (body !== undefined)
|
|
55
|
+
perBody.set(body, (perBody.get(body) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
t.forEachChild(n, visit);
|
|
59
|
+
};
|
|
60
|
+
visit(ctx.sourceFile);
|
|
61
|
+
let maxInBody = 0;
|
|
62
|
+
for (const count of perBody.values())
|
|
63
|
+
if (count > maxInBody)
|
|
64
|
+
maxInBody = count;
|
|
65
|
+
if (maxInBody < 2)
|
|
66
|
+
return;
|
|
67
|
+
const sf = ctx.sourceFile;
|
|
68
|
+
const start = node.getStart(sf);
|
|
69
|
+
ctx.report({
|
|
70
|
+
ruleName: NAME,
|
|
71
|
+
code: CODE,
|
|
72
|
+
start,
|
|
73
|
+
length: node.getEnd() - start,
|
|
74
|
+
messageText: "This East expression is bound to a JS `const`/`let` and used more than once inside an East block, so it is re-inlined — and re-evaluated, with a fresh identity for mutable values — at each use. Bind it once with `$.const`/`$.let`.",
|
|
75
|
+
category: "error",
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=no-reinlined-east-binding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-reinlined-east-binding.js","sourceRoot":"","sources":["../../../src/rules/no-reinlined-east-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-relative-src-import.d.ts","sourceRoot":"","sources":["../../../src/rules/no-relative-src-import.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAW5C,eAAO,MAAM,mBAAmB,EAAE,QA6BjC,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const NAME = "no-relative-src-import";
|
|
2
|
+
const CODE = 990007;
|
|
3
|
+
const DEEP_PACKAGE_SRC = /^@elaraai\/[^/]+\/src(\/|$)/;
|
|
4
|
+
const RELATIVE_SRC = /\/src(\/|$)/;
|
|
5
|
+
// Tests and examples must import East packages by their published name
|
|
6
|
+
// (`@elaraai/east`), not reach into a package's `src/` via a relative path or a
|
|
7
|
+
// deep `/src` specifier — those bypass the package's public API surface.
|
|
8
|
+
export const noRelativeSrcImport = {
|
|
9
|
+
name: NAME,
|
|
10
|
+
code: CODE,
|
|
11
|
+
description: "Import East packages by published name, not via ../src or a deep /src path.",
|
|
12
|
+
check(node, ctx) {
|
|
13
|
+
const t = ctx.ts;
|
|
14
|
+
let specifier;
|
|
15
|
+
if (t.isImportDeclaration(node))
|
|
16
|
+
specifier = node.moduleSpecifier;
|
|
17
|
+
else if (t.isExportDeclaration(node))
|
|
18
|
+
specifier = node.moduleSpecifier;
|
|
19
|
+
else
|
|
20
|
+
return;
|
|
21
|
+
if (specifier === undefined || !t.isStringLiteral(specifier))
|
|
22
|
+
return;
|
|
23
|
+
const text = specifier.text;
|
|
24
|
+
const relativeIntoSrc = text.startsWith(".") && RELATIVE_SRC.test(text);
|
|
25
|
+
const deepPackageSrc = DEEP_PACKAGE_SRC.test(text);
|
|
26
|
+
if (!relativeIntoSrc && !deepPackageSrc)
|
|
27
|
+
return;
|
|
28
|
+
const sf = ctx.sourceFile;
|
|
29
|
+
const start = specifier.getStart(sf);
|
|
30
|
+
ctx.report({
|
|
31
|
+
ruleName: NAME,
|
|
32
|
+
code: CODE,
|
|
33
|
+
start,
|
|
34
|
+
length: specifier.getEnd() - start,
|
|
35
|
+
messageText: "Import East packages by their published name (e.g. `@elaraai/east`), not a relative `../src/...` path or a deep `/src` import.",
|
|
36
|
+
category: "warning",
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=no-relative-src-import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-relative-src-import.js","sourceRoot":"","sources":["../../../src/rules/no-relative-src-import.ts"],"names":[],"mappings":"AAOA,MAAM,IAAI,GAAG,wBAAwB,CAAC;AACtC,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AACvD,MAAM,YAAY,GAAG,aAAa,CAAC;AAEnC,uEAAuE;AACvE,gFAAgF;AAChF,yEAAyE;AACzE,MAAM,CAAC,MAAM,mBAAmB,GAAa;IAC3C,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,WAAW,EAAE,6EAA6E;IAC1F,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QACjB,IAAI,SAAoC,CAAC;QACzC,IAAI,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC;YAAE,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC;aAC7D,IAAI,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC;YAAE,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC;;YAClE,OAAO;QAEZ,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,eAAe,CAAC,SAAS,CAAC;YAAE,OAAO;QACrE,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC;QAC5B,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxE,MAAM,cAAc,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,eAAe,IAAI,CAAC,cAAc;YAAE,OAAO;QAEhD,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC1B,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACrC,GAAG,CAAC,MAAM,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,IAAI;YACV,KAAK;YACL,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,KAAK;YAClC,WAAW,EACT,gIAAgI;YAClI,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-ts-helpers-in-east.d.ts","sourceRoot":"","sources":["../../../src/rules/no-ts-helpers-in-east.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAiB5C,eAAO,MAAM,iBAAiB,EAAE,QAsC/B,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isEastExprType } from "../east-type.js";
|
|
2
|
+
const NAME = "no-ts-helpers-in-east";
|
|
3
|
+
const CODE = 990010;
|
|
4
|
+
// A TypeScript helper that constructs East values — a `function`/`const` whose
|
|
5
|
+
// return type is an East `Expr` — should be inlined at each call site;
|
|
6
|
+
// repetition is preferred over a `node()`/`link()`-style builder.
|
|
7
|
+
//
|
|
8
|
+
// Keyed on *declarations* (function declarations and `const fn = (…) => …`),
|
|
9
|
+
// which excludes every inline callback (East.function bodies, `.map`, event
|
|
10
|
+
// handlers are bare arrow *arguments*, never declarations). Note: a component
|
|
11
|
+
// factory (`createButton(): ExprType<UIComponentType>`) is structurally
|
|
12
|
+
// identical, so this rule is APPLICATION-SCOPED — the daemon must not run it on
|
|
13
|
+
// East library-factory packages. Hence it is exported separately, not in the
|
|
14
|
+
// default `allRules`.
|
|
15
|
+
export const noTsHelpersInEast = {
|
|
16
|
+
name: NAME,
|
|
17
|
+
code: CODE,
|
|
18
|
+
description: "Disallow TypeScript helpers that build East values (functions returning an East expression); inline at the call site.",
|
|
19
|
+
check(node, ctx) {
|
|
20
|
+
const t = ctx.ts;
|
|
21
|
+
let fnLike;
|
|
22
|
+
let reportNode;
|
|
23
|
+
if (t.isFunctionDeclaration(node) && node.body !== undefined && node.name !== undefined) {
|
|
24
|
+
fnLike = node;
|
|
25
|
+
reportNode = node.name;
|
|
26
|
+
}
|
|
27
|
+
else if (t.isVariableDeclaration(node) &&
|
|
28
|
+
node.initializer !== undefined &&
|
|
29
|
+
(t.isArrowFunction(node.initializer) || t.isFunctionExpression(node.initializer))) {
|
|
30
|
+
fnLike = node.initializer;
|
|
31
|
+
reportNode = node.name;
|
|
32
|
+
}
|
|
33
|
+
if (fnLike === undefined || reportNode === undefined)
|
|
34
|
+
return;
|
|
35
|
+
const signature = ctx.checker.getSignatureFromDeclaration(fnLike);
|
|
36
|
+
if (signature === undefined || !isEastExprType(signature.getReturnType()))
|
|
37
|
+
return;
|
|
38
|
+
const sf = ctx.sourceFile;
|
|
39
|
+
const start = reportNode.getStart(sf);
|
|
40
|
+
ctx.report({
|
|
41
|
+
ruleName: NAME,
|
|
42
|
+
code: CODE,
|
|
43
|
+
start,
|
|
44
|
+
length: reportNode.getEnd() - start,
|
|
45
|
+
messageText: "This TypeScript helper builds an East value (it returns an East expression). Inline the construction at each call site instead of factoring it into a helper — repetition is preferred.",
|
|
46
|
+
category: "warning",
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=no-ts-helpers-in-east.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-ts-helpers-in-east.js","sourceRoot":"","sources":["../../../src/rules/no-ts-helpers-in-east.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,MAAM,IAAI,GAAG,uBAAuB,CAAC;AACrC,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB,+EAA+E;AAC/E,uEAAuE;AACvE,kEAAkE;AAClE,EAAE;AACF,6EAA6E;AAC7E,4EAA4E;AAC5E,8EAA8E;AAC9E,wEAAwE;AACxE,gFAAgF;AAChF,6EAA6E;AAC7E,sBAAsB;AACtB,MAAM,CAAC,MAAM,iBAAiB,GAAa;IACzC,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,WAAW,EACT,uHAAuH;IACzH,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QAEjB,IAAI,MAA2C,CAAC;QAChD,IAAI,UAA+B,CAAC;QACpC,IAAI,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACxF,MAAM,GAAG,IAAI,CAAC;YACd,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QACzB,CAAC;aAAM,IACL,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC;YAC7B,IAAI,CAAC,WAAW,KAAK,SAAS;YAC9B,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,EACjF,CAAC;YACD,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;YAC1B,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC;QACzB,CAAC;QACD,IAAI,MAAM,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS;YAAE,OAAO;QAE7D,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,2BAA2B,CAAC,MAAM,CAAC,CAAC;QAClE,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,aAAa,EAAE,CAAC;YAAE,OAAO;QAElF,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC1B,MAAM,KAAK,GAAG,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACtC,GAAG,CAAC,MAAM,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,IAAI;YACV,KAAK;YACL,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,GAAG,KAAK;YACnC,WAAW,EACT,yLAAyL;YAC3L,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unexecuted-east-expression.d.ts","sourceRoot":"","sources":["../../../src/rules/no-unexecuted-east-expression.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAY5C,eAAO,MAAM,0BAA0B,EAAE,QAuCxC,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isEastExprType, isBlockBuilderType } from "../east-type.js";
|
|
2
|
+
const NAME = "no-unexecuted-east-expression";
|
|
3
|
+
const CODE = 990009;
|
|
4
|
+
// A bare expression statement whose value is an East `Expr` is dead: East
|
|
5
|
+
// expressions are pure values, so unless executed with `$( … )` or bound with
|
|
6
|
+
// `$.let`/`$.const`, the statement has no effect. Catches both calling an
|
|
7
|
+
// effectful function without `$()` (`log(msg);`) and `East.value(x);` standing
|
|
8
|
+
// alone. The `$( … )` / `$.x( … )` execution forms return `void`, and are
|
|
9
|
+
// excluded explicitly so renamed builders never false-positive.
|
|
10
|
+
export const noUnexecutedEastExpression = {
|
|
11
|
+
name: NAME,
|
|
12
|
+
code: CODE,
|
|
13
|
+
description: "Flag a bare East expression statement that is never executed with $() or bound — it has no effect.",
|
|
14
|
+
check(node, ctx) {
|
|
15
|
+
const t = ctx.ts;
|
|
16
|
+
if (!t.isExpressionStatement(node))
|
|
17
|
+
return;
|
|
18
|
+
const expr = node.expression;
|
|
19
|
+
// Exclude any statement rooted in the block builder `$`: `$(...)`, `$.if(...)`,
|
|
20
|
+
// chained `$.if(...).else(...)`, `$.try(...).catch(...)`, etc. Walk down the
|
|
21
|
+
// call/member chain to its base receiver and check that.
|
|
22
|
+
let root = expr;
|
|
23
|
+
for (;;) {
|
|
24
|
+
if (t.isCallExpression(root)) {
|
|
25
|
+
root = root.expression;
|
|
26
|
+
}
|
|
27
|
+
else if (t.isPropertyAccessExpression(root) || t.isElementAccessExpression(root)) {
|
|
28
|
+
root = root.expression;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (isBlockBuilderType(ctx.checker.getTypeAtLocation(root)))
|
|
35
|
+
return;
|
|
36
|
+
if (!isEastExprType(ctx.checker.getTypeAtLocation(expr)))
|
|
37
|
+
return;
|
|
38
|
+
const sf = ctx.sourceFile;
|
|
39
|
+
const start = expr.getStart(sf);
|
|
40
|
+
ctx.report({
|
|
41
|
+
ruleName: NAME,
|
|
42
|
+
code: CODE,
|
|
43
|
+
start,
|
|
44
|
+
length: expr.getEnd() - start,
|
|
45
|
+
messageText: "This East expression is never executed or bound, so it has no effect. Wrap it in `$( … )` to run it for its effect, or bind it with `$.let` / `$.const`.",
|
|
46
|
+
category: "warning",
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=no-unexecuted-east-expression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unexecuted-east-expression.js","sourceRoot":"","sources":["../../../src/rules/no-unexecuted-east-expression.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErE,MAAM,IAAI,GAAG,+BAA+B,CAAC;AAC7C,MAAM,IAAI,GAAG,MAAM,CAAC;AAEpB,0EAA0E;AAC1E,8EAA8E;AAC9E,0EAA0E;AAC1E,+EAA+E;AAC/E,0EAA0E;AAC1E,gEAAgE;AAChE,MAAM,CAAC,MAAM,0BAA0B,GAAa;IAClD,IAAI,EAAE,IAAI;IACV,IAAI,EAAE,IAAI;IACV,WAAW,EACT,oGAAoG;IACtG,KAAK,CAAC,IAAI,EAAE,GAAG;QACb,MAAM,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;QACjB,IAAI,CAAC,CAAC,CAAC,qBAAqB,CAAC,IAAI,CAAC;YAAE,OAAO;QAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;QAE7B,gFAAgF;QAChF,6EAA6E;QAC7E,yDAAyD;QACzD,IAAI,IAAI,GAAY,IAAI,CAAC;QACzB,SAAS,CAAC;YACR,IAAI,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;YACzB,CAAC;iBAAM,IAAI,CAAC,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,yBAAyB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnF,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO;QAEpE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO;QAEjE,MAAM,EAAE,GAAG,GAAG,CAAC,UAAU,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QAChC,GAAG,CAAC,MAAM,CAAC;YACT,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,IAAI;YACV,KAAK;YACL,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,KAAK;YAC7B,WAAW,EACT,0JAA0J;YAC5J,QAAQ,EAAE,SAAS;SACpB,CAAC,CAAC;IACL,CAAC;CACF,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-explicit-east-type.d.ts","sourceRoot":"","sources":["../../../src/rules/prefer-explicit-east-type.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAY,MAAM,aAAa,CAAC;AAiCtD,eAAO,MAAM,sBAAsB,EAAE,QAiCpC,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { matchBlockBuilderCall } from "../block-builder.js";
|
|
2
|
+
const NAME = "prefer-explicit-east-type";
|
|
3
|
+
const CODE = 990002;
|
|
4
|
+
function isEmptyNewMapOrSet(node, t) {
|
|
5
|
+
if (!t.isNewExpression(node) || !t.isIdentifier(node.expression))
|
|
6
|
+
return false;
|
|
7
|
+
const ctor = node.expression.text;
|
|
8
|
+
if (ctor !== "Map" && ctor !== "Set")
|
|
9
|
+
return false;
|
|
10
|
+
return node.arguments === undefined || node.arguments.length === 0;
|
|
11
|
+
}
|
|
12
|
+
function isRawValueLiteral(node, t) {
|
|
13
|
+
return (t.isNumericLiteral(node) ||
|
|
14
|
+
t.isBigIntLiteral(node) ||
|
|
15
|
+
t.isStringLiteralLike(node) ||
|
|
16
|
+
node.kind === t.SyntaxKind.TrueKeyword ||
|
|
17
|
+
node.kind === t.SyntaxKind.FalseKeyword ||
|
|
18
|
+
node.kind === t.SyntaxKind.NullKeyword ||
|
|
19
|
+
t.isArrayLiteralExpression(node) ||
|
|
20
|
+
t.isObjectLiteralExpression(node) ||
|
|
21
|
+
t.isNewExpression(node));
|
|
22
|
+
}
|
|
23
|
+
// The one-arg `$.let(x)` / `$.const(x)` form infers the East type from the JS
|
|
24
|
+
// value. That is fine when `x` is already an East expression (type fully
|
|
25
|
+
// determined), but for an under-determined literal — `[]`, `{}`, `new Map()` —
|
|
26
|
+
// the inferred type is `never`/empty and almost always wrong. Nudge toward the
|
|
27
|
+
// explicit two-arg form. Stays silent on East expressions so `$.let(arr.sum())`
|
|
28
|
+
// is not flagged.
|
|
29
|
+
export const preferExplicitEastType = {
|
|
30
|
+
name: NAME,
|
|
31
|
+
code: CODE,
|
|
32
|
+
description: "Encourage passing the East type as the second argument to $.let/$.const for raw JS values whose East type is under-determined.",
|
|
33
|
+
check(node, ctx) {
|
|
34
|
+
const match = matchBlockBuilderCall(node, ctx);
|
|
35
|
+
if (match === undefined || match.args.length !== 1)
|
|
36
|
+
return;
|
|
37
|
+
const t = ctx.ts;
|
|
38
|
+
const value = match.args[0];
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return;
|
|
41
|
+
const underDetermined = (t.isArrayLiteralExpression(value) && value.elements.length === 0) ||
|
|
42
|
+
(t.isObjectLiteralExpression(value) && value.properties.length === 0) ||
|
|
43
|
+
isEmptyNewMapOrSet(value, t);
|
|
44
|
+
const mode = ctx.options.preferExplicitEastType?.mode ?? "under-determined";
|
|
45
|
+
const flag = underDetermined || (mode === "all-raw-values" && isRawValueLiteral(value, t));
|
|
46
|
+
if (!flag)
|
|
47
|
+
return;
|
|
48
|
+
const sf = ctx.sourceFile;
|
|
49
|
+
const start = value.getStart(sf);
|
|
50
|
+
ctx.report({
|
|
51
|
+
ruleName: NAME,
|
|
52
|
+
code: CODE,
|
|
53
|
+
start,
|
|
54
|
+
length: value.getEnd() - start,
|
|
55
|
+
messageText: `Provide the East type as the second argument to \`$.${match.method}\` — e.g. \`$.${match.method}([], ArrayType(FloatType))\` — to pin the value type; it is under-determined from the value alone.`,
|
|
56
|
+
category: "suggestion",
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=prefer-explicit-east-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-explicit-east-type.js","sourceRoot":"","sources":["../../../src/rules/prefer-explicit-east-type.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-jsx-over-factory-call.d.ts","sourceRoot":"","sources":["../../../src/rules/prefer-jsx-over-factory-call.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAyB,MAAM,aAAa,CAAC;AAoHnE,eAAO,MAAM,wBAAwB,EAAE,QA0CtC,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const NAME = "prefer-jsx-over-factory-call";
|
|
2
|
+
const CODE = 990012;
|
|
3
|
+
// The signal that a `Foo.Root(...)` factory call has a `<Foo>` JSX tag form is
|
|
4
|
+
// not the import path — it's the RESULT TYPE. Every UI component factory's
|
|
5
|
+
// `.Root(...)` returns the same type a JSX element evaluates to (`JSX.Element`
|
|
6
|
+
// for this file's JSX runtime). So a `.Root(...)` whose result is that element
|
|
7
|
+
// type is, by construction, authorable as a tag — independent of which package
|
|
8
|
+
// it came from. This keys on the type, so it generalises to any JSX-producing
|
|
9
|
+
// factory and needs no per-package list.
|
|
10
|
+
// Resolving `JSX.Element` walks the filesystem, so memoize it per file (the
|
|
11
|
+
// SourceFile is rebuilt on every program, so the key is naturally invalidated).
|
|
12
|
+
// `null` records "no JSX element type here" so we don't re-resolve on each call.
|
|
13
|
+
const jsxElementCache = new WeakMap();
|
|
14
|
+
/** The `JSX.Element` type for `sourceFile`'s active JSX runtime, or undefined. */
|
|
15
|
+
function jsxElementType(ctx) {
|
|
16
|
+
const cached = jsxElementCache.get(ctx.sourceFile);
|
|
17
|
+
if (cached !== undefined)
|
|
18
|
+
return cached ?? undefined;
|
|
19
|
+
const computed = computeJsxElementType(ctx);
|
|
20
|
+
jsxElementCache.set(ctx.sourceFile, computed ?? null);
|
|
21
|
+
return computed;
|
|
22
|
+
}
|
|
23
|
+
function computeJsxElementType(ctx) {
|
|
24
|
+
const t = ctx.ts;
|
|
25
|
+
const elementOf = (ns) => {
|
|
26
|
+
const el = ctx.checker.getExportsOfModule(ns).find((s) => s.name === "Element");
|
|
27
|
+
return el ? ctx.checker.getDeclaredTypeOfSymbol(el) : undefined;
|
|
28
|
+
};
|
|
29
|
+
// Classic / global runtime: a visible `JSX` namespace (global or imported).
|
|
30
|
+
const resolveName = ctx.checker.resolveName;
|
|
31
|
+
const globalNs = resolveName?.("JSX", ctx.sourceFile, t.SymbolFlags.Namespace, false);
|
|
32
|
+
if (globalNs !== undefined) {
|
|
33
|
+
const fromGlobal = elementOf(globalNs);
|
|
34
|
+
if (fromGlobal !== undefined)
|
|
35
|
+
return fromGlobal;
|
|
36
|
+
}
|
|
37
|
+
// Automatic runtime (`jsx: react-jsx` + `jsxImportSource`): the `JSX` namespace
|
|
38
|
+
// lives in `<source>/jsx-runtime`, not globally. Resolve that module and read
|
|
39
|
+
// its `JSX.Element`. Needs the program for compiler options + resolution.
|
|
40
|
+
const program = ctx.program;
|
|
41
|
+
if (program === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
const options = program.getCompilerOptions();
|
|
44
|
+
const base = jsxImportSourceFor(options, ctx.sourceFile, t);
|
|
45
|
+
if (base === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
// Resolve exactly as TypeScript does. The host checks the program's loaded
|
|
48
|
+
// source files first (so in-memory programs resolve), then falls through to
|
|
49
|
+
// `ts.sys` for everything else on disk (package.json `exports`, node_modules).
|
|
50
|
+
const resolutionHost = {
|
|
51
|
+
fileExists: (f) => program.getSourceFile(f) !== undefined || t.sys.fileExists(f),
|
|
52
|
+
readFile: (f) => program.getSourceFile(f)?.text ?? t.sys.readFile(f),
|
|
53
|
+
directoryExists: (d) => t.sys.directoryExists(d),
|
|
54
|
+
getCurrentDirectory: () => t.sys.getCurrentDirectory(),
|
|
55
|
+
getDirectories: (d) => t.sys.getDirectories(d),
|
|
56
|
+
};
|
|
57
|
+
if (t.sys.realpath !== undefined)
|
|
58
|
+
resolutionHost.realpath = (p) => t.sys.realpath(p);
|
|
59
|
+
const resolved = t.resolveModuleName(`${base}/jsx-runtime`, ctx.sourceFile.fileName, options, resolutionHost).resolvedModule?.resolvedFileName;
|
|
60
|
+
if (resolved === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
const runtimeSf = program.getSourceFile(resolved);
|
|
63
|
+
const moduleSym = runtimeSf ? ctx.checker.getSymbolAtLocation(runtimeSf) : undefined;
|
|
64
|
+
if (moduleSym === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const jsxNs = ctx.checker.getExportsOfModule(moduleSym).find((s) => s.name === "JSX");
|
|
67
|
+
return jsxNs ? elementOf(jsxNs) : undefined;
|
|
68
|
+
}
|
|
69
|
+
/** The JSX import source in effect for `sourceFile` (per-file pragma overrides). */
|
|
70
|
+
function jsxImportSourceFor(options, sourceFile, t) {
|
|
71
|
+
const pragmas = sourceFile.pragmas;
|
|
72
|
+
const pragma = pragmas?.get("jsximportsource");
|
|
73
|
+
const fromPragma = Array.isArray(pragma)
|
|
74
|
+
? pragma[pragma.length - 1]?.arguments?.factory
|
|
75
|
+
: pragma?.arguments?.factory;
|
|
76
|
+
if (fromPragma !== undefined)
|
|
77
|
+
return fromPragma;
|
|
78
|
+
// Only the automatic runtime carries an implicit import source.
|
|
79
|
+
if (options.jsx !== t.JsxEmit.ReactJSX && options.jsx !== t.JsxEmit.ReactJSXDev)
|
|
80
|
+
return undefined;
|
|
81
|
+
return options.jsxImportSource ?? "react";
|
|
82
|
+
}
|
|
83
|
+
/** True when `a` and `b` are the same type (mutually assignable). */
|
|
84
|
+
function sameType(a, b, checker) {
|
|
85
|
+
const c = checker;
|
|
86
|
+
if (c.isTypeAssignableTo === undefined)
|
|
87
|
+
return a === b; // older TS: identity only
|
|
88
|
+
return c.isTypeAssignableTo(a, b) && c.isTypeAssignableTo(b, a);
|
|
89
|
+
}
|
|
90
|
+
// In a `.tsx` file, `Foo.Root(...)` on a plain identifier whose result is the
|
|
91
|
+
// file's JSX element type is the un-migrated form — author it as `<Foo>`. Gated:
|
|
92
|
+
// JSX file only; `Ident.Root(...)` on a bare identifier (so `Slice.Frame.Root(...)`
|
|
93
|
+
// / `Slice.config(...)` never match); and the call's result type is JSX.Element
|
|
94
|
+
// (so a `.Root` returning anything else — a config object, a number — is silent).
|
|
95
|
+
export const preferJsxOverFactoryCall = {
|
|
96
|
+
name: NAME,
|
|
97
|
+
code: CODE,
|
|
98
|
+
description: "In a .tsx file, prefer the <Foo> JSX tag over a factory's Foo.Root(...) when the call produces a JSX element.",
|
|
99
|
+
check(node, ctx) {
|
|
100
|
+
const t = ctx.ts;
|
|
101
|
+
if (ctx.sourceFile.languageVariant !== t.LanguageVariant.JSX)
|
|
102
|
+
return;
|
|
103
|
+
if (!t.isCallExpression(node))
|
|
104
|
+
return;
|
|
105
|
+
const callee = node.expression;
|
|
106
|
+
if (!t.isPropertyAccessExpression(callee))
|
|
107
|
+
return;
|
|
108
|
+
if (callee.name.text !== "Root")
|
|
109
|
+
return;
|
|
110
|
+
if (!t.isIdentifier(callee.expression))
|
|
111
|
+
return;
|
|
112
|
+
const factoryIdent = callee.expression;
|
|
113
|
+
const element = jsxElementType(ctx);
|
|
114
|
+
if (element === undefined)
|
|
115
|
+
return;
|
|
116
|
+
const result = ctx.checker.getTypeAtLocation(node);
|
|
117
|
+
if (!sameType(result, element, ctx.checker))
|
|
118
|
+
return;
|
|
119
|
+
// The tag is named after the factory's *exported* name, not the local
|
|
120
|
+
// binding — so an aliased import (`import { Box as BoxFactory }`) still
|
|
121
|
+
// suggests `<Box>`, not `<BoxFactory>`.
|
|
122
|
+
let tagName = factoryIdent.text;
|
|
123
|
+
const sym = ctx.checker.getSymbolAtLocation(factoryIdent);
|
|
124
|
+
if (sym !== undefined && (sym.flags & t.SymbolFlags.Alias) !== 0) {
|
|
125
|
+
const target = ctx.checker.getAliasedSymbol(sym);
|
|
126
|
+
if (target.name.length > 0)
|
|
127
|
+
tagName = target.name;
|
|
128
|
+
}
|
|
129
|
+
const sf = ctx.sourceFile;
|
|
130
|
+
const start = callee.getStart(sf);
|
|
131
|
+
ctx.report({
|
|
132
|
+
ruleName: NAME,
|
|
133
|
+
code: CODE,
|
|
134
|
+
start,
|
|
135
|
+
length: callee.getEnd() - start,
|
|
136
|
+
messageText: `Author this with the \`<${tagName}>\` JSX tag instead of \`${factoryIdent.text}.Root(...)\` — in a .tsx file the JSX tag is the authoring surface (the call already produces a JSX element).`,
|
|
137
|
+
category: "suggestion",
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=prefer-jsx-over-factory-call.js.map
|