@timmo001/oxlint-rules 0.4.0 → 0.6.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 +59 -5
- package/dist/cli.js +498 -55
- package/dist/configs/recommended-effect.js +539 -99
- package/dist/configs/recommended.js +336 -16
- package/dist/effect/index.js +162 -13
- package/dist/generic/index.js +331 -15
- package/package.json +12 -9
- package/skills/install-timmo-oxlint-rules/SKILL.md +18 -5
- package/src/effect/index.ts +4 -0
- package/src/effect/rules/no-raw-gh.ts +126 -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 +9 -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 +6 -5
- 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])) {
|
|
2566
2707
|
return null;
|
|
2567
2708
|
}
|
|
2568
|
-
const
|
|
2569
|
-
const
|
|
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") {
|
|
2712
|
+
return null;
|
|
2713
|
+
}
|
|
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,9 +2856,69 @@ 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
|
+
|
|
2607
2919
|
// src/generic/rules/prefer-types-over-assertions.ts
|
|
2608
|
-
import { defineRule as
|
|
2609
|
-
var preferTypesOverAssertionsRule =
|
|
2920
|
+
import { defineRule as defineRule23 } from "@oxlint/plugins";
|
|
2921
|
+
var preferTypesOverAssertionsRule = defineRule23({
|
|
2610
2922
|
meta: {
|
|
2611
2923
|
type: "problem",
|
|
2612
2924
|
docs: {
|
|
@@ -2618,7 +2930,7 @@ var preferTypesOverAssertionsRule = defineRule19({
|
|
|
2618
2930
|
},
|
|
2619
2931
|
create(context) {
|
|
2620
2932
|
const checkAssertion = (node) => {
|
|
2621
|
-
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const") {
|
|
2933
|
+
if (node.typeAnnotation.type === "TSTypeReference" && node.typeAnnotation.typeName.type === "Identifier" && node.typeAnnotation.typeName.name === "const" || assertedEventParameter(node)) {
|
|
2622
2934
|
return;
|
|
2623
2935
|
}
|
|
2624
2936
|
context.report({ node, messageId: "avoidAssertion" });
|
|
@@ -2634,7 +2946,11 @@ var preferTypesOverAssertionsRule = defineRule19({
|
|
|
2634
2946
|
var timmoPlugin = eslintCompatPlugin2({
|
|
2635
2947
|
meta: { name: "timmo" },
|
|
2636
2948
|
rules: {
|
|
2949
|
+
"no-explicit-inferable-type-predicate": noExplicitInferableTypePredicateRule,
|
|
2950
|
+
"no-unchecked-query-selector-type": noUncheckedQuerySelectorTypeRule,
|
|
2951
|
+
"no-untyped-custom-event": noUntypedCustomEventRule,
|
|
2637
2952
|
"prefer-event-parameter-type": preferEventParameterTypeRule,
|
|
2953
|
+
"prefer-lit-query-decorators": preferLitQueryDecoratorsRule,
|
|
2638
2954
|
"prefer-types-over-assertions": preferTypesOverAssertionsRule
|
|
2639
2955
|
}
|
|
2640
2956
|
});
|
|
@@ -2657,7 +2973,11 @@ var recommended = defineConfig({
|
|
|
2657
2973
|
rules: {
|
|
2658
2974
|
...enablePluginRules("anti-slop", src_default),
|
|
2659
2975
|
...enablePluginRules("timmo", generic_default),
|
|
2660
|
-
"anti-slop/require-safety-comment-for-type-assertion": "off"
|
|
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"
|
|
2661
2981
|
}
|
|
2662
2982
|
});
|
|
2663
2983
|
var recommended_default = recommended;
|
|
@@ -2665,35 +2985,74 @@ var recommended_default = recommended;
|
|
|
2665
2985
|
// src/effect/index.ts
|
|
2666
2986
|
import { eslintCompatPlugin as eslintCompatPlugin3 } from "@oxlint/plugins";
|
|
2667
2987
|
|
|
2668
|
-
// src/effect/rules/no-
|
|
2669
|
-
import { defineRule as
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
if (
|
|
2675
|
-
return
|
|
2676
|
-
scope = scope.upper;
|
|
2677
|
-
}
|
|
2678
|
-
return null;
|
|
2679
|
-
}
|
|
2680
|
-
function nearestEnclosingFunction2(node) {
|
|
2681
|
-
let current = node.parent;
|
|
2682
|
-
while (current) {
|
|
2683
|
-
if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
|
|
2684
|
-
return current;
|
|
2988
|
+
// src/effect/rules/no-raw-gh.ts
|
|
2989
|
+
import { defineRule as defineRule24 } from "@oxlint/plugins";
|
|
2990
|
+
var PACKAGE = "@timmo001/effect-gh";
|
|
2991
|
+
var rawMethods = new Set(["execute", "json", "stream"]);
|
|
2992
|
+
function isEffectGhImport(sourceCode, identifier, name) {
|
|
2993
|
+
return resolveVariable(sourceCode, identifier)?.defs.some((definition) => {
|
|
2994
|
+
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== PACKAGE || definition.node.type !== "ImportSpecifier") {
|
|
2995
|
+
return false;
|
|
2685
2996
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2997
|
+
const imported = definition.node.imported;
|
|
2998
|
+
return (imported.type === "Identifier" ? imported.name : imported.value) === name;
|
|
2999
|
+
}) ?? false;
|
|
2689
3000
|
}
|
|
2690
|
-
function
|
|
2691
|
-
|
|
2692
|
-
return null;
|
|
2693
|
-
return node.property.type === "Identifier" ? node.property.name : null;
|
|
3001
|
+
function isYieldGh(sourceCode, node) {
|
|
3002
|
+
return node?.type === "YieldExpression" && node.delegate && node.argument?.type === "Identifier" && isEffectGhImport(sourceCode, node.argument, "Gh");
|
|
2694
3003
|
}
|
|
3004
|
+
function isGhBinding(sourceCode, identifier) {
|
|
3005
|
+
return resolveVariable(sourceCode, identifier)?.defs.some((definition) => definition.type === "Variable" && definition.node.type === "VariableDeclarator" && isYieldGh(sourceCode, definition.node.init)) ?? false;
|
|
3006
|
+
}
|
|
3007
|
+
function isGhArgument(node) {
|
|
3008
|
+
if (node?.type === "Literal")
|
|
3009
|
+
return node.value === "gh";
|
|
3010
|
+
if (node?.type !== "ArrayExpression")
|
|
3011
|
+
return false;
|
|
3012
|
+
const first = node.elements[0];
|
|
3013
|
+
return first?.type === "Literal" && first.value === "gh";
|
|
3014
|
+
}
|
|
3015
|
+
var noRawGhRule = defineRule24({
|
|
3016
|
+
meta: {
|
|
3017
|
+
type: "problem",
|
|
3018
|
+
docs: {
|
|
3019
|
+
description: "Use @timmo001/effect-gh typed operations instead of the raw Gh methods, Api.raw, or spawning gh directly."
|
|
3020
|
+
},
|
|
3021
|
+
messages: {
|
|
3022
|
+
rawGh: "Use a typed effect-gh operation instead of `Gh.{{method}}` with raw argv. Add one to effect-gh if the command has none.",
|
|
3023
|
+
rawApi: "Use `Api.json`, `Api.pages`, `Api.empty`, `Api.text` or `Api.headers` instead of `Api.raw`, so the response is typed.",
|
|
3024
|
+
spawnGh: "Run gh through a typed @timmo001/effect-gh operation instead of spawning it directly."
|
|
3025
|
+
}
|
|
3026
|
+
},
|
|
3027
|
+
create(context) {
|
|
3028
|
+
return {
|
|
3029
|
+
CallExpression(node) {
|
|
3030
|
+
const callee = node.callee;
|
|
3031
|
+
const method = staticMemberName(callee);
|
|
3032
|
+
if (callee.type === "MemberExpression" && method !== null) {
|
|
3033
|
+
const object = callee.object;
|
|
3034
|
+
if (rawMethods.has(method) && (isYieldGh(context.sourceCode, object) || object.type === "Identifier" && isGhBinding(context.sourceCode, object))) {
|
|
3035
|
+
context.report({ node, messageId: "rawGh", data: { method } });
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
if (method === "raw" && object.type === "Identifier" && isEffectGhImport(context.sourceCode, object, "Api")) {
|
|
3039
|
+
context.report({ node, messageId: "rawApi" });
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
if (isGhArgument(node.arguments[0]))
|
|
3044
|
+
context.report({ node, messageId: "spawnGh" });
|
|
3045
|
+
}
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
|
|
3050
|
+
// src/effect/rules/no-try-catch-in-effect-generators.ts
|
|
3051
|
+
import { defineRule as defineRule25 } from "@oxlint/plugins";
|
|
3052
|
+
|
|
3053
|
+
// src/shared/effect.ts
|
|
2695
3054
|
function isEffectImport(sourceCode, identifier, namespace) {
|
|
2696
|
-
const variable =
|
|
3055
|
+
const variable = resolveVariable(sourceCode, identifier);
|
|
2697
3056
|
return variable?.defs.some((definition) => {
|
|
2698
3057
|
if (definition.type !== "ImportBinding" || definition.parent?.type !== "ImportDeclaration" || definition.parent.source.value !== "effect") {
|
|
2699
3058
|
return false;
|
|
@@ -2719,6 +3078,11 @@ function isEffectMethod(sourceCode, node, method) {
|
|
|
2719
3078
|
}
|
|
2720
3079
|
return isEffectImport(sourceCode, object.object, true);
|
|
2721
3080
|
}
|
|
3081
|
+
function importsEffect(program) {
|
|
3082
|
+
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/")));
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
// src/effect/rules/no-try-catch-in-effect-generators.ts
|
|
2722
3086
|
function isDirectArgument(owner, call) {
|
|
2723
3087
|
return call.arguments.some((argument) => argument === owner);
|
|
2724
3088
|
}
|
|
@@ -2732,7 +3096,7 @@ function isRecognisedEffectGenerator(sourceCode, owner) {
|
|
|
2732
3096
|
const factoryCall = parent.callee;
|
|
2733
3097
|
return factoryCall.type === "CallExpression" && isEffectMethod(sourceCode, factoryCall.callee, "fn");
|
|
2734
3098
|
}
|
|
2735
|
-
var noTryCatchInEffectGeneratorsRule =
|
|
3099
|
+
var noTryCatchInEffectGeneratorsRule = defineRule25({
|
|
2736
3100
|
meta: {
|
|
2737
3101
|
type: "problem",
|
|
2738
3102
|
docs: {
|
|
@@ -2747,7 +3111,7 @@ var noTryCatchInEffectGeneratorsRule = defineRule20({
|
|
|
2747
3111
|
TryStatement(node) {
|
|
2748
3112
|
if (!node.handler)
|
|
2749
3113
|
return;
|
|
2750
|
-
const owner =
|
|
3114
|
+
const owner = nearestEnclosingFunction(node);
|
|
2751
3115
|
if (!owner?.generator || !isRecognisedEffectGenerator(context.sourceCode, owner)) {
|
|
2752
3116
|
return;
|
|
2753
3117
|
}
|
|
@@ -2757,11 +3121,86 @@ var noTryCatchInEffectGeneratorsRule = defineRule20({
|
|
|
2757
3121
|
}
|
|
2758
3122
|
});
|
|
2759
3123
|
|
|
3124
|
+
// src/effect/rules/prefer-platform-services.ts
|
|
3125
|
+
import { defineRule as defineRule26 } from "@oxlint/plugins";
|
|
3126
|
+
var moduleServices = new Map([
|
|
3127
|
+
["child_process", "childProcess"],
|
|
3128
|
+
["fs", "fileSystem"],
|
|
3129
|
+
["fs/promises", "fileSystem"]
|
|
3130
|
+
]);
|
|
3131
|
+
var bunServices = new Map([
|
|
3132
|
+
["$", "childProcess"],
|
|
3133
|
+
["file", "fileSystem"],
|
|
3134
|
+
["spawn", "childProcess"],
|
|
3135
|
+
["spawnSync", "childProcess"],
|
|
3136
|
+
["write", "fileSystem"]
|
|
3137
|
+
]);
|
|
3138
|
+
var preferPlatformServicesRule = defineRule26({
|
|
3139
|
+
meta: {
|
|
3140
|
+
type: "suggestion",
|
|
3141
|
+
docs: {
|
|
3142
|
+
description: "Prefer Effect's ChildProcessSpawner and FileSystem services over Node or Bun process and file system APIs in modules that use Effect."
|
|
3143
|
+
},
|
|
3144
|
+
messages: {
|
|
3145
|
+
childProcess: "Use Effect's `ChildProcessSpawner` service (`effect/process`) instead of `{{api}}`, so scope cleanup, interruption, and test layers apply.",
|
|
3146
|
+
fileSystem: "Use Effect's `FileSystem` service instead of `{{api}}`, so failures stay typed and tests can provide a layer."
|
|
3147
|
+
}
|
|
3148
|
+
},
|
|
3149
|
+
create(context) {
|
|
3150
|
+
let enabled = false;
|
|
3151
|
+
return {
|
|
3152
|
+
Program(node) {
|
|
3153
|
+
enabled = importsEffect(node);
|
|
3154
|
+
},
|
|
3155
|
+
ImportDeclaration(node) {
|
|
3156
|
+
if (!enabled || node.importKind === "type")
|
|
3157
|
+
return;
|
|
3158
|
+
const source = node.source.value;
|
|
3159
|
+
const service = moduleServices.get(source.replace(/^node:/u, ""));
|
|
3160
|
+
if (service) {
|
|
3161
|
+
context.report({ node, messageId: service, data: { api: source } });
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
if (source !== "bun")
|
|
3165
|
+
return;
|
|
3166
|
+
for (const specifier of node.specifiers) {
|
|
3167
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
3171
|
+
const bunService = bunServices.get(imported);
|
|
3172
|
+
if (bunService) {
|
|
3173
|
+
context.report({
|
|
3174
|
+
node: specifier,
|
|
3175
|
+
messageId: bunService,
|
|
3176
|
+
data: { api: `Bun.${imported}` }
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
},
|
|
3181
|
+
MemberExpression(node) {
|
|
3182
|
+
const name = staticMemberName(node);
|
|
3183
|
+
const service = name ? bunServices.get(name) : undefined;
|
|
3184
|
+
if (!enabled || !service || node.object.type !== "Identifier" || node.object.name !== "Bun" || (resolveVariable(context.sourceCode, node.object)?.defs.length ?? 0) > 0) {
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
context.report({
|
|
3188
|
+
node,
|
|
3189
|
+
messageId: service,
|
|
3190
|
+
data: { api: `Bun.${name}` }
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
|
|
2760
3197
|
// src/effect/index.ts
|
|
2761
3198
|
var timmoEffectPlugin = eslintCompatPlugin3({
|
|
2762
3199
|
meta: { name: "timmo-effect" },
|
|
2763
3200
|
rules: {
|
|
2764
|
-
"no-
|
|
3201
|
+
"no-raw-gh": noRawGhRule,
|
|
3202
|
+
"no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
|
|
3203
|
+
"prefer-platform-services": preferPlatformServicesRule
|
|
2765
3204
|
}
|
|
2766
3205
|
});
|
|
2767
3206
|
var effect_default = timmoEffectPlugin;
|
|
@@ -2770,7 +3209,7 @@ var effect_default = timmoEffectPlugin;
|
|
|
2770
3209
|
import { eslintCompatPlugin as eslintCompatPlugin4 } from "@oxlint/plugins";
|
|
2771
3210
|
|
|
2772
3211
|
// vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
|
|
2773
|
-
import { defineRule as
|
|
3212
|
+
import { defineRule as defineRule27 } from "@oxlint/plugins";
|
|
2774
3213
|
|
|
2775
3214
|
// vendor/anti-slop/src/effect/shared/tagged-values.ts
|
|
2776
3215
|
var equalityOperators = new Set(["==", "===", "!=", "!=="]);
|
|
@@ -2817,7 +3256,7 @@ var isMatchPatternObject = (node) => {
|
|
|
2817
3256
|
};
|
|
2818
3257
|
|
|
2819
3258
|
// vendor/anti-slop/src/effect/rules/no-manual-effect-error-tag.ts
|
|
2820
|
-
var noManualEffectErrorTagRule =
|
|
3259
|
+
var noManualEffectErrorTagRule = defineRule27({
|
|
2821
3260
|
meta: {
|
|
2822
3261
|
type: "problem",
|
|
2823
3262
|
docs: {
|
|
@@ -2854,8 +3293,8 @@ var noManualEffectErrorTagRule = defineRule21({
|
|
|
2854
3293
|
});
|
|
2855
3294
|
|
|
2856
3295
|
// vendor/anti-slop/src/effect/rules/no-manual-tag-comparison.ts
|
|
2857
|
-
import { defineRule as
|
|
2858
|
-
var noManualTagComparisonRule =
|
|
3296
|
+
import { defineRule as defineRule28 } from "@oxlint/plugins";
|
|
3297
|
+
var noManualTagComparisonRule = defineRule28({
|
|
2859
3298
|
meta: {
|
|
2860
3299
|
type: "problem",
|
|
2861
3300
|
docs: {
|
|
@@ -2885,8 +3324,8 @@ var noManualTagComparisonRule = defineRule22({
|
|
|
2885
3324
|
});
|
|
2886
3325
|
|
|
2887
3326
|
// vendor/anti-slop/src/effect/rules/no-manual-tagged-construction.ts
|
|
2888
|
-
import { defineRule as
|
|
2889
|
-
var noManualTaggedConstructionRule =
|
|
3327
|
+
import { defineRule as defineRule29 } from "@oxlint/plugins";
|
|
3328
|
+
var noManualTaggedConstructionRule = defineRule29({
|
|
2890
3329
|
meta: {
|
|
2891
3330
|
type: "problem",
|
|
2892
3331
|
docs: {
|
|
@@ -2911,7 +3350,7 @@ var noManualTaggedConstructionRule = defineRule23({
|
|
|
2911
3350
|
});
|
|
2912
3351
|
|
|
2913
3352
|
// vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
|
|
2914
|
-
import { defineRule as
|
|
3353
|
+
import { defineRule as defineRule30 } from "@oxlint/plugins";
|
|
2915
3354
|
var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
|
|
2916
3355
|
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
2917
3356
|
function isProjectLocalImport(source) {
|
|
@@ -2922,7 +3361,7 @@ function getImportedName(specifier) {
|
|
|
2922
3361
|
return specifier.imported.name;
|
|
2923
3362
|
return specifier.imported.value;
|
|
2924
3363
|
}
|
|
2925
|
-
var noServiceConstructorImportsRule =
|
|
3364
|
+
var noServiceConstructorImportsRule = defineRule30({
|
|
2926
3365
|
meta: {
|
|
2927
3366
|
type: "problem",
|
|
2928
3367
|
docs: {
|
|
@@ -2956,9 +3395,9 @@ var noServiceConstructorImportsRule = defineRule24({
|
|
|
2956
3395
|
});
|
|
2957
3396
|
|
|
2958
3397
|
// vendor/anti-slop/src/effect/rules/prefer-effect-match.ts
|
|
2959
|
-
import { defineRule as
|
|
3398
|
+
import { defineRule as defineRule31 } from "@oxlint/plugins";
|
|
2960
3399
|
var equalityOperators2 = new Set(["==", "===", "!=", "!=="]);
|
|
2961
|
-
var preferEffectMatchRule =
|
|
3400
|
+
var preferEffectMatchRule = defineRule31({
|
|
2962
3401
|
meta: {
|
|
2963
3402
|
type: "problem",
|
|
2964
3403
|
docs: {
|
|
@@ -3033,7 +3472,7 @@ var recommendedEffect = defineConfig({
|
|
|
3033
3472
|
var recommended_effect_default = recommendedEffect;
|
|
3034
3473
|
|
|
3035
3474
|
// src/install/copy.ts
|
|
3036
|
-
import { cp, mkdir, readdir, rm } from "node:fs/promises";
|
|
3475
|
+
import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises";
|
|
3037
3476
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
3038
3477
|
import { fileURLToPath } from "node:url";
|
|
3039
3478
|
var DEFAULT_COPY_DESTINATION = "tools/oxlint/timmo-rules";
|
|
@@ -3070,6 +3509,10 @@ async function copyRules(destination, options = {}) {
|
|
|
3070
3509
|
await cp(join(root, "LICENSE"), join(target, "effect/LICENSE"));
|
|
3071
3510
|
await copyTree(join(root, "src/generic"), join(target, "generic"));
|
|
3072
3511
|
await cp(join(root, "LICENSE"), join(target, "generic/LICENSE"));
|
|
3512
|
+
await copyTree(join(root, "src/shared"), join(target, "shared"));
|
|
3513
|
+
await cp(join(root, "LICENSE"), join(target, "shared/LICENSE"));
|
|
3514
|
+
await writeFile(join(target, "shared/scope.ts"), `export { resolveVariable } from "../upstream/anti-slop/shared/scope.ts";
|
|
3515
|
+
`);
|
|
3073
3516
|
const displayRoot = relative(process.cwd(), target) || ".";
|
|
3074
3517
|
const entryPoint = (path) => `./${path.split(sep).join("/")}`;
|
|
3075
3518
|
return {
|