@timmo001/oxlint-rules 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -4
- package/dist/cli.js +462 -55
- package/dist/configs/recommended-effect.js +498 -95
- package/dist/configs/recommended.js +362 -14
- package/dist/effect/index.js +95 -11
- package/dist/generic/index.js +356 -13
- package/package.json +8 -6
- package/src/effect/index.ts +2 -0
- package/src/effect/rules/no-try-catch-in-effect-generators.ts +4 -98
- package/src/effect/rules/prefer-platform-services.ts +105 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/rules/no-explicit-inferable-type-predicate.ts +146 -0
- package/src/generic/rules/no-unchecked-query-selector-type.ts +48 -0
- package/src/generic/rules/no-untyped-custom-event.ts +31 -0
- package/src/generic/rules/prefer-event-parameter-type.ts +36 -35
- package/src/generic/rules/prefer-lit-query-decorators.ts +84 -0
- package/src/generic/rules/prefer-types-over-assertions.ts +39 -0
- package/src/shared/ast.ts +31 -0
- package/src/shared/effect.ts +72 -0
- package/src/shared/json.ts +11 -0
- package/src/shared/lit.ts +219 -0
- package/src/shared/scope.ts +1 -0
package/dist/cli.js
CHANGED
|
@@ -2548,8 +2548,86 @@ var src_default = antiSlopPlugin;
|
|
|
2548
2548
|
// src/generic/index.ts
|
|
2549
2549
|
import { eslintCompatPlugin as eslintCompatPlugin2 } from "@oxlint/plugins";
|
|
2550
2550
|
|
|
2551
|
-
// src/generic/rules/
|
|
2551
|
+
// src/generic/rules/no-explicit-inferable-type-predicate.ts
|
|
2552
2552
|
import { defineRule as defineRule18 } from "@oxlint/plugins";
|
|
2553
|
+
function isNamed(node, name) {
|
|
2554
|
+
return node.type === "Identifier" && node.name === name;
|
|
2555
|
+
}
|
|
2556
|
+
function isNullish(node) {
|
|
2557
|
+
return node.type === "Literal" && node.value === null || isNamed(node, "undefined");
|
|
2558
|
+
}
|
|
2559
|
+
function isNullishExclusion(node, name) {
|
|
2560
|
+
return node.type === "BinaryExpression" && (node.operator === "!==" || node.operator === "!=") && (isNamed(node.left, name) && isNullish(node.right) || isNullish(node.left) && isNamed(node.right, name));
|
|
2561
|
+
}
|
|
2562
|
+
function isTypeofCheck(node, name) {
|
|
2563
|
+
if (node.type !== "BinaryExpression" || node.operator !== "===" && node.operator !== "==") {
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2566
|
+
const isTypeof = (side) => side.type === "UnaryExpression" && side.operator === "typeof" && isNamed(side.argument, name);
|
|
2567
|
+
const isLiteral = (side) => side.type === "Literal";
|
|
2568
|
+
return isTypeof(node.left) && isLiteral(node.right) || isLiteral(node.left) && isTypeof(node.right);
|
|
2569
|
+
}
|
|
2570
|
+
function isInferableCheck(node, name) {
|
|
2571
|
+
if (node.type === "LogicalExpression") {
|
|
2572
|
+
return node.operator === "&&" && isNullishExclusion(node.left, name) && isNullishExclusion(node.right, name);
|
|
2573
|
+
}
|
|
2574
|
+
return isNullishExclusion(node, name) || isTypeofCheck(node, name) || node.type === "BinaryExpression" && node.operator === "instanceof" && isNamed(node.left, name);
|
|
2575
|
+
}
|
|
2576
|
+
function returnedExpression(owner) {
|
|
2577
|
+
const body = owner.body;
|
|
2578
|
+
if (!body)
|
|
2579
|
+
return null;
|
|
2580
|
+
if (body.type !== "BlockStatement")
|
|
2581
|
+
return body;
|
|
2582
|
+
const [statement] = body.body;
|
|
2583
|
+
return body.body.length === 1 && statement?.type === "ReturnStatement" ? statement.argument : null;
|
|
2584
|
+
}
|
|
2585
|
+
function isPublicSignature(owner) {
|
|
2586
|
+
const parent = owner.parent;
|
|
2587
|
+
if (parent.type === "MethodDefinition" || parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") {
|
|
2588
|
+
return true;
|
|
2589
|
+
}
|
|
2590
|
+
return parent.type === "VariableDeclarator" && parent.parent.parent?.type === "ExportNamedDeclaration";
|
|
2591
|
+
}
|
|
2592
|
+
var noExplicitInferableTypePredicateRule = defineRule18({
|
|
2593
|
+
meta: {
|
|
2594
|
+
type: "suggestion",
|
|
2595
|
+
docs: {
|
|
2596
|
+
description: "Disallow explicit type predicates on simple null, typeof, or instanceof checks that TypeScript 5.5+ infers."
|
|
2597
|
+
},
|
|
2598
|
+
messages: {
|
|
2599
|
+
removeTypePredicate: "TypeScript infers the type predicate for `{{parameter}}` from this check, while an explicit predicate is never checked against the body. Remove the annotation and rely on the inferred predicate."
|
|
2600
|
+
}
|
|
2601
|
+
},
|
|
2602
|
+
create(context) {
|
|
2603
|
+
const check = (owner) => {
|
|
2604
|
+
const annotation = owner.returnType?.typeAnnotation;
|
|
2605
|
+
if (annotation?.type !== "TSTypePredicate" || annotation.asserts || annotation.parameterName.type !== "Identifier" || isPublicSignature(owner)) {
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
const parameter = annotation.parameterName.name;
|
|
2609
|
+
const expression = returnedExpression(owner);
|
|
2610
|
+
if (!owner.params.some((param) => isNamed(param, parameter)) || !expression || !isInferableCheck(expression, parameter)) {
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
context.report({
|
|
2614
|
+
node: annotation,
|
|
2615
|
+
messageId: "removeTypePredicate",
|
|
2616
|
+
data: { parameter }
|
|
2617
|
+
});
|
|
2618
|
+
};
|
|
2619
|
+
return {
|
|
2620
|
+
ArrowFunctionExpression: check,
|
|
2621
|
+
FunctionDeclaration: check,
|
|
2622
|
+
FunctionExpression: check
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
});
|
|
2626
|
+
|
|
2627
|
+
// src/generic/rules/no-unchecked-query-selector-type.ts
|
|
2628
|
+
import { defineRule as defineRule19 } from "@oxlint/plugins";
|
|
2629
|
+
|
|
2630
|
+
// src/shared/ast.ts
|
|
2553
2631
|
function nearestEnclosingFunction(node) {
|
|
2554
2632
|
let current = node.parent;
|
|
2555
2633
|
while (current) {
|
|
@@ -2560,30 +2638,204 @@ function nearestEnclosingFunction(node) {
|
|
|
2560
2638
|
}
|
|
2561
2639
|
return null;
|
|
2562
2640
|
}
|
|
2563
|
-
function
|
|
2564
|
-
|
|
2565
|
-
|
|
2641
|
+
function staticMemberName(node) {
|
|
2642
|
+
if (node.type !== "MemberExpression" || node.computed)
|
|
2643
|
+
return null;
|
|
2644
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/shared/json.ts
|
|
2648
|
+
function jsonObject(value) {
|
|
2649
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// src/shared/lit.ts
|
|
2653
|
+
var renderRoots = new Set(["renderRoot", "shadowRoot"]);
|
|
2654
|
+
function litElementBaseClasses(settings) {
|
|
2655
|
+
const configured = jsonObject(settings.lit)?.elementBaseClasses;
|
|
2656
|
+
return new Set([
|
|
2657
|
+
"LitElement",
|
|
2658
|
+
...Array.isArray(configured) ? configured.map(String) : []
|
|
2659
|
+
]);
|
|
2660
|
+
}
|
|
2661
|
+
function isWrapperExpression(node) {
|
|
2662
|
+
return node?.type === "ChainExpression" || node?.type === "ParenthesizedExpression" || node?.type === "TSAsExpression" || node?.type === "TSNonNullExpression" || node?.type === "TSSatisfiesExpression" || node?.type === "TSTypeAssertion";
|
|
2663
|
+
}
|
|
2664
|
+
function unwrapExpression2(node) {
|
|
2665
|
+
let current = node;
|
|
2666
|
+
while (isWrapperExpression(current))
|
|
2667
|
+
current = current.expression;
|
|
2668
|
+
return current;
|
|
2669
|
+
}
|
|
2670
|
+
function extendsLitBase(node, bases) {
|
|
2671
|
+
if (node.type === "Identifier")
|
|
2672
|
+
return bases.has(node.name);
|
|
2673
|
+
return node.type === "CallExpression" && node.arguments.some((argument) => extendsLitBase(argument, bases));
|
|
2674
|
+
}
|
|
2675
|
+
function isLitClass(node, bases) {
|
|
2676
|
+
const hasCustomElementDecorator = node.decorators.some((decorator) => decorator.expression.type === "CallExpression" && decorator.expression.callee.type === "Identifier" && decorator.expression.callee.name === "customElement");
|
|
2677
|
+
return hasCustomElementDecorator || node.superClass !== null && extendsLitBase(node.superClass, bases);
|
|
2678
|
+
}
|
|
2679
|
+
function classOfBody(body) {
|
|
2680
|
+
const owner = body.parent;
|
|
2681
|
+
return owner?.type === "ClassDeclaration" || owner?.type === "ClassExpression" ? owner : null;
|
|
2682
|
+
}
|
|
2683
|
+
function thisClass(node) {
|
|
2684
|
+
let current = node;
|
|
2685
|
+
while (current.parent) {
|
|
2686
|
+
const parent = current.parent;
|
|
2687
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression") {
|
|
2688
|
+
const owner = parent.parent;
|
|
2689
|
+
return (owner?.type === "MethodDefinition" || owner?.type === "TSAbstractMethodDefinition") && !owner.static && owner.value === parent ? classOfBody(owner.parent) : null;
|
|
2690
|
+
}
|
|
2691
|
+
if ((parent.type === "PropertyDefinition" || parent.type === "AccessorProperty") && parent.value === current) {
|
|
2692
|
+
return parent.static ? null : classOfBody(parent.parent);
|
|
2693
|
+
}
|
|
2694
|
+
if (parent.type === "StaticBlock" || parent.type === "ClassBody" || parent.type === "Program") {
|
|
2695
|
+
return null;
|
|
2696
|
+
}
|
|
2697
|
+
current = parent;
|
|
2698
|
+
}
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
function isStaticSelector(node) {
|
|
2702
|
+
return node?.type === "Literal" || node?.type === "TemplateLiteral" && node.expressions.length === 0;
|
|
2703
|
+
}
|
|
2704
|
+
function renderRootQuery(node, bases) {
|
|
2705
|
+
const method = staticMemberName(node.callee);
|
|
2706
|
+
if (method !== "querySelector" && method !== "querySelectorAll" || node.callee.type !== "MemberExpression" || !isStaticSelector(node.arguments[0])) {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
const receiver = unwrapExpression2(node.callee.object);
|
|
2710
|
+
const root = staticMemberName(receiver);
|
|
2711
|
+
if (!root || !renderRoots.has(root) || receiver.type !== "MemberExpression" || receiver.object.type !== "ThisExpression") {
|
|
2566
2712
|
return null;
|
|
2567
2713
|
}
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2714
|
+
const owner = thisClass(receiver.object);
|
|
2715
|
+
return owner && isLitClass(owner, bases) ? { method, root } : null;
|
|
2716
|
+
}
|
|
2717
|
+
function isAssignedMethod(name) {
|
|
2718
|
+
return name === "assignedElements" || name === "assignedNodes";
|
|
2719
|
+
}
|
|
2720
|
+
function feedsAssignedCall(node) {
|
|
2721
|
+
let current = node;
|
|
2722
|
+
while (isWrapperExpression(current.parent))
|
|
2723
|
+
current = current.parent;
|
|
2724
|
+
const member = current.parent;
|
|
2725
|
+
return member?.type === "MemberExpression" && member.object === current && isAssignedMethod(staticMemberName(member));
|
|
2726
|
+
}
|
|
2727
|
+
function litQueryDecoratorCall(node, bases) {
|
|
2728
|
+
const method = staticMemberName(node.callee);
|
|
2729
|
+
if (!isAssignedMethod(method)) {
|
|
2730
|
+
return feedsAssignedCall(node) ? null : renderRootQuery(node, bases);
|
|
2731
|
+
}
|
|
2732
|
+
if (node.callee.type !== "MemberExpression")
|
|
2733
|
+
return null;
|
|
2734
|
+
const inner = unwrapExpression2(node.callee.object);
|
|
2735
|
+
if (inner.type !== "CallExpression")
|
|
2736
|
+
return null;
|
|
2737
|
+
const query = renderRootQuery(inner, bases);
|
|
2738
|
+
return query?.method === "querySelector" ? { method, root: query.root } : null;
|
|
2739
|
+
}
|
|
2740
|
+
function isLitRenderRootQuery(node, bases) {
|
|
2741
|
+
return renderRootQuery(node, bases) !== null;
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// src/generic/rules/no-unchecked-query-selector-type.ts
|
|
2745
|
+
var queryMethods = new Set(["closest", "querySelector", "querySelectorAll"]);
|
|
2746
|
+
var noUncheckedQuerySelectorTypeRule = defineRule19({
|
|
2747
|
+
meta: {
|
|
2748
|
+
type: "problem",
|
|
2749
|
+
docs: {
|
|
2750
|
+
description: "Disallow type arguments on `querySelector`, `querySelectorAll`, and `closest`, which cast the result without checking it."
|
|
2751
|
+
},
|
|
2752
|
+
messages: {
|
|
2753
|
+
checkQueryResult: "`{{method}}<T>` casts the result without checking it. Check it with `instanceof`, use a checked lookup helper, or query a tag name so TypeScript infers the element type."
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
create(context) {
|
|
2757
|
+
const bases = litElementBaseClasses(context.settings);
|
|
2758
|
+
return {
|
|
2759
|
+
CallExpression(node) {
|
|
2760
|
+
const method = staticMemberName(node.callee);
|
|
2761
|
+
if (!node.typeArguments || !method || !queryMethods.has(method) || isLitRenderRootQuery(node, bases)) {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
context.report({
|
|
2765
|
+
node: node.typeArguments,
|
|
2766
|
+
messageId: "checkQueryResult",
|
|
2767
|
+
data: { method }
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
|
|
2774
|
+
// src/generic/rules/no-untyped-custom-event.ts
|
|
2775
|
+
import { defineRule as defineRule20 } from "@oxlint/plugins";
|
|
2776
|
+
var noUntypedCustomEventRule = defineRule20({
|
|
2777
|
+
meta: {
|
|
2778
|
+
type: "problem",
|
|
2779
|
+
docs: {
|
|
2780
|
+
description: "Disallow `CustomEvent` type references without a detail type, which leave `detail` as `any`."
|
|
2781
|
+
},
|
|
2782
|
+
messages: {
|
|
2783
|
+
typeCustomEventDetail: "`CustomEvent` without a type argument leaves `detail` as `any`. Reuse the project's typed event helper if one fits, or add one, instead of a bare `CustomEvent`."
|
|
2784
|
+
}
|
|
2785
|
+
},
|
|
2786
|
+
create(context) {
|
|
2787
|
+
return {
|
|
2788
|
+
TSTypeReference(node) {
|
|
2789
|
+
if (node.typeName.type !== "Identifier" || node.typeName.name !== "CustomEvent" || node.typeArguments) {
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
context.report({ node, messageId: "typeCustomEventDetail" });
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
// src/generic/rules/prefer-event-parameter-type.ts
|
|
2799
|
+
import { defineRule as defineRule21 } from "@oxlint/plugins";
|
|
2800
|
+
function staticMemberPath(node) {
|
|
2801
|
+
const path = [];
|
|
2802
|
+
let current = node.type === "ChainExpression" ? node.expression : node;
|
|
2803
|
+
while (current.type === "MemberExpression") {
|
|
2804
|
+
if (current.computed || current.property.type !== "Identifier") {
|
|
2805
|
+
return null;
|
|
2806
|
+
}
|
|
2807
|
+
path.unshift(current.property.name);
|
|
2808
|
+
current = current.object;
|
|
2809
|
+
}
|
|
2810
|
+
return current.type === "Identifier" && path.length > 0 ? { root: current.name, path } : null;
|
|
2811
|
+
}
|
|
2812
|
+
function isEventMemberPath(path) {
|
|
2813
|
+
const [first] = path;
|
|
2814
|
+
if (first === "detail")
|
|
2815
|
+
return true;
|
|
2816
|
+
return path.length === 1 && (first === "currentTarget" || first === "target");
|
|
2817
|
+
}
|
|
2818
|
+
function assertedEventParameter(node) {
|
|
2819
|
+
const member = staticMemberPath(node.expression);
|
|
2820
|
+
if (!member || !isEventMemberPath(member.path))
|
|
2821
|
+
return null;
|
|
2570
2822
|
const owner = nearestEnclosingFunction(node);
|
|
2571
|
-
if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name ===
|
|
2823
|
+
if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name === member.root)) {
|
|
2572
2824
|
return null;
|
|
2573
2825
|
}
|
|
2574
2826
|
return {
|
|
2575
|
-
parameter:
|
|
2576
|
-
property
|
|
2827
|
+
parameter: member.root,
|
|
2828
|
+
property: member.path.join(".")
|
|
2577
2829
|
};
|
|
2578
2830
|
}
|
|
2579
|
-
var preferEventParameterTypeRule =
|
|
2831
|
+
var preferEventParameterTypeRule = defineRule21({
|
|
2580
2832
|
meta: {
|
|
2581
2833
|
type: "suggestion",
|
|
2582
2834
|
docs: {
|
|
2583
|
-
description: "Prefer typing event target properties in the handler parameter instead of asserting them at use sites."
|
|
2835
|
+
description: "Prefer typing event target and detail properties in the handler parameter instead of asserting them at use sites."
|
|
2584
2836
|
},
|
|
2585
2837
|
messages: {
|
|
2586
|
-
typeEventParameter: "Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site."
|
|
2838
|
+
typeEventParameter: "Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site. Reuse the project's typed event helper if one fits, or add one."
|
|
2587
2839
|
}
|
|
2588
2840
|
},
|
|
2589
2841
|
create(context) {
|
|
@@ -2604,11 +2856,102 @@ var preferEventParameterTypeRule = defineRule18({
|
|
|
2604
2856
|
}
|
|
2605
2857
|
});
|
|
2606
2858
|
|
|
2859
|
+
// src/generic/rules/prefer-lit-query-decorators.ts
|
|
2860
|
+
import { defineRule as defineRule22 } from "@oxlint/plugins";
|
|
2861
|
+
var messageIds = {
|
|
2862
|
+
assignedElements: "preferQueryAssignedElements",
|
|
2863
|
+
assignedNodes: "preferQueryAssignedNodes",
|
|
2864
|
+
querySelector: "preferQuery",
|
|
2865
|
+
querySelectorAll: "preferQueryAll"
|
|
2866
|
+
};
|
|
2867
|
+
var preferLitQueryDecoratorsRule = defineRule22({
|
|
2868
|
+
meta: {
|
|
2869
|
+
type: "suggestion",
|
|
2870
|
+
docs: {
|
|
2871
|
+
description: "Require Lit query decorators instead of static render-root queries in Lit elements."
|
|
2872
|
+
},
|
|
2873
|
+
schema: [
|
|
2874
|
+
{
|
|
2875
|
+
type: "object",
|
|
2876
|
+
properties: {
|
|
2877
|
+
querySelector: { type: "boolean" },
|
|
2878
|
+
querySelectorAll: { type: "boolean" },
|
|
2879
|
+
assignedElements: { type: "boolean" },
|
|
2880
|
+
assignedNodes: { type: "boolean" }
|
|
2881
|
+
},
|
|
2882
|
+
additionalProperties: false
|
|
2883
|
+
}
|
|
2884
|
+
],
|
|
2885
|
+
defaultOptions: [
|
|
2886
|
+
{
|
|
2887
|
+
querySelector: true,
|
|
2888
|
+
querySelectorAll: true,
|
|
2889
|
+
assignedElements: true,
|
|
2890
|
+
assignedNodes: true
|
|
2891
|
+
}
|
|
2892
|
+
],
|
|
2893
|
+
messages: {
|
|
2894
|
+
preferQuery: "Use the @query decorator instead of this.{{root}}.querySelector().",
|
|
2895
|
+
preferQueryAll: "Use the @queryAll decorator instead of this.{{root}}.querySelectorAll().",
|
|
2896
|
+
preferQueryAssignedElements: "Use the @queryAssignedElements decorator instead of this.{{root}}.querySelector().assignedElements().",
|
|
2897
|
+
preferQueryAssignedNodes: "Use the @queryAssignedNodes decorator instead of this.{{root}}.querySelector().assignedNodes()."
|
|
2898
|
+
}
|
|
2899
|
+
},
|
|
2900
|
+
create(context) {
|
|
2901
|
+
const bases = litElementBaseClasses(context.settings);
|
|
2902
|
+
const options = jsonObject(context.options[0]);
|
|
2903
|
+
const disabled = new Set(options ? Object.keys(options).filter((method) => options[method] === false) : []);
|
|
2904
|
+
return {
|
|
2905
|
+
CallExpression(node) {
|
|
2906
|
+
const query = litQueryDecoratorCall(node, bases);
|
|
2907
|
+
if (!query || disabled.has(query.method))
|
|
2908
|
+
return;
|
|
2909
|
+
context.report({
|
|
2910
|
+
node,
|
|
2911
|
+
messageId: messageIds[query.method],
|
|
2912
|
+
data: { root: query.root }
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
});
|
|
2918
|
+
|
|
2919
|
+
// src/generic/rules/prefer-types-over-assertions.ts
|
|
2920
|
+
import { defineRule as defineRule23 } from "@oxlint/plugins";
|
|
2921
|
+
var preferTypesOverAssertionsRule = defineRule23({
|
|
2922
|
+
meta: {
|
|
2923
|
+
type: "problem",
|
|
2924
|
+
docs: {
|
|
2925
|
+
description: "Prefer typing a value at its source over a TypeScript type assertion."
|
|
2926
|
+
},
|
|
2927
|
+
messages: {
|
|
2928
|
+
avoidAssertion: "Type this value at its source, such as a function parameter or DOM lookup, instead of asserting it here. A SAFETY comment is not a fix."
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2931
|
+
create(context) {
|
|
2932
|
+
const checkAssertion = (node) => {
|
|
2933
|
+
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const" || assertedEventParameter(node)) {
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
context.report({ node, messageId: "avoidAssertion" });
|
|
2937
|
+
};
|
|
2938
|
+
return {
|
|
2939
|
+
TSAsExpression: checkAssertion,
|
|
2940
|
+
TSTypeAssertion: checkAssertion
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
});
|
|
2944
|
+
|
|
2607
2945
|
// src/generic/index.ts
|
|
2608
2946
|
var timmoPlugin = eslintCompatPlugin2({
|
|
2609
2947
|
meta: { name: "timmo" },
|
|
2610
2948
|
rules: {
|
|
2611
|
-
"
|
|
2949
|
+
"no-explicit-inferable-type-predicate": noExplicitInferableTypePredicateRule,
|
|
2950
|
+
"no-unchecked-query-selector-type": noUncheckedQuerySelectorTypeRule,
|
|
2951
|
+
"no-untyped-custom-event": noUntypedCustomEventRule,
|
|
2952
|
+
"prefer-event-parameter-type": preferEventParameterTypeRule,
|
|
2953
|
+
"prefer-lit-query-decorators": preferLitQueryDecoratorsRule,
|
|
2954
|
+
"prefer-types-over-assertions": preferTypesOverAssertionsRule
|
|
2612
2955
|
}
|
|
2613
2956
|
});
|
|
2614
2957
|
var generic_default = timmoPlugin;
|
|
@@ -2629,7 +2972,12 @@ var recommended = defineConfig({
|
|
|
2629
2972
|
],
|
|
2630
2973
|
rules: {
|
|
2631
2974
|
...enablePluginRules("anti-slop", src_default),
|
|
2632
|
-
...enablePluginRules("timmo", generic_default)
|
|
2975
|
+
...enablePluginRules("timmo", generic_default),
|
|
2976
|
+
"anti-slop/require-safety-comment-for-type-assertion": "off",
|
|
2977
|
+
"typescript/no-non-null-assertion": "error",
|
|
2978
|
+
"typescript/no-unnecessary-type-arguments": "error",
|
|
2979
|
+
"typescript/no-unnecessary-type-assertion": "error",
|
|
2980
|
+
"typescript/no-unnecessary-type-parameters": "error"
|
|
2633
2981
|
}
|
|
2634
2982
|
});
|
|
2635
2983
|
var recommended_default = recommended;
|
|
@@ -2638,34 +2986,10 @@ var recommended_default = recommended;
|
|
|
2638
2986
|
import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
|
|
2639
2987
|
|
|
2640
2988
|
// src/effect/rules/no-try-catch-in-effect-generators.ts
|
|
2641
|
-
import { defineRule as
|
|
2642
|
-
|
|
2643
|
-
let scope = sourceCode.getScope(identifier);
|
|
2644
|
-
while (scope) {
|
|
2645
|
-
const variable = scope.set.get(identifier.name);
|
|
2646
|
-
if (variable)
|
|
2647
|
-
return variable;
|
|
2648
|
-
scope = scope.upper;
|
|
2649
|
-
}
|
|
2650
|
-
return null;
|
|
2651
|
-
}
|
|
2652
|
-
function nearestEnclosingFunction2(node) {
|
|
2653
|
-
let current = node.parent;
|
|
2654
|
-
while (current) {
|
|
2655
|
-
if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
|
|
2656
|
-
return current;
|
|
2657
|
-
}
|
|
2658
|
-
current = current.parent;
|
|
2659
|
-
}
|
|
2660
|
-
return null;
|
|
2661
|
-
}
|
|
2662
|
-
function staticMemberName(node) {
|
|
2663
|
-
if (node.type !== "MemberExpression" || node.computed)
|
|
2664
|
-
return null;
|
|
2665
|
-
return node.property.type === "Identifier" ? node.property.name : null;
|
|
2666
|
-
}
|
|
2989
|
+
import { defineRule as defineRule24 } from "@oxlint/plugins";
|
|
2990
|
+
// src/shared/effect.ts
|
|
2667
2991
|
function isEffectImport(sourceCode, identifier, namespace) {
|
|
2668
|
-
const variable =
|
|
2992
|
+
const variable = resolveVariable(sourceCode, identifier);
|
|
2669
2993
|
return variable?.defs.some((definition) => {
|
|
2670
2994
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== "effect") {
|
|
2671
2995
|
return false;
|
|
@@ -2691,6 +3015,11 @@ function isEffectMethod(sourceCode, node, method) {
|
|
|
2691
3015
|
}
|
|
2692
3016
|
return isEffectImport(sourceCode, object.object, true);
|
|
2693
3017
|
}
|
|
3018
|
+
function importsEffect(program) {
|
|
3019
|
+
return program.body.some((statement) => statement.type === "ImportDeclaration" && statement.importKind !== "type" && (statement.source.value === "effect" || statement.source.value.startsWith("effect/") || statement.source.value.startsWith("@effect/")));
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
// src/effect/rules/no-try-catch-in-effect-generators.ts
|
|
2694
3023
|
function isDirectArgument(owner, call) {
|
|
2695
3024
|
return call.arguments.some((argument) => argument === owner);
|
|
2696
3025
|
}
|
|
@@ -2704,7 +3033,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
|
|
|
2704
3033
|
const factoryCall = parent.callee;
|
|
2705
3034
|
return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
|
|
2706
3035
|
}
|
|
2707
|
-
var noTryCatchInEffectGeneratorsRule =
|
|
3036
|
+
var noTryCatchInEffectGeneratorsRule = defineRule24({
|
|
2708
3037
|
meta: {
|
|
2709
3038
|
type: "problem",
|
|
2710
3039
|
docs: {
|
|
@@ -2719,7 +3048,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule19({
|
|
|
2719
3048
|
TryStatement(node) {
|
|
2720
3049
|
if (!node.handler)
|
|
2721
3050
|
return;
|
|
2722
|
-
const owner =
|
|
3051
|
+
const owner = nearestEnclosingFunction(node);
|
|
2723
3052
|
if (!owner?.generator || !isRecognisedEffectGenerator(context.sourceCode, owner)) {
|
|
2724
3053
|
return;
|
|
2725
3054
|
}
|
|
@@ -2729,11 +3058,85 @@ var noTryCatchInEffectGeneratorsRule = defineRule19({
|
|
|
2729
3058
|
}
|
|
2730
3059
|
});
|
|
2731
3060
|
|
|
3061
|
+
// src/effect/rules/prefer-platform-services.ts
|
|
3062
|
+
import { defineRule as defineRule25 } from "@oxlint/plugins";
|
|
3063
|
+
var moduleServices = new Map([
|
|
3064
|
+
["child_process", "childProcess"],
|
|
3065
|
+
["fs", "fileSystem"],
|
|
3066
|
+
["fs/promises", "fileSystem"]
|
|
3067
|
+
]);
|
|
3068
|
+
var bunServices = new Map([
|
|
3069
|
+
["$", "childProcess"],
|
|
3070
|
+
["file", "fileSystem"],
|
|
3071
|
+
["spawn", "childProcess"],
|
|
3072
|
+
["spawnSync", "childProcess"],
|
|
3073
|
+
["write", "fileSystem"]
|
|
3074
|
+
]);
|
|
3075
|
+
var preferPlatformServicesRule = defineRule25({
|
|
3076
|
+
meta: {
|
|
3077
|
+
type: "suggestion",
|
|
3078
|
+
docs: {
|
|
3079
|
+
description: "Prefer Effect's ChildProcessSpawner and FileSystem services over Node or Bun process and file system APIs in modules that use Effect."
|
|
3080
|
+
},
|
|
3081
|
+
messages: {
|
|
3082
|
+
childProcess: "Use Effect's `ChildProcessSpawner` service (`effect/process`) instead of `{{api}}`, so scope cleanup, interruption, and test layers apply.",
|
|
3083
|
+
fileSystem: "Use Effect's `FileSystem` service instead of `{{api}}`, so failures stay typed and tests can provide a layer."
|
|
3084
|
+
}
|
|
3085
|
+
},
|
|
3086
|
+
create(context) {
|
|
3087
|
+
let enabled = false;
|
|
3088
|
+
return {
|
|
3089
|
+
Program(node) {
|
|
3090
|
+
enabled = importsEffect(node);
|
|
3091
|
+
},
|
|
3092
|
+
ImportDeclaration(node) {
|
|
3093
|
+
if (!enabled || node.importKind === "type")
|
|
3094
|
+
return;
|
|
3095
|
+
const source = node.source.value;
|
|
3096
|
+
const service = moduleServices.get(source.replace(/^node:/u, ""));
|
|
3097
|
+
if (service) {
|
|
3098
|
+
context.report({ node, messageId: service, data: { api: source } });
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
if (source !== "bun")
|
|
3102
|
+
return;
|
|
3103
|
+
for (const specifier of node.specifiers) {
|
|
3104
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
3108
|
+
const bunService = bunServices.get(imported);
|
|
3109
|
+
if (bunService) {
|
|
3110
|
+
context.report({
|
|
3111
|
+
node: specifier,
|
|
3112
|
+
messageId: bunService,
|
|
3113
|
+
data: { api: `Bun.${imported}` }
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
},
|
|
3118
|
+
MemberExpression(node) {
|
|
3119
|
+
const name = staticMemberName(node);
|
|
3120
|
+
const service = name ? bunServices.get(name) : undefined;
|
|
3121
|
+
if (!enabled || !service || node.object.type !== "Identifier" || node.object.name !== "Bun" || (resolveVariable(context.sourceCode, node.object)?.defs.length ?? 0) > 0) {
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
context.report({
|
|
3125
|
+
node,
|
|
3126
|
+
messageId: service,
|
|
3127
|
+
data: { api: `Bun.${name}` }
|
|
3128
|
+
});
|
|
3129
|
+
}
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
|
|
2732
3134
|
// src/effect/index.ts
|
|
2733
3135
|
var timmoEffectPlugin = eslintCompatPlugin3({
|
|
2734
3136
|
meta: { name: "timmo-effect" },
|
|
2735
3137
|
rules: {
|
|
2736
|
-
"no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule
|
|
3138
|
+
"no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
|
|
3139
|
+
"prefer-platform-services": preferPlatformServicesRule
|
|
2737
3140
|
}
|
|
2738
3141
|
});
|
|
2739
3142
|
var effect_default = timmoEffectPlugin;
|
|
@@ -2742,7 +3145,7 @@ var effect_default = timmoEffectPlugin;
|
|
|
2742
3145
|
import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
|
|
2743
3146
|
|
|
2744
3147
|
// vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
|
|
2745
|
-
import { defineRule as
|
|
3148
|
+
import { defineRule as defineRule26 } from "@oxlint/plugins";
|
|
2746
3149
|
|
|
2747
3150
|
// vendor/anti-slop/src/effect/shared/tagged-values.ts
|
|
2748
3151
|
var equalityOperators = new Set(["==", "===", "!=", "!=="]);
|
|
@@ -2789,7 +3192,7 @@ var isMatchPatternObject = (node) => {
|
|
|
2789
3192
|
};
|
|
2790
3193
|
|
|
2791
3194
|
// vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
|
|
2792
|
-
var noManualEffectErrorTagRule =
|
|
3195
|
+
var noManualEffectErrorTagRule = defineRule26({
|
|
2793
3196
|
meta: {
|
|
2794
3197
|
type: "problem",
|
|
2795
3198
|
docs: {
|
|
@@ -2826,8 +3229,8 @@ var noManualEffectErrorTagRule = defineRule20({
|
|
|
2826
3229
|
});
|
|
2827
3230
|
|
|
2828
3231
|
// vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
|
|
2829
|
-
import { defineRule as
|
|
2830
|
-
var noManualTagComparisonRule =
|
|
3232
|
+
import { defineRule as defineRule27 } from "@oxlint/plugins";
|
|
3233
|
+
var noManualTagComparisonRule = defineRule27({
|
|
2831
3234
|
meta: {
|
|
2832
3235
|
type: "problem",
|
|
2833
3236
|
docs: {
|
|
@@ -2857,8 +3260,8 @@ var noManualTagComparisonRule = defineRule21({
|
|
|
2857
3260
|
});
|
|
2858
3261
|
|
|
2859
3262
|
// vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
|
|
2860
|
-
import { defineRule as
|
|
2861
|
-
var noManualTaggedConstructionRule =
|
|
3263
|
+
import { defineRule as defineRule28 } from "@oxlint/plugins";
|
|
3264
|
+
var noManualTaggedConstructionRule = defineRule28({
|
|
2862
3265
|
meta: {
|
|
2863
3266
|
type: "problem",
|
|
2864
3267
|
docs: {
|
|
@@ -2883,7 +3286,7 @@ var noManualTaggedConstructionRule = defineRule22({
|
|
|
2883
3286
|
});
|
|
2884
3287
|
|
|
2885
3288
|
// vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
|
|
2886
|
-
import { defineRule as
|
|
3289
|
+
import { defineRule as defineRule29 } from "@oxlint/plugins";
|
|
2887
3290
|
var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
|
|
2888
3291
|
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
2889
3292
|
function isProjectLocalImport(source) {
|
|
@@ -2894,7 +3297,7 @@ function getImportedName(specifier) {
|
|
|
2894
3297
|
return specifier.imported.name;
|
|
2895
3298
|
return specifier.imported.value;
|
|
2896
3299
|
}
|
|
2897
|
-
var noServiceConstructorImportsRule =
|
|
3300
|
+
var noServiceConstructorImportsRule = defineRule29({
|
|
2898
3301
|
meta: {
|
|
2899
3302
|
type: "problem",
|
|
2900
3303
|
docs: {
|
|
@@ -2928,9 +3331,9 @@ var noServiceConstructorImportsRule = defineRule23({
|
|
|
2928
3331
|
});
|
|
2929
3332
|
|
|
2930
3333
|
// vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
|
|
2931
|
-
import { defineRule as
|
|
3334
|
+
import { defineRule as defineRule30 } from "@oxlint/plugins";
|
|
2932
3335
|
var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
|
|
2933
|
-
var preferEffectMatchRule =
|
|
3336
|
+
var preferEffectMatchRule = defineRule30({
|
|
2934
3337
|
meta: {
|
|
2935
3338
|
type: "problem",
|
|
2936
3339
|
docs: {
|
|
@@ -3005,7 +3408,7 @@ var recommendedEffect = defineConfig({
|
|
|
3005
3408
|
var recommended_effect_default = recommendedEffect;
|
|
3006
3409
|
|
|
3007
3410
|
// src/install/copy.ts
|
|
3008
|
-
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
3411
|
+
import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
3009
3412
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3010
3413
|
import { fileURLToPath } from "node:url";
|
|
3011
3414
|
var DEFAULT_COPY_DESTINATION = "tools/oxlint/timmo-rules";
|
|
@@ -3042,6 +3445,10 @@ async function copyRules(destination, options = {}) {
|
|
|
3042
3445
|
await cp(join(root, "LICENSE"), join(target, "effect/LICENSE"));
|
|
3043
3446
|
await copyTree(join(root, "src/generic"), join(target, "generic"));
|
|
3044
3447
|
await cp(join(root, "LICENSE"), join(target, "generic/LICENSE"));
|
|
3448
|
+
await copyTree(join(root, "src/shared"), join(target, "shared"));
|
|
3449
|
+
await cp(join(root, "LICENSE"), join(target, "shared/LICENSE"));
|
|
3450
|
+
await writeFile(join(target, "shared/scope.ts"), `export { resolveVariable } from "../upstream/anti-slop/shared/scope.ts";
|
|
3451
|
+
`);
|
|
3045
3452
|
const displayRoot = relative(process.cwd(), target) || ".";
|
|
3046
3453
|
const entryPoint = (path) => `./${path.split(sep).join("/")}`;
|
|
3047
3454
|
return {
|