@timmo001/oxlint-rules 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ // src/generic/index.ts
2
+ import { eslintCompatPlugin } from "@oxlint/plugins";
3
+
4
+ // src/generic/rules/prefer-event-parameter-type.ts
5
+ import { defineRule } from "@oxlint/plugins";
6
+ function nearestEnclosingFunction(node) {
7
+ let current = node.parent;
8
+ while (current) {
9
+ if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
10
+ return current;
11
+ }
12
+ current = current.parent;
13
+ }
14
+ return null;
15
+ }
16
+ function assertedEventParameter(node) {
17
+ const expression = node.expression;
18
+ if (expression.type !== "MemberExpression" || expression.computed || expression.object.type !== "Identifier" || expression.property.type !== "Identifier" || expression.property.name !== "currentTarget" && expression.property.name !== "target") {
19
+ return null;
20
+ }
21
+ const parameterName = expression.object.name;
22
+ const property = expression.property.name;
23
+ const owner = nearestEnclosingFunction(node);
24
+ if (!owner?.params.some((parameter) => parameter.type === "Identifier" && parameter.name === parameterName)) {
25
+ return null;
26
+ }
27
+ return {
28
+ parameter: parameterName,
29
+ property
30
+ };
31
+ }
32
+ var preferEventParameterTypeRule = defineRule({
33
+ meta: {
34
+ type: "suggestion",
35
+ docs: {
36
+ description: "Prefer typing event target properties in the handler parameter instead of asserting them at use sites."
37
+ },
38
+ messages: {
39
+ typeEventParameter: "Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site."
40
+ }
41
+ },
42
+ create(context) {
43
+ const checkAssertion = (node) => {
44
+ const eventParameter = assertedEventParameter(node);
45
+ if (!eventParameter)
46
+ return;
47
+ context.report({
48
+ node,
49
+ messageId: "typeEventParameter",
50
+ data: eventParameter
51
+ });
52
+ };
53
+ return {
54
+ TSAsExpression: checkAssertion,
55
+ TSTypeAssertion: checkAssertion
56
+ };
57
+ }
58
+ });
59
+
60
+ // src/generic/index.ts
61
+ var timmoPlugin = eslintCompatPlugin({
62
+ meta: { name: "timmo" },
63
+ rules: {
64
+ "prefer-event-parameter-type": preferEventParameterTypeRule
65
+ }
66
+ });
67
+ var generic_default = timmoPlugin;
68
+ export {
69
+ generic_default as default
70
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timmo001/oxlint-rules",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Shared Oxlint plugins and configs with optional Effect rules.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,17 +14,21 @@
14
14
  "exports": {
15
15
  "./upstream/anti-slop": "./dist/upstream/anti-slop.js",
16
16
  "./upstream/effect": "./dist/upstream/effect.js",
17
+ "./generic": "./dist/generic/index.js",
17
18
  "./effect": "./dist/effect/index.js",
18
19
  "./configs/recommended": "./dist/configs/recommended.js",
19
20
  "./configs/effect": "./dist/configs/effect.js"
20
21
  },
21
22
  "files": [
22
23
  "dist",
23
- "src",
24
- "!src/**/*.test.ts",
24
+ "src/effect",
25
+ "src/generic",
26
+ "!src/generic/**/*.test.ts",
27
+ "!src/effect/**/*.test.ts",
25
28
  "vendor/anti-slop/src",
26
29
  "vendor/anti-slop/LICENSE",
27
30
  "!vendor/anti-slop/src/**/*.test.ts",
31
+ "!vendor/anti-slop/README.md",
28
32
  "skills",
29
33
  "README.md",
30
34
  "THIRD_PARTY_NOTICES.md",
@@ -37,7 +41,7 @@
37
41
  "format:check": "prettier --check \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.md\" \"*.json\"",
38
42
  "lint": "oxlint src scripts",
39
43
  "skills:validate": "bunx skills-ref validate skills/install-timmo-oxlint-rules && bunx skills-ref validate skills/add-oxlint-rule",
40
- "test": "node --experimental-strip-types src/effect/rules/no-try-catch-in-effect-generators.test.ts && bun test src/install/copy.test.ts",
44
+ "test": "node --experimental-strip-types scripts/test.ts",
41
45
  "typecheck": "tsc --noEmit"
42
46
  },
43
47
  "peerDependencies": {
@@ -21,8 +21,10 @@ description: >-
21
21
  5. Put generic upstream-independent rules under the appropriate locally owned
22
22
  plugin. Never modify `vendor/anti-slop`; propose an upstream contribution
23
23
  separately when Dylan Mulroy's plugin should own the behaviour.
24
- 6. Register the rule in its plugin and matching config, then update the README
25
- rule list and behaviour description.
24
+ 6. Register the rule in its plugin. Verify the matching config and copy command
25
+ discover it from the plugin's `rules` map, then update the README rule list
26
+ and behaviour description. Keep skills workflow-only; do not duplicate rule
27
+ or plugin inventories or counts in them.
26
28
  7. Run `mise run check`, `mise run build`, `npm pack --dry-run`, and
27
29
  `bunx jsr@0.14.3 publish --dry-run` in the central checkout.
28
30
 
@@ -23,9 +23,9 @@ description: >-
23
23
  2. Detect Bun, npm, pnpm, or Yarn from the target's package manager declaration
24
24
  and lockfile. Add an exact development dependency through that package
25
25
  manager.
26
- 3. Extend `@timmo001/oxlint-rules/configs/recommended`. Use `/configs/effect`
27
- instead only when `effect` is a direct dependency or the user explicitly
28
- requests it.
26
+ 3. Extend `@timmo001/oxlint-rules/configs/recommended`. The config owns plugin
27
+ registration and recommended severities. Use `/configs/effect` instead only
28
+ when `effect` is a direct dependency or the user explicitly requests it.
29
29
  4. Keep dependency and config edits visible. Do not delegate them to a script.
30
30
 
31
31
  ## Copy rules
@@ -36,9 +36,11 @@ description: >-
36
36
  meaningful differences before asking whether replacement is intended.
37
37
  3. Run `node scripts/copy.mjs <bun|npm|pnpm|yarn> <destination>`. Add `--force`
38
38
  only after explicit replacement approval.
39
- 4. Register the three entry points printed by the command as `anti-slop`,
40
- `anti-slop-effect`, and `timmo-effect`. Enable `timmo-effect` only for direct
41
- Effect use or an explicit request.
39
+ 4. Register every plugin entry point printed by the command. Merge every printed
40
+ general rule setting into the target config. Merge every printed Effect rule
41
+ setting only for direct Effect use or an explicit request. Treat the command
42
+ output as authoritative rather than maintaining plugin or rule inventories
43
+ in this skill.
42
44
 
43
45
  Run the target repository's normal lint, typecheck, tests, and build. Report
44
46
  package-manager changes, preserved local configuration, enabled rule groups,
@@ -0,0 +1,12 @@
1
+ import { eslintCompatPlugin } from "@oxlint/plugins";
2
+
3
+ import { preferEventParameterTypeRule } from "./rules/prefer-event-parameter-type.ts";
4
+
5
+ const timmoPlugin = eslintCompatPlugin({
6
+ meta: { name: "timmo" },
7
+ rules: {
8
+ "prefer-event-parameter-type": preferEventParameterTypeRule,
9
+ },
10
+ });
11
+
12
+ export default timmoPlugin;
@@ -0,0 +1,83 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+
3
+ import type { ESTree } from "@oxlint/plugins";
4
+
5
+ type FunctionNode = ESTree.Function | ESTree.ArrowFunctionExpression;
6
+ type TypeAssertion = ESTree.TSAsExpression | ESTree.TSTypeAssertion;
7
+
8
+ function nearestEnclosingFunction(node: ESTree.Node): FunctionNode | null {
9
+ let current: ESTree.Node | null = node.parent;
10
+ while (current) {
11
+ if (
12
+ current.type === "FunctionDeclaration" ||
13
+ current.type === "FunctionExpression" ||
14
+ current.type === "ArrowFunctionExpression"
15
+ ) {
16
+ return current;
17
+ }
18
+ current = current.parent;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function assertedEventParameter(node: TypeAssertion): {
24
+ readonly parameter: string;
25
+ readonly property: "currentTarget" | "target";
26
+ } | null {
27
+ const expression = node.expression;
28
+ if (
29
+ expression.type !== "MemberExpression" ||
30
+ expression.computed ||
31
+ expression.object.type !== "Identifier" ||
32
+ expression.property.type !== "Identifier" ||
33
+ (expression.property.name !== "currentTarget" &&
34
+ expression.property.name !== "target")
35
+ ) {
36
+ return null;
37
+ }
38
+ const parameterName = expression.object.name;
39
+ const property = expression.property.name;
40
+ const owner = nearestEnclosingFunction(node);
41
+ if (
42
+ !owner?.params.some(
43
+ (parameter) =>
44
+ parameter.type === "Identifier" && parameter.name === parameterName,
45
+ )
46
+ ) {
47
+ return null;
48
+ }
49
+ return {
50
+ parameter: parameterName,
51
+ property,
52
+ };
53
+ }
54
+
55
+ /** Prefer expressing an event target type in its handler parameter signature. */
56
+ export const preferEventParameterTypeRule = defineRule({
57
+ meta: {
58
+ type: "suggestion",
59
+ docs: {
60
+ description:
61
+ "Prefer typing event target properties in the handler parameter instead of asserting them at use sites.",
62
+ },
63
+ messages: {
64
+ typeEventParameter:
65
+ "Type `{{parameter}}.{{property}}` in the function signature instead of asserting it at the use site.",
66
+ },
67
+ },
68
+ create(context) {
69
+ const checkAssertion = (node: TypeAssertion) => {
70
+ const eventParameter = assertedEventParameter(node);
71
+ if (!eventParameter) return;
72
+ context.report({
73
+ node,
74
+ messageId: "typeEventParameter",
75
+ data: eventParameter,
76
+ });
77
+ };
78
+ return {
79
+ TSAsExpression: checkAssertion,
80
+ TSTypeAssertion: checkAssertion,
81
+ };
82
+ },
83
+ });
package/src/cli.ts DELETED
@@ -1,35 +0,0 @@
1
- #!/usr/bin/env node
2
- import { resolve } from "node:path";
3
-
4
- import { copyRules, DEFAULT_COPY_DESTINATION } from "./install/copy.ts";
5
-
6
- function usage() {
7
- return "Usage: oxlint-rules copy [destination] [--force]";
8
- }
9
-
10
- const [command, ...arguments_] = process.argv.slice(2);
11
- if (command === "--help" || command === "-h") {
12
- console.log(usage());
13
- } else if (command !== "copy") {
14
- console.error(usage());
15
- process.exitCode = 1;
16
- } else {
17
- const force = arguments_.includes("--force");
18
- const positional = arguments_.filter((argument) => argument !== "--force");
19
- if (positional.length > 1) {
20
- console.error(usage());
21
- process.exitCode = 1;
22
- } else {
23
- try {
24
- const destination = resolve(positional[0] ?? DEFAULT_COPY_DESTINATION);
25
- const entries = await copyRules(destination, { force });
26
- console.log("Copied Oxlint plugins:");
27
- console.log(` anti-slop: ${entries.antiSlop}`);
28
- console.log(` anti-slop-effect: ${entries.antiSlopEffect}`);
29
- console.log(` timmo-effect: ${entries.timmoEffect}`);
30
- } catch (error) {
31
- console.error(error instanceof Error ? error.message : String(error));
32
- process.exitCode = 1;
33
- }
34
- }
35
- }
@@ -1,20 +0,0 @@
1
- import { defineConfig } from "oxlint";
2
-
3
- import recommended from "./recommended.ts";
4
-
5
- const effect = defineConfig({
6
- extends: [recommended],
7
- jsPlugins: [
8
- {
9
- name: "anti-slop-effect",
10
- specifier: "@timmo001/oxlint-rules/upstream/effect",
11
- },
12
- { name: "timmo-effect", specifier: "@timmo001/oxlint-rules/effect" },
13
- ],
14
- rules: {
15
- "anti-slop-effect/no-service-constructor-imports": "error",
16
- "timmo-effect/no-try-catch-in-effect-generators": "error",
17
- },
18
- });
19
-
20
- export default effect;
@@ -1,29 +0,0 @@
1
- import { defineConfig } from "oxlint";
2
-
3
- const recommended = defineConfig({
4
- jsPlugins: [
5
- {
6
- name: "anti-slop",
7
- specifier: "@timmo001/oxlint-rules/upstream/anti-slop",
8
- },
9
- ],
10
- rules: {
11
- "anti-slop/no-chained-type-assertions": "error",
12
- "anti-slop/no-conditional-empty-object-spread": "error",
13
- "anti-slop/no-known-value-widening": "error",
14
- "anti-slop/no-module-mocking": "error",
15
- "anti-slop/no-object-parameters": "error",
16
- "anti-slop/no-reflect-apply": "error",
17
- "anti-slop/no-reflect-get": "error",
18
- "anti-slop/no-runtime-typeof": "error",
19
- "anti-slop/no-shape-in-symbol-names": "error",
20
- "anti-slop/no-unknown-parameters": "error",
21
- "anti-slop/no-unknown-returns": "error",
22
- "anti-slop/no-unknown-type-aliases": "error",
23
- "anti-slop/no-unsafe-dictionary-type": "error",
24
- "anti-slop/no-widen-then-assert": "error",
25
- "anti-slop/require-safety-comment-for-type-assertion": "error",
26
- },
27
- });
28
-
29
- export default recommended;
@@ -1,77 +0,0 @@
1
- import { cp, mkdir, readdir, rm } from "node:fs/promises";
2
- import { dirname, join, relative, resolve, sep } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- export const DEFAULT_COPY_DESTINATION = "tools/oxlint/timmo-rules";
6
-
7
- export interface CopyRulesOptions {
8
- readonly force?: boolean;
9
- readonly sourceRoot?: string;
10
- }
11
-
12
- export interface CopiedRuleEntryPoints {
13
- readonly antiSlop: string;
14
- readonly antiSlopEffect: string;
15
- readonly timmoEffect: string;
16
- }
17
-
18
- function packageRoot() {
19
- const current = dirname(fileURLToPath(import.meta.url));
20
- return current.endsWith(join("src", "install"))
21
- ? resolve(current, "../..")
22
- : resolve(current, "..");
23
- }
24
-
25
- async function copyTree(source: string, destination: string) {
26
- await cp(source, destination, {
27
- recursive: true,
28
- filter: (path) =>
29
- !path.endsWith(".test.ts") && !path.includes(`${join("", ".git")}`),
30
- });
31
- }
32
-
33
- export async function copyRules(
34
- destination: string,
35
- options: CopyRulesOptions = {},
36
- ): Promise<CopiedRuleEntryPoints> {
37
- const target = resolve(destination);
38
- const entries = await readdir(dirname(target), { withFileTypes: true }).catch(
39
- () => [],
40
- );
41
- if (entries.some((entry) => entry.name === target.split(/[\\/]/u).at(-1))) {
42
- if (!options.force) {
43
- throw new Error(
44
- `Destination already exists: ${target}. Pass --force to replace it.`,
45
- );
46
- }
47
- await rm(target, { force: true, recursive: true });
48
- }
49
-
50
- const root = options.sourceRoot ?? packageRoot();
51
- await mkdir(join(target, "upstream"), { recursive: true });
52
- await copyTree(
53
- join(root, "vendor/anti-slop/src"),
54
- join(target, "upstream/anti-slop"),
55
- );
56
- await cp(
57
- join(root, "vendor/anti-slop/LICENSE"),
58
- join(target, "upstream/anti-slop/LICENSE"),
59
- );
60
- await rm(join(target, "upstream/anti-slop/effect"), {
61
- force: true,
62
- recursive: true,
63
- });
64
- await copyTree(
65
- join(root, "vendor/anti-slop/src/effect"),
66
- join(target, "upstream/effect"),
67
- );
68
- await copyTree(join(root, "src/effect"), join(target, "effect"));
69
-
70
- const displayRoot = relative(process.cwd(), target) || ".";
71
- const entryPoint = (path: string) => `./${path.split(sep).join("/")}`;
72
- return {
73
- antiSlop: entryPoint(join(displayRoot, "upstream/anti-slop/index.ts")),
74
- antiSlopEffect: entryPoint(join(displayRoot, "upstream/effect/index.ts")),
75
- timmoEffect: entryPoint(join(displayRoot, "effect/index.ts")),
76
- };
77
- }
@@ -1,7 +0,0 @@
1
- import type { OxlintConfig } from "oxlint";
2
-
3
- import bundledConfig from "../../dist/configs/effect.js";
4
-
5
- const config: OxlintConfig = bundledConfig;
6
-
7
- export default config;
package/src/jsr/effect.ts DELETED
@@ -1,7 +0,0 @@
1
- import type { Plugin } from "@oxlint/plugins";
2
-
3
- import bundledPlugin from "../../dist/effect/index.js";
4
-
5
- const plugin: Plugin = bundledPlugin;
6
-
7
- export default plugin;
@@ -1,7 +0,0 @@
1
- import type { OxlintConfig } from "oxlint";
2
-
3
- import bundledConfig from "../../dist/configs/recommended.js";
4
-
5
- const config: OxlintConfig = bundledConfig;
6
-
7
- export default config;
@@ -1,7 +0,0 @@
1
- import type { Plugin } from "@oxlint/plugins";
2
-
3
- import bundledPlugin from "../../dist/upstream/anti-slop.js";
4
-
5
- const plugin: Plugin = bundledPlugin;
6
-
7
- export default plugin;
@@ -1,7 +0,0 @@
1
- import type { Plugin } from "@oxlint/plugins";
2
-
3
- import bundledPlugin from "../../dist/upstream/effect.js";
4
-
5
- const plugin: Plugin = bundledPlugin;
6
-
7
- export default plugin;
@@ -1 +0,0 @@
1
- export { default } from "../../vendor/anti-slop/src/index.ts";
@@ -1 +0,0 @@
1
- export { default } from "../../vendor/anti-slop/src/effect/index.ts";