@commonplaces/oxlint-plugin 0.0.0-stage → 0.1.1
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 +21 -0
- package/README.md +130 -2
- package/dist/alchemy.d.ts +2 -0
- package/dist/alchemy.js +196 -0
- package/dist/anti-slop/effect/index.d.ts +3 -0
- package/dist/anti-slop/effect/index.js +10 -0
- package/dist/anti-slop/effect/rules/no-service-constructor-imports.d.ts +2 -0
- package/dist/anti-slop/effect/rules/no-service-constructor-imports.js +45 -0
- package/dist/anti-slop/index.d.ts +3 -0
- package/dist/anti-slop/index.js +38 -0
- package/dist/anti-slop/rules/no-chained-type-assertions.d.ts +2 -0
- package/dist/anti-slop/rules/no-chained-type-assertions.js +57 -0
- package/dist/anti-slop/rules/no-conditional-empty-object-spread.d.ts +2 -0
- package/dist/anti-slop/rules/no-conditional-empty-object-spread.js +36 -0
- package/dist/anti-slop/rules/no-known-value-widening.d.ts +2 -0
- package/dist/anti-slop/rules/no-known-value-widening.js +171 -0
- package/dist/anti-slop/rules/no-module-mocking.d.ts +2 -0
- package/dist/anti-slop/rules/no-module-mocking.js +73 -0
- package/dist/anti-slop/rules/no-object-parameters.d.ts +2 -0
- package/dist/anti-slop/rules/no-object-parameters.js +86 -0
- package/dist/anti-slop/rules/no-reflect-apply.d.ts +2 -0
- package/dist/anti-slop/rules/no-reflect-apply.js +25 -0
- package/dist/anti-slop/rules/no-reflect-get.d.ts +2 -0
- package/dist/anti-slop/rules/no-reflect-get.js +25 -0
- package/dist/anti-slop/rules/no-runtime-typeof.d.ts +2 -0
- package/dist/anti-slop/rules/no-runtime-typeof.js +47 -0
- package/dist/anti-slop/rules/no-shape-in-symbol-names.d.ts +2 -0
- package/dist/anti-slop/rules/no-shape-in-symbol-names.js +33 -0
- package/dist/anti-slop/rules/no-unknown-parameters.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-parameters.js +69 -0
- package/dist/anti-slop/rules/no-unknown-returns.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-returns.js +77 -0
- package/dist/anti-slop/rules/no-unknown-type-aliases.d.ts +2 -0
- package/dist/anti-slop/rules/no-unknown-type-aliases.js +58 -0
- package/dist/anti-slop/rules/no-unsafe-dictionary-type.d.ts +2 -0
- package/dist/anti-slop/rules/no-unsafe-dictionary-type.js +111 -0
- package/dist/anti-slop/rules/no-widen-then-assert.d.ts +2 -0
- package/dist/anti-slop/rules/no-widen-then-assert.js +255 -0
- package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
- package/dist/anti-slop/rules/require-safety-comment-for-type-assertion.js +51 -0
- package/dist/anti-slop/shared/dictionary-types.d.ts +21 -0
- package/dist/anti-slop/shared/dictionary-types.js +374 -0
- package/dist/anti-slop/shared/lexical-type-parameters.d.ts +3 -0
- package/dist/anti-slop/shared/lexical-type-parameters.js +25 -0
- package/dist/anti-slop/shared/reflect-method.d.ts +3 -0
- package/dist/anti-slop/shared/reflect-method.js +24 -0
- package/dist/bun.d.ts +9 -0
- package/dist/bun.js +30 -0
- package/dist/effect.d.ts +17 -0
- package/dist/effect.js +352 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +62 -0
- package/dist/package.d.ts +9 -0
- package/dist/package.js +50 -0
- package/dist/strict-effect.d.ts +2 -0
- package/dist/strict-effect.js +2381 -0
- package/dist/syntax.d.ts +5 -0
- package/dist/syntax.js +26 -0
- package/package.json +77 -4
package/dist/effect.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { Array, Option, Predicate, Schema } from "effect";
|
|
3
|
+
import { childNodes, propertyName, variableInScope } from "./syntax.js";
|
|
4
|
+
const memberExpressionName = (node) => {
|
|
5
|
+
if (node?.type !== "MemberExpression" || node.object?.type !== "Identifier") {
|
|
6
|
+
return Option.none();
|
|
7
|
+
}
|
|
8
|
+
const property = propertyName(node.property);
|
|
9
|
+
const objectName = node.object.name;
|
|
10
|
+
return Option.map(property, (name) => `${objectName}.${name}`);
|
|
11
|
+
};
|
|
12
|
+
const isMemberCall = (node, objectName, methodName) => node?.type === "CallExpression" &&
|
|
13
|
+
node.callee?.type === "MemberExpression" &&
|
|
14
|
+
node.callee.object?.type === "Identifier" &&
|
|
15
|
+
node.callee.object.name === objectName &&
|
|
16
|
+
Option.contains(propertyName(node.callee.property), methodName);
|
|
17
|
+
const noAmbientProcess = defineRule({
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
MemberExpression(node) {
|
|
21
|
+
if (Option.exists(memberExpressionName(node), (name) => Array.contains(["process.argv", "process.env", "process.exit"], name))) {
|
|
22
|
+
context.report({ messageId: "noAmbientProcess", node });
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
meta: {
|
|
28
|
+
docs: { description: "Disallow ambient process input and exit control." },
|
|
29
|
+
messages: {
|
|
30
|
+
noAmbientProcess: "Use decoded command input, Effect Config and typed command results instead of ambient process state.",
|
|
31
|
+
},
|
|
32
|
+
type: "problem",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const noRawFetch = defineRule({
|
|
36
|
+
create(context) {
|
|
37
|
+
return {
|
|
38
|
+
CallExpression(node) {
|
|
39
|
+
const isBareFetch = node.callee?.type === "Identifier" && node.callee.name === "fetch";
|
|
40
|
+
const isGlobalFetch = node.callee?.type === "MemberExpression" &&
|
|
41
|
+
node.callee.object?.type === "Identifier" &&
|
|
42
|
+
["globalThis", "window"].includes(node.callee.object.name) &&
|
|
43
|
+
Option.contains(propertyName(node.callee.property), "fetch");
|
|
44
|
+
if (isBareFetch || isGlobalFetch) {
|
|
45
|
+
context.report({ messageId: "noRawFetch", node: node.callee });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
meta: {
|
|
51
|
+
docs: { description: "Disallow raw fetch in Effect-owned TypeScript." },
|
|
52
|
+
messages: {
|
|
53
|
+
noRawFetch: "Use Effect HttpClient and decode the response with its owning Schema.",
|
|
54
|
+
},
|
|
55
|
+
type: "problem",
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const noUncheckedJson = defineRule({
|
|
59
|
+
create(context) {
|
|
60
|
+
return {
|
|
61
|
+
CallExpression(node) {
|
|
62
|
+
if (isMemberCall(node, "JSON", "parse")) {
|
|
63
|
+
context.report({ messageId: "noUncheckedJson", node: node.callee });
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
meta: {
|
|
69
|
+
docs: { description: "Disallow JSON.parse without an owning Schema." },
|
|
70
|
+
messages: {
|
|
71
|
+
noUncheckedJson: "Decode JSON text with the owning Effect Schema.",
|
|
72
|
+
},
|
|
73
|
+
type: "problem",
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const noConsole = defineRule({
|
|
77
|
+
create(context) {
|
|
78
|
+
return {
|
|
79
|
+
MemberExpression(node) {
|
|
80
|
+
if (node.object?.type === "Identifier" && node.object.name === "console") {
|
|
81
|
+
context.report({ messageId: "noConsole", node });
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
meta: {
|
|
87
|
+
docs: { description: "Disallow raw console calls in Effect-owned TypeScript." },
|
|
88
|
+
messages: {
|
|
89
|
+
noConsole: "Use Effect Console or structured Effect logging.",
|
|
90
|
+
},
|
|
91
|
+
type: "problem",
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const isFunctionNode = (node) => node.type === "ArrowFunctionExpression" ||
|
|
95
|
+
node.type === "FunctionDeclaration" ||
|
|
96
|
+
node.type === "FunctionExpression";
|
|
97
|
+
const nearestFunction = (node) => node === null
|
|
98
|
+
? Option.none()
|
|
99
|
+
: isFunctionNode(node)
|
|
100
|
+
? Option.some(node)
|
|
101
|
+
: nearestFunction(node.parent);
|
|
102
|
+
const isAllowedPromiseBoundary = (node) => {
|
|
103
|
+
const parent = node?.parent;
|
|
104
|
+
if (parent?.type === "CallExpression" && parent.arguments?.includes(node)) {
|
|
105
|
+
return (parent.callee?.type === "MemberExpression" &&
|
|
106
|
+
parent.callee.object?.type === "Identifier" &&
|
|
107
|
+
parent.callee.object.name === "Effect" &&
|
|
108
|
+
Option.exists(propertyName(parent.callee.property), (name) => Array.contains(["promise", "tryPromise"], name)));
|
|
109
|
+
}
|
|
110
|
+
if (parent?.type !== "Property" || !Option.contains(propertyName(parent.key), "try")) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const options = parent.parent;
|
|
114
|
+
const call = options?.parent;
|
|
115
|
+
return (options?.type === "ObjectExpression" &&
|
|
116
|
+
call?.type === "CallExpression" &&
|
|
117
|
+
call.arguments?.includes(options) &&
|
|
118
|
+
call.callee?.type === "MemberExpression" &&
|
|
119
|
+
call.callee.object?.type === "Identifier" &&
|
|
120
|
+
call.callee.object.name === "Effect" &&
|
|
121
|
+
Option.contains(propertyName(call.callee.property), "tryPromise"));
|
|
122
|
+
};
|
|
123
|
+
const noAsyncAwaitPromise = defineRule({
|
|
124
|
+
create(context) {
|
|
125
|
+
const reportAsync = (node) => {
|
|
126
|
+
if (node.async && !isAllowedPromiseBoundary(node)) {
|
|
127
|
+
context.report({ messageId: "noAsync", node });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
ArrowFunctionExpression: reportAsync,
|
|
132
|
+
AwaitExpression(node) {
|
|
133
|
+
if (!Option.exists(nearestFunction(node.parent), isAllowedPromiseBoundary)) {
|
|
134
|
+
context.report({ messageId: "noAwait", node });
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
FunctionDeclaration: reportAsync,
|
|
138
|
+
FunctionExpression: reportAsync,
|
|
139
|
+
Identifier(node) {
|
|
140
|
+
if (node.name === "Promise" &&
|
|
141
|
+
node.parent?.type === "TSTypeReference" &&
|
|
142
|
+
node.parent.typeName === node) {
|
|
143
|
+
context.report({ messageId: "noPromiseType", node });
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
NewExpression(node) {
|
|
147
|
+
if (node.callee?.type === "Identifier" && node.callee.name === "Promise") {
|
|
148
|
+
context.report({ messageId: "noPromise", node: node.callee });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
meta: {
|
|
154
|
+
docs: { description: "Disallow raw Promise and async orchestration." },
|
|
155
|
+
messages: {
|
|
156
|
+
noAsync: "Return an Effect program instead of declaring an async function.",
|
|
157
|
+
noAwait: "Compose Effect programs instead of awaiting raw Promises.",
|
|
158
|
+
noPromise: "Use Effect.async, Effect.promise or Effect.tryPromise at the host boundary.",
|
|
159
|
+
noPromiseType: "Expose an Effect result instead of a raw Promise type.",
|
|
160
|
+
},
|
|
161
|
+
type: "problem",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
const noInstanceof = defineRule({
|
|
165
|
+
create(context) {
|
|
166
|
+
return {
|
|
167
|
+
BinaryExpression(node) {
|
|
168
|
+
if (node.operator === "instanceof") {
|
|
169
|
+
context.report({ messageId: "noInstanceof", node });
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
meta: {
|
|
175
|
+
docs: { description: "Disallow instanceof checks in Effect-owned TypeScript." },
|
|
176
|
+
messages: {
|
|
177
|
+
noInstanceof: "Use tagged data, Schema guards or Effect matchers instead of instanceof.",
|
|
178
|
+
},
|
|
179
|
+
type: "problem",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
const isManualTagKey = (node) => {
|
|
183
|
+
const parent = node?.parent;
|
|
184
|
+
if (!Option.contains(propertyName(node), "_tag")) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (parent?.type === "MemberExpression") {
|
|
188
|
+
return parent.property === node;
|
|
189
|
+
}
|
|
190
|
+
if (parent?.type === "Property" ||
|
|
191
|
+
parent?.type === "PropertyDefinition" ||
|
|
192
|
+
parent?.type === "TSPropertySignature") {
|
|
193
|
+
return parent.key === node;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
const noManualTag = defineRule({
|
|
198
|
+
create(context) {
|
|
199
|
+
const reportTag = (node) => {
|
|
200
|
+
if (isManualTagKey(node)) {
|
|
201
|
+
context.report({ messageId: "noManualTag", node });
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
Identifier: reportTag,
|
|
206
|
+
Literal: reportTag,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
meta: {
|
|
210
|
+
docs: { description: "Disallow manual Effect tag definitions and checks." },
|
|
211
|
+
messages: {
|
|
212
|
+
noManualTag: "Use Data or Schema tagged classes to define variants, then use Match, Schema.is or the matching Effect primitive to inspect them.",
|
|
213
|
+
},
|
|
214
|
+
type: "problem",
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const RuntimeOptions = Schema.Struct({ allowedFiles: Schema.Array(Schema.String) });
|
|
218
|
+
const ServiceOptions = Schema.Struct({ serviceNames: Schema.Array(Schema.String) });
|
|
219
|
+
const noRuntimeExecutionOutsideMain = defineRule({
|
|
220
|
+
meta: {
|
|
221
|
+
type: "problem",
|
|
222
|
+
schema: [
|
|
223
|
+
{
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: { allowedFiles: { type: "array", items: { type: "string", minLength: 1 } } },
|
|
226
|
+
required: ["allowedFiles"],
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
docs: { description: "Restrict Effect runtime execution to configured entry files." },
|
|
231
|
+
messages: {
|
|
232
|
+
noRuntimeExecutionOutsideMain: "Only configured entry files may execute the Effect runtime. Packages return Effect programs.",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
create(context) {
|
|
236
|
+
// Oxlint requires a synchronous create callback; invalid options stop linting.
|
|
237
|
+
const options = Schema.decodeUnknownSync(RuntimeOptions)(Option.getOrElse(Array.head(context.options), () => ({ allowedFiles: [] })));
|
|
238
|
+
const filename = context.filename.replaceAll("\\", "/");
|
|
239
|
+
const cwd = context.cwd.replaceAll("\\", "/").replace(/\/$/u, "");
|
|
240
|
+
const relative = filename.startsWith(`${cwd}/`) ? filename.slice(cwd.length + 1) : filename;
|
|
241
|
+
const isMain = Array.contains(options.allowedFiles, relative);
|
|
242
|
+
return {
|
|
243
|
+
CallExpression(node) {
|
|
244
|
+
if (node.callee.type !== "MemberExpression" || node.callee.object.type !== "Identifier")
|
|
245
|
+
return;
|
|
246
|
+
const object = node.callee.object.name;
|
|
247
|
+
const method = propertyName(node.callee.property);
|
|
248
|
+
const isRunner = Option.exists(method, (name) => (object === "Effect" && name.startsWith("run")) ||
|
|
249
|
+
((object === "BunRuntime" || object === "NodeRuntime") && name === "runMain"));
|
|
250
|
+
if (isRunner && !isMain)
|
|
251
|
+
context.report({ messageId: "noRuntimeExecutionOutsideMain", node: node.callee });
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
const instrumentationName = (node) => {
|
|
257
|
+
if (node.type !== "CallExpression" ||
|
|
258
|
+
node.callee.type !== "MemberExpression" ||
|
|
259
|
+
node.callee.object.type !== "Identifier" ||
|
|
260
|
+
node.callee.object.name !== "Effect" ||
|
|
261
|
+
!Option.exists(propertyName(node.callee.property), (name) => Array.contains(["fn", "withSpan"], name)))
|
|
262
|
+
return Option.none();
|
|
263
|
+
return Array.head(node.arguments).pipe(Option.flatMap((argument) => argument.type === "Literal" && Predicate.isString(argument.value)
|
|
264
|
+
? Option.some(argument.value)
|
|
265
|
+
: Option.none()));
|
|
266
|
+
};
|
|
267
|
+
const containsInstrumentation = (context, node, name) => Option.contains(instrumentationName(node), name) ||
|
|
268
|
+
Array.some(childNodes(context, node), (child) => containsInstrumentation(context, child, name));
|
|
269
|
+
const operationValue = (context, node) => {
|
|
270
|
+
if (node.type !== "Identifier")
|
|
271
|
+
return Option.some(node);
|
|
272
|
+
return variableInScope(context.sourceCode.getScope(node), node.name).pipe(Option.flatMap((variable) => Array.head(variable.defs)), Option.flatMap((definition) => definition.node.type === "VariableDeclarator"
|
|
273
|
+
? Option.fromNullishOr(definition.node.init)
|
|
274
|
+
: Option.none()));
|
|
275
|
+
};
|
|
276
|
+
const requireServiceOperationInstrumentation = defineRule({
|
|
277
|
+
meta: {
|
|
278
|
+
type: "problem",
|
|
279
|
+
schema: [
|
|
280
|
+
{
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: { serviceNames: { type: "array", items: { type: "string", minLength: 1 } } },
|
|
283
|
+
required: ["serviceNames"],
|
|
284
|
+
additionalProperties: false,
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
docs: {
|
|
288
|
+
description: "Require exact tracing names on configured public Effect service operations.",
|
|
289
|
+
},
|
|
290
|
+
messages: {
|
|
291
|
+
missingInstrumentation: "Instrument {{operation}} with Effect.fn or Effect.withSpan using that exact name.",
|
|
292
|
+
unsupportedOperation: "Declare public Effect service operations as named object properties so tracing can be checked.",
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
create(context) {
|
|
296
|
+
// Oxlint requires a synchronous create callback; invalid options stop linting.
|
|
297
|
+
const options = Schema.decodeUnknownSync(ServiceOptions)(Option.getOrUndefined(Array.head(context.options)));
|
|
298
|
+
return {
|
|
299
|
+
CallExpression(node) {
|
|
300
|
+
if (node.callee.type !== "MemberExpression" ||
|
|
301
|
+
node.callee.object.type !== "Identifier" ||
|
|
302
|
+
!Array.contains(options.serviceNames, node.callee.object.name) ||
|
|
303
|
+
!Option.contains(propertyName(node.callee.property), "of"))
|
|
304
|
+
return;
|
|
305
|
+
const service = node.callee.object.name;
|
|
306
|
+
const operations = Array.head(node.arguments);
|
|
307
|
+
Option.match(operations, {
|
|
308
|
+
onNone: () => { },
|
|
309
|
+
onSome: (argument) => {
|
|
310
|
+
if (argument.type !== "ObjectExpression")
|
|
311
|
+
return;
|
|
312
|
+
Array.forEach(argument.properties, (operation) => {
|
|
313
|
+
if (operation.type !== "Property") {
|
|
314
|
+
context.report({ messageId: "unsupportedOperation", node: operation });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
Option.match(propertyName(operation.key), {
|
|
318
|
+
onNone: () => {
|
|
319
|
+
context.report({ messageId: "unsupportedOperation", node: operation });
|
|
320
|
+
},
|
|
321
|
+
onSome: (name) => {
|
|
322
|
+
const fullName = `${service}.${name}`;
|
|
323
|
+
if (!Option.exists(operationValue(context, operation.value), (value) => containsInstrumentation(context, value, fullName))) {
|
|
324
|
+
context.report({
|
|
325
|
+
messageId: "missingInstrumentation",
|
|
326
|
+
data: { operation: fullName },
|
|
327
|
+
node: operation,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
export default {
|
|
340
|
+
meta: { name: "effect" },
|
|
341
|
+
rules: {
|
|
342
|
+
"no-ambient-process": noAmbientProcess,
|
|
343
|
+
"no-async-await-promise": noAsyncAwaitPromise,
|
|
344
|
+
"no-console": noConsole,
|
|
345
|
+
"no-instanceof": noInstanceof,
|
|
346
|
+
"no-manual-tag": noManualTag,
|
|
347
|
+
"no-raw-fetch": noRawFetch,
|
|
348
|
+
"no-runtime-execution-outside-main": noRuntimeExecutionOutsideMain,
|
|
349
|
+
"no-unchecked-json": noUncheckedJson,
|
|
350
|
+
"require-service-operation-instrumentation": requireServiceOperationInstrumentation,
|
|
351
|
+
},
|
|
352
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** General code checks, with no project-specific file names or settings. */
|
|
2
|
+
export declare const general: {
|
|
3
|
+
jsPlugins: string[];
|
|
4
|
+
rules: {
|
|
5
|
+
"anti-slop/no-chained-type-assertions": "error";
|
|
6
|
+
"anti-slop/no-conditional-empty-object-spread": "error";
|
|
7
|
+
"anti-slop/no-known-value-widening": "error";
|
|
8
|
+
"anti-slop/no-module-mocking": "error";
|
|
9
|
+
"anti-slop/no-object-parameters": "error";
|
|
10
|
+
"anti-slop/no-reflect-apply": "error";
|
|
11
|
+
"anti-slop/no-reflect-get": "error";
|
|
12
|
+
"anti-slop/no-runtime-typeof": "error";
|
|
13
|
+
"anti-slop/no-shape-in-symbol-names": "error";
|
|
14
|
+
"anti-slop/no-unknown-parameters": "error";
|
|
15
|
+
"anti-slop/no-unknown-returns": "error";
|
|
16
|
+
"anti-slop/no-unknown-type-aliases": "error";
|
|
17
|
+
"anti-slop/no-unsafe-dictionary-type": "error";
|
|
18
|
+
"anti-slop/no-widen-then-assert": "error";
|
|
19
|
+
"anti-slop/require-safety-comment-for-type-assertion": "error";
|
|
20
|
+
"package/no-cross-package-source-imports": "error";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** Effect code checks. Add the general group separately if wanted. */
|
|
24
|
+
export declare const effect: {
|
|
25
|
+
jsPlugins: string[];
|
|
26
|
+
rules: {
|
|
27
|
+
"effect/no-ambient-process": "error";
|
|
28
|
+
"effect/no-async-await-promise": "error";
|
|
29
|
+
"effect/no-console": "error";
|
|
30
|
+
"effect/no-instanceof": "error";
|
|
31
|
+
"effect/no-manual-tag": "error";
|
|
32
|
+
"effect/no-raw-fetch": "error";
|
|
33
|
+
"strict-effect/static-layers": "error";
|
|
34
|
+
"strict-effect/no-unchecked-index": "error";
|
|
35
|
+
"strict-effect/no-native-at": "error";
|
|
36
|
+
"strict-effect/tagged-error-name": "error";
|
|
37
|
+
"strict-effect/error-constructor-new": "error";
|
|
38
|
+
"strict-effect/no-promise-workflow": "error";
|
|
39
|
+
"strict-effect/no-unsafe-option-unwrap": "error";
|
|
40
|
+
"strict-effect/no-unchecked-json": "error";
|
|
41
|
+
"strict-effect/no-native-work": "error";
|
|
42
|
+
"strict-effect/no-imperative-collections": "error";
|
|
43
|
+
"strict-effect/no-native-client-replica": "error";
|
|
44
|
+
"bun/no-ambient-host-api": "error";
|
|
45
|
+
"anti-slop-effect/no-service-constructor-imports": "error";
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/** Native Alchemy configuration and secret-source checks. */
|
|
49
|
+
export declare const alchemy: {
|
|
50
|
+
jsPlugins: string[];
|
|
51
|
+
rules: {
|
|
52
|
+
"alchemy-secrets/native-sources": "error";
|
|
53
|
+
"alchemy-secrets/explicit-stack-sources": "error";
|
|
54
|
+
"alchemy-secrets/no-doppler-wrapper": "error";
|
|
55
|
+
"strict-effect/native-cloudflare-cache": "error";
|
|
56
|
+
};
|
|
57
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** General code checks, with no project-specific file names or settings. */
|
|
2
|
+
export const general = {
|
|
3
|
+
jsPlugins: ["@commonplaces/oxlint-plugin/anti-slop", "@commonplaces/oxlint-plugin/package"],
|
|
4
|
+
rules: {
|
|
5
|
+
"anti-slop/no-chained-type-assertions": "error",
|
|
6
|
+
"anti-slop/no-conditional-empty-object-spread": "error",
|
|
7
|
+
"anti-slop/no-known-value-widening": "error",
|
|
8
|
+
"anti-slop/no-module-mocking": "error",
|
|
9
|
+
"anti-slop/no-object-parameters": "error",
|
|
10
|
+
"anti-slop/no-reflect-apply": "error",
|
|
11
|
+
"anti-slop/no-reflect-get": "error",
|
|
12
|
+
"anti-slop/no-runtime-typeof": "error",
|
|
13
|
+
"anti-slop/no-shape-in-symbol-names": "error",
|
|
14
|
+
"anti-slop/no-unknown-parameters": "error",
|
|
15
|
+
"anti-slop/no-unknown-returns": "error",
|
|
16
|
+
"anti-slop/no-unknown-type-aliases": "error",
|
|
17
|
+
"anti-slop/no-unsafe-dictionary-type": "error",
|
|
18
|
+
"anti-slop/no-widen-then-assert": "error",
|
|
19
|
+
"anti-slop/require-safety-comment-for-type-assertion": "error",
|
|
20
|
+
"package/no-cross-package-source-imports": "error",
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
/** Effect code checks. Add the general group separately if wanted. */
|
|
24
|
+
export const effect = {
|
|
25
|
+
jsPlugins: [
|
|
26
|
+
"@commonplaces/oxlint-plugin/effect",
|
|
27
|
+
"@commonplaces/oxlint-plugin/strict-effect",
|
|
28
|
+
"@commonplaces/oxlint-plugin/bun",
|
|
29
|
+
"@commonplaces/oxlint-plugin/anti-slop-effect",
|
|
30
|
+
],
|
|
31
|
+
rules: {
|
|
32
|
+
"effect/no-ambient-process": "error",
|
|
33
|
+
"effect/no-async-await-promise": "error",
|
|
34
|
+
"effect/no-console": "error",
|
|
35
|
+
"effect/no-instanceof": "error",
|
|
36
|
+
"effect/no-manual-tag": "error",
|
|
37
|
+
"effect/no-raw-fetch": "error",
|
|
38
|
+
"strict-effect/static-layers": "error",
|
|
39
|
+
"strict-effect/no-unchecked-index": "error",
|
|
40
|
+
"strict-effect/no-native-at": "error",
|
|
41
|
+
"strict-effect/tagged-error-name": "error",
|
|
42
|
+
"strict-effect/error-constructor-new": "error",
|
|
43
|
+
"strict-effect/no-promise-workflow": "error",
|
|
44
|
+
"strict-effect/no-unsafe-option-unwrap": "error",
|
|
45
|
+
"strict-effect/no-unchecked-json": "error",
|
|
46
|
+
"strict-effect/no-native-work": "error",
|
|
47
|
+
"strict-effect/no-imperative-collections": "error",
|
|
48
|
+
"strict-effect/no-native-client-replica": "error",
|
|
49
|
+
"bun/no-ambient-host-api": "error",
|
|
50
|
+
"anti-slop-effect/no-service-constructor-imports": "error",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/** Native Alchemy configuration and secret-source checks. */
|
|
54
|
+
export const alchemy = {
|
|
55
|
+
jsPlugins: ["@commonplaces/oxlint-plugin/alchemy", "@commonplaces/oxlint-plugin/strict-effect"],
|
|
56
|
+
rules: {
|
|
57
|
+
"alchemy-secrets/native-sources": "error",
|
|
58
|
+
"alchemy-secrets/explicit-stack-sources": "error",
|
|
59
|
+
"alchemy-secrets/no-doppler-wrapper": "error",
|
|
60
|
+
"strict-effect/native-cloudflare-cache": "error",
|
|
61
|
+
},
|
|
62
|
+
};
|
package/dist/package.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { Array, Option, Predicate } from "effect";
|
|
3
|
+
const resolveRelative = (fileName, specifier) => Array.join(Array.reduce(specifier.split("/"), Array.dropRight(fileName.split("/"), 1), (segments, segment) => segment === ".."
|
|
4
|
+
? Array.dropRight(segments, 1)
|
|
5
|
+
: segment === "." || segment.length === 0
|
|
6
|
+
? segments
|
|
7
|
+
: [...segments, segment]), "/");
|
|
8
|
+
const workspaceSourceRoot = (fileName) => Option.fromNullishOr(fileName.match(/\/(?:apps|packages)\/(?:[^/]+\/)+?src(?:\/|$)/u)).pipe(Option.flatMap(Array.head), Option.map((root) => root.replace(/\/$/u, "")));
|
|
9
|
+
const isPrivatePackageAlias = (specifier) => /^@[^/]+\/[^/]+\/src(?:\/|$)/u.test(specifier);
|
|
10
|
+
const isCrossWorkspaceSourcePath = (fileName, specifier) => {
|
|
11
|
+
if (!specifier.startsWith(".")) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const sourceRoot = workspaceSourceRoot(fileName);
|
|
15
|
+
const target = resolveRelative(fileName, specifier);
|
|
16
|
+
const targetRoot = workspaceSourceRoot(target);
|
|
17
|
+
return Option.exists(Option.all([sourceRoot, targetRoot]), ([source, target]) => source !== target);
|
|
18
|
+
};
|
|
19
|
+
const noCrossPackageSourceImports = defineRule({
|
|
20
|
+
create(context) {
|
|
21
|
+
const fileName = context.filename.replaceAll("\\", "/");
|
|
22
|
+
const inspect = (node) => {
|
|
23
|
+
const source = node.source;
|
|
24
|
+
if (source?.type === "Literal" &&
|
|
25
|
+
Predicate.isString(source.value) &&
|
|
26
|
+
(isPrivatePackageAlias(source.value) || isCrossWorkspaceSourcePath(fileName, source.value))) {
|
|
27
|
+
context.report({ messageId: "noCrossPackageSourceImports", node: source });
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
ExportAllDeclaration: inspect,
|
|
32
|
+
ExportNamedDeclaration: inspect,
|
|
33
|
+
ImportDeclaration: inspect,
|
|
34
|
+
ImportExpression: inspect,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
meta: {
|
|
38
|
+
docs: { description: "Prevent imports from another workspace's private source tree." },
|
|
39
|
+
messages: {
|
|
40
|
+
noCrossPackageSourceImports: "Import the package's public export instead of reaching into another workspace's source folder.",
|
|
41
|
+
},
|
|
42
|
+
type: "problem",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
export default {
|
|
46
|
+
meta: { name: "package" },
|
|
47
|
+
rules: {
|
|
48
|
+
"no-cross-package-source-imports": noCrossPackageSourceImports,
|
|
49
|
+
},
|
|
50
|
+
};
|