@timmo001/oxlint-rules 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +102 -0
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/dist/cli.js +78 -0
- package/dist/configs/effect.js +52 -0
- package/dist/configs/recommended.js +35 -0
- package/dist/effect/index.js +106 -0
- package/dist/upstream/anti-slop.js +1838 -0
- package/dist/upstream/effect.js +59 -0
- package/package.json +61 -0
- package/skills/add-oxlint-rule/SKILL.md +31 -0
- package/skills/install-timmo-oxlint-rules/SKILL.md +45 -0
- package/skills/install-timmo-oxlint-rules/scripts/copy.mjs +30 -0
- package/src/cli.ts +35 -0
- package/src/configs/effect.ts +20 -0
- package/src/configs/recommended.ts +29 -0
- package/src/effect/index.ts +12 -0
- package/src/effect/rules/no-try-catch-in-effect-generators.ts +140 -0
- package/src/install/copy.ts +77 -0
- package/src/jsr/effect-config.ts +7 -0
- package/src/jsr/effect.ts +7 -0
- package/src/jsr/recommended.ts +7 -0
- package/src/jsr/upstream-anti-slop.ts +7 -0
- package/src/jsr/upstream-effect.ts +7 -0
- package/src/upstream/anti-slop.ts +1 -0
- package/src/upstream/effect.ts +1 -0
- package/vendor/anti-slop/LICENSE +21 -0
- package/vendor/anti-slop/README.md +299 -0
- package/vendor/anti-slop/src/effect/index.ts +13 -0
- package/vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts +52 -0
- package/vendor/anti-slop/src/index.ts +41 -0
- package/vendor/anti-slop/src/rules/no-chained-type-assertions.ts +77 -0
- package/vendor/anti-slop/src/rules/no-conditional-empty-object-spread.ts +49 -0
- package/vendor/anti-slop/src/rules/no-known-value-widening.ts +427 -0
- package/vendor/anti-slop/src/rules/no-module-mocking.ts +91 -0
- package/vendor/anti-slop/src/rules/no-object-parameters.ts +83 -0
- package/vendor/anti-slop/src/rules/no-reflect-apply.ts +28 -0
- package/vendor/anti-slop/src/rules/no-reflect-get.ts +28 -0
- package/vendor/anti-slop/src/rules/no-runtime-typeof.ts +77 -0
- package/vendor/anti-slop/src/rules/no-shape-in-symbol-names.ts +46 -0
- package/vendor/anti-slop/src/rules/no-unknown-parameters.ts +69 -0
- package/vendor/anti-slop/src/rules/no-unknown-returns.ts +82 -0
- package/vendor/anti-slop/src/rules/no-unknown-type-aliases.ts +54 -0
- package/vendor/anti-slop/src/rules/no-unsafe-dictionary-type.ts +154 -0
- package/vendor/anti-slop/src/rules/no-widen-then-assert.ts +366 -0
- package/vendor/anti-slop/src/rules/require-safety-comment-for-type-assertion.ts +131 -0
- package/vendor/anti-slop/src/shared/dictionary-types.ts +515 -0
- package/vendor/anti-slop/src/shared/function-parameters.ts +49 -0
- package/vendor/anti-slop/src/shared/lexical-type-parameters.ts +61 -0
- package/vendor/anti-slop/src/shared/reflect-method.ts +35 -0
- package/vendor/anti-slop/src/shared/type-alias-resolution.ts +250 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// vendor/anti-slop/src/effect/index.ts
|
|
2
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
3
|
+
|
|
4
|
+
// vendor/anti-slop/src/effect/rules/no-service-constructor-imports.ts
|
|
5
|
+
import { defineRule } from "@oxlint/plugins";
|
|
6
|
+
var SERVICE_CONSTRUCTOR_NAME = /^make[A-Z]/u;
|
|
7
|
+
var TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/u;
|
|
8
|
+
function isProjectLocalImport(source) {
|
|
9
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
10
|
+
}
|
|
11
|
+
function getImportedName(specifier) {
|
|
12
|
+
if (specifier.imported.type === "Identifier")
|
|
13
|
+
return specifier.imported.name;
|
|
14
|
+
return specifier.imported.value;
|
|
15
|
+
}
|
|
16
|
+
var noServiceConstructorImportsRule = defineRule({
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Disallow project-local make<CapabilityName> imports outside test and spec files."
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
const isTestFile = TEST_FILE.test(context.filename.replaceAll("\\", "/"));
|
|
28
|
+
return {
|
|
29
|
+
ImportDeclaration(node) {
|
|
30
|
+
if (isTestFile || !isProjectLocalImport(node.source.value))
|
|
31
|
+
return;
|
|
32
|
+
for (const specifier of node.specifiers) {
|
|
33
|
+
if (specifier.type !== "ImportSpecifier")
|
|
34
|
+
continue;
|
|
35
|
+
const importedName = getImportedName(specifier);
|
|
36
|
+
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName))
|
|
37
|
+
continue;
|
|
38
|
+
context.report({
|
|
39
|
+
node: specifier,
|
|
40
|
+
messageId: "serviceConstructorImport",
|
|
41
|
+
data: { name: importedName }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// vendor/anti-slop/src/effect/index.ts
|
|
50
|
+
var antiSlopEffectPlugin = eslintCompatPlugin({
|
|
51
|
+
meta: { name: "anti-slop-effect" },
|
|
52
|
+
rules: {
|
|
53
|
+
"no-service-constructor-imports": noServiceConstructorImportsRule
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
var effect_default = antiSlopEffectPlugin;
|
|
57
|
+
export {
|
|
58
|
+
effect_default as default
|
|
59
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timmo001/oxlint-rules",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Oxlint plugins and configs with optional Effect rules.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/timmo001/oxlint-rules.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"oxlint-rules": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./upstream/anti-slop": "./dist/upstream/anti-slop.js",
|
|
16
|
+
"./upstream/effect": "./dist/upstream/effect.js",
|
|
17
|
+
"./effect": "./dist/effect/index.js",
|
|
18
|
+
"./configs/recommended": "./dist/configs/recommended.js",
|
|
19
|
+
"./configs/effect": "./dist/configs/effect.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src",
|
|
24
|
+
"!src/**/*.test.ts",
|
|
25
|
+
"vendor/anti-slop/src",
|
|
26
|
+
"vendor/anti-slop/LICENSE",
|
|
27
|
+
"!vendor/anti-slop/src/**/*.test.ts",
|
|
28
|
+
"skills",
|
|
29
|
+
"README.md",
|
|
30
|
+
"THIRD_PARTY_NOTICES.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun run scripts/build.ts",
|
|
35
|
+
"check": "bun run build && bun run test && bun run lint && bun run typecheck && bun run format:check && bun run skills:validate",
|
|
36
|
+
"format": "prettier --write \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.md\" \"*.json\"",
|
|
37
|
+
"format:check": "prettier --check \"{src,scripts}/**/*.ts\" \"skills/**/*.{md,mjs}\" \"*.md\" \"*.json\"",
|
|
38
|
+
"lint": "oxlint src scripts",
|
|
39
|
+
"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",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@oxlint/plugins": "1.81.0",
|
|
45
|
+
"oxlint": "1.81.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@oxlint/plugins": "1.81.0",
|
|
49
|
+
"@types/bun": "^1.3.14",
|
|
50
|
+
"oxlint": "1.81.0",
|
|
51
|
+
"prettier": "^3.9.5",
|
|
52
|
+
"skills-ref": "0.1.5",
|
|
53
|
+
"typescript": "^7.0.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-oxlint-rule
|
|
3
|
+
description: >-
|
|
4
|
+
Create or revise a centrally maintained rule in @timmo001/oxlint-rules. Use
|
|
5
|
+
for requests to add an Oxlint anti-slop rule, change an existing central
|
|
6
|
+
rule, or promote a repository-specific lint preference into the shared
|
|
7
|
+
package.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Add An Oxlint Rule
|
|
11
|
+
|
|
12
|
+
1. Treat the current repository as fixture context, not automatically as the
|
|
13
|
+
central package checkout.
|
|
14
|
+
2. Search available writable checkouts by their Git remote for
|
|
15
|
+
`timmo001/oxlint-rules`. If none exists, ask where the user wants to clone or
|
|
16
|
+
fork it. Do not assume the user can write to the upstream account.
|
|
17
|
+
3. Read the central repository guidance, relevant plugin registration, config,
|
|
18
|
+
nearby rules, tests, and README rule list.
|
|
19
|
+
4. Define the narrow syntax contract. Add at least one failing fixture and one
|
|
20
|
+
valid fixture before implementing the rule.
|
|
21
|
+
5. Put generic upstream-independent rules under the appropriate locally owned
|
|
22
|
+
plugin. Never modify `vendor/anti-slop`; propose an upstream contribution
|
|
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.
|
|
26
|
+
7. Run `mise run check`, `mise run build`, `npm pack --dry-run`, and
|
|
27
|
+
`bunx jsr@0.14.3 publish --dry-run` in the central checkout.
|
|
28
|
+
|
|
29
|
+
Report the fixture contract, registration and docs changed, checks, and any
|
|
30
|
+
consumer rollout left for a separate stage. Do not publish or assume a local
|
|
31
|
+
machine path.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install-timmo-oxlint-rules
|
|
3
|
+
description: >-
|
|
4
|
+
Install or copy @timmo001/oxlint-rules into a JavaScript or TypeScript
|
|
5
|
+
repository. Use when adding the shared anti-slop Oxlint config, enabling its
|
|
6
|
+
Effect rules, or replacing a local anti-slop copy.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Install Timmo Oxlint Rules
|
|
10
|
+
|
|
11
|
+
1. Ask one structured question before changing files: `Package (Recommended)`
|
|
12
|
+
or `Copy rules`.
|
|
13
|
+
2. Inspect the target's manifests, lockfiles, Oxlint config, repository
|
|
14
|
+
instructions, and normal checks. Use the current working directory unless
|
|
15
|
+
the user names another target.
|
|
16
|
+
3. Preserve existing ignores, overrides, plugins, and repository-owned rules.
|
|
17
|
+
Confirm `oxlint` and `@oxlint/plugins` use the same exact version supported
|
|
18
|
+
by the selected package version.
|
|
19
|
+
|
|
20
|
+
## Package
|
|
21
|
+
|
|
22
|
+
1. Ask whether to use npmjs.org or JSR.
|
|
23
|
+
2. Detect Bun, npm, pnpm, or Yarn from the target's package manager declaration
|
|
24
|
+
and lockfile. Add an exact development dependency through that package
|
|
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.
|
|
29
|
+
4. Keep dependency and config edits visible. Do not delegate them to a script.
|
|
30
|
+
|
|
31
|
+
## Copy rules
|
|
32
|
+
|
|
33
|
+
1. Ask for a repository-relative destination. Do not assume a personal
|
|
34
|
+
filesystem layout.
|
|
35
|
+
2. If the destination exists, compare it with the proposed source and explain
|
|
36
|
+
meaningful differences before asking whether replacement is intended.
|
|
37
|
+
3. Run `node scripts/copy.mjs <bun|npm|pnpm|yarn> <destination>`. Add `--force`
|
|
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.
|
|
42
|
+
|
|
43
|
+
Run the target repository's normal lint, typecheck, tests, and build. Report
|
|
44
|
+
package-manager changes, preserved local configuration, enabled rule groups,
|
|
45
|
+
and checks.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const [runner, ...copyArguments] = process.argv.slice(2);
|
|
5
|
+
const packageName = "@timmo001/oxlint-rules";
|
|
6
|
+
const commands = {
|
|
7
|
+
bun: ["bunx", packageName, "copy", ...copyArguments],
|
|
8
|
+
npm: [
|
|
9
|
+
"npm",
|
|
10
|
+
"exec",
|
|
11
|
+
"--yes",
|
|
12
|
+
`--package=${packageName}`,
|
|
13
|
+
"--",
|
|
14
|
+
"oxlint-rules",
|
|
15
|
+
"copy",
|
|
16
|
+
...copyArguments,
|
|
17
|
+
],
|
|
18
|
+
pnpm: ["pnpm", "dlx", packageName, "copy", ...copyArguments],
|
|
19
|
+
yarn: ["yarn", "dlx", packageName, "copy", ...copyArguments],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const command = commands[runner];
|
|
23
|
+
if (!command) {
|
|
24
|
+
console.error("Usage: copy.mjs <bun|npm|pnpm|yarn> <destination> [--force]");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = spawnSync(command[0], command.slice(1), { stdio: "inherit" });
|
|
29
|
+
if (result.error) throw result.error;
|
|
30
|
+
process.exit(result.status ?? 1);
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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;
|
|
@@ -0,0 +1,29 @@
|
|
|
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;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { eslintCompatPlugin } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import { noTryCatchInEffectGeneratorsRule } from "./rules/no-try-catch-in-effect-generators.ts";
|
|
4
|
+
|
|
5
|
+
const timmoEffectPlugin = eslintCompatPlugin({
|
|
6
|
+
meta: { name: "timmo-effect" },
|
|
7
|
+
rules: {
|
|
8
|
+
"no-try-catch-in-effect-generators": noTryCatchInEffectGeneratorsRule,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export default timmoEffectPlugin;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
import type { ESTree, Scope, SourceCode, Variable } from "@oxlint/plugins";
|
|
4
|
+
|
|
5
|
+
type FunctionNode = ESTree.Function | ESTree.ArrowFunctionExpression;
|
|
6
|
+
|
|
7
|
+
function resolveVariable(
|
|
8
|
+
sourceCode: SourceCode,
|
|
9
|
+
identifier: ESTree.IdentifierReference,
|
|
10
|
+
): Variable | null {
|
|
11
|
+
let scope: Scope | null = sourceCode.getScope(identifier);
|
|
12
|
+
while (scope) {
|
|
13
|
+
const variable = scope.set.get(identifier.name);
|
|
14
|
+
if (variable) return variable;
|
|
15
|
+
scope = scope.upper;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nearestEnclosingFunction(node: ESTree.Node): FunctionNode | null {
|
|
21
|
+
let current: ESTree.Node | null = node.parent;
|
|
22
|
+
while (current) {
|
|
23
|
+
if (
|
|
24
|
+
current.type === "FunctionDeclaration" ||
|
|
25
|
+
current.type === "FunctionExpression" ||
|
|
26
|
+
current.type === "ArrowFunctionExpression"
|
|
27
|
+
) {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
current = current.parent;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function staticMemberName(node: ESTree.Expression): string | null {
|
|
36
|
+
if (node.type !== "MemberExpression" || node.computed) return null;
|
|
37
|
+
return node.property.type === "Identifier" ? node.property.name : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isEffectImport(
|
|
41
|
+
sourceCode: SourceCode,
|
|
42
|
+
identifier: ESTree.IdentifierReference,
|
|
43
|
+
namespace: boolean,
|
|
44
|
+
): boolean {
|
|
45
|
+
const variable = resolveVariable(sourceCode, identifier);
|
|
46
|
+
return (
|
|
47
|
+
variable?.defs.some((definition) => {
|
|
48
|
+
if (
|
|
49
|
+
definition.type !== "ImportBinding" ||
|
|
50
|
+
definition.parent?.type !== "ImportDeclaration" ||
|
|
51
|
+
definition.parent.source.value !== "effect"
|
|
52
|
+
) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (namespace) return definition.node.type === "ImportNamespaceSpecifier";
|
|
56
|
+
if (definition.node.type !== "ImportSpecifier") return false;
|
|
57
|
+
const imported = definition.node.imported;
|
|
58
|
+
return (
|
|
59
|
+
(imported.type === "Identifier" ? imported.name : imported.value) ===
|
|
60
|
+
"Effect"
|
|
61
|
+
);
|
|
62
|
+
}) ?? false
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isEffectMethod(
|
|
67
|
+
sourceCode: SourceCode,
|
|
68
|
+
node: ESTree.Expression,
|
|
69
|
+
method: "fn" | "gen",
|
|
70
|
+
): boolean {
|
|
71
|
+
if (staticMemberName(node) !== method || node.type !== "MemberExpression") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const object = node.object;
|
|
75
|
+
if (object.type === "Identifier") {
|
|
76
|
+
return isEffectImport(sourceCode, object, false);
|
|
77
|
+
}
|
|
78
|
+
if (
|
|
79
|
+
staticMemberName(object) !== "Effect" ||
|
|
80
|
+
object.type !== "MemberExpression" ||
|
|
81
|
+
object.object.type !== "Identifier"
|
|
82
|
+
) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return isEffectImport(sourceCode, object.object, true);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isDirectArgument(
|
|
89
|
+
owner: FunctionNode,
|
|
90
|
+
call: ESTree.CallExpression,
|
|
91
|
+
): boolean {
|
|
92
|
+
return call.arguments.some((argument) => argument === owner);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isRecognisedEffectGenerator(
|
|
96
|
+
sourceCode: SourceCode,
|
|
97
|
+
owner: FunctionNode,
|
|
98
|
+
): boolean {
|
|
99
|
+
const parent = owner.parent;
|
|
100
|
+
if (parent.type !== "CallExpression" || !isDirectArgument(owner, parent)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (isEffectMethod(sourceCode, parent.callee, "gen")) return true;
|
|
104
|
+
|
|
105
|
+
const factoryCall = parent.callee;
|
|
106
|
+
return (
|
|
107
|
+
factoryCall.type === "CallExpression" &&
|
|
108
|
+
isEffectMethod(sourceCode, factoryCall.callee, "fn")
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Keep expected failures in the Effect error channel inside Effect generators. */
|
|
113
|
+
export const noTryCatchInEffectGeneratorsRule = defineRule({
|
|
114
|
+
meta: {
|
|
115
|
+
type: "problem",
|
|
116
|
+
docs: {
|
|
117
|
+
description:
|
|
118
|
+
"Disallow synchronous try/catch owned by recognised Effect generator callbacks.",
|
|
119
|
+
},
|
|
120
|
+
messages: {
|
|
121
|
+
useEffectErrorChannel:
|
|
122
|
+
"Keep expected failures in the Effect error channel. Use Effect.try for synchronous throwing work, Effect.tryPromise for asynchronous throwing work, Effect-returning schema APIs for decoding, and Effect recovery combinators for recovery.",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
create(context) {
|
|
126
|
+
return {
|
|
127
|
+
TryStatement(node) {
|
|
128
|
+
if (!node.handler) return;
|
|
129
|
+
const owner = nearestEnclosingFunction(node);
|
|
130
|
+
if (
|
|
131
|
+
!owner?.generator ||
|
|
132
|
+
!isRecognisedEffectGenerator(context.sourceCode, owner)
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
context.report({ node, messageId: "useEffectErrorChannel" });
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../vendor/anti-slop/src/index.ts";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "../../vendor/anti-slop/src/effect/index.ts";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dillon Mulroy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|