oxlint-plugin-effect 0.6.0 → 0.8.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 +22 -0
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/presets/recommended.d.ts +12 -0
- package/dist/presets/recommended.js +13 -1
- package/dist/rules/_anti-slop-dictionary-types.d.ts +24 -0
- package/dist/rules/_anti-slop-dictionary-types.js +248 -0
- package/dist/rules/index.d.ts +14 -2
- package/dist/rules/index.js +26 -2
- package/dist/rules/no-chained-type-assertions.d.ts +7 -0
- package/dist/rules/no-chained-type-assertions.js +60 -0
- package/dist/rules/no-conditional-empty-object-spread.d.ts +7 -0
- package/dist/rules/no-conditional-empty-object-spread.js +79 -0
- package/dist/rules/no-known-value-widening.d.ts +7 -0
- package/dist/rules/no-known-value-widening.js +144 -0
- package/dist/rules/no-nullish.d.ts +6 -0
- package/dist/rules/no-nullish.js +50 -0
- package/dist/rules/no-object-parameters.d.ts +7 -0
- package/dist/rules/no-object-parameters.js +70 -0
- package/dist/rules/no-runtime-typeof.d.ts +7 -0
- package/dist/rules/no-runtime-typeof.js +24 -0
- package/dist/rules/no-shape-in-symbol-names.d.ts +7 -0
- package/dist/rules/no-shape-in-symbol-names.js +34 -0
- package/dist/rules/no-unknown-parameters.d.ts +7 -0
- package/dist/rules/no-unknown-parameters.js +55 -0
- package/dist/rules/no-unknown-type-aliases.d.ts +7 -0
- package/dist/rules/no-unknown-type-aliases.js +48 -0
- package/dist/rules/no-unsafe-dictionary-type.d.ts +7 -0
- package/dist/rules/no-unsafe-dictionary-type.js +75 -0
- package/dist/rules/no-widen-then-assert.d.ts +7 -0
- package/dist/rules/no-widen-then-assert.js +199 -0
- package/dist/rules/prefer-effect-fn.d.ts +6 -0
- package/dist/rules/prefer-effect-fn.js +31 -0
- package/package.json +3 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-known-value-widening.d.ts
|
|
4
|
+
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
|
|
5
|
+
declare const noKnownValueWidening: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noKnownValueWidening };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { classifyWideningTarget, createTypeEnvironment, isKnownEvidenceExpression } from "./_anti-slop-dictionary-types.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/no-known-value-widening.ts
|
|
8
|
+
/**
|
|
9
|
+
* Ported from dmmulroy/anti-slop at
|
|
10
|
+
* b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0.
|
|
11
|
+
*/
|
|
12
|
+
function unwrapExpression(expression) {
|
|
13
|
+
let current = expression;
|
|
14
|
+
while (current.type === "ParenthesizedExpression" || current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression") current = current.expression;
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
function resolveVariable(sourceCode, identifier) {
|
|
18
|
+
let scope = sourceCode.getScope(identifier);
|
|
19
|
+
while (scope !== null) {
|
|
20
|
+
const variable = scope.set.get(identifier.name);
|
|
21
|
+
if (variable !== void 0) return variable;
|
|
22
|
+
scope = scope.upper;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function variableDeclarator(variable) {
|
|
27
|
+
if (variable.defs.length !== 1) return null;
|
|
28
|
+
const [definition] = variable.defs;
|
|
29
|
+
return definition?.type === "Variable" && definition.node.type === "VariableDeclarator" ? definition.node : null;
|
|
30
|
+
}
|
|
31
|
+
function isStableConstVariable(variable, declarator) {
|
|
32
|
+
return declarator.parent.type === "VariableDeclaration" && declarator.parent.kind === "const" && variable.references.every((reference) => reference.init || !reference.isWrite());
|
|
33
|
+
}
|
|
34
|
+
function hasKnownEvidence(sourceCode, expression, visitedVariables = /* @__PURE__ */ new Set()) {
|
|
35
|
+
if (isKnownEvidenceExpression(expression)) return true;
|
|
36
|
+
const unwrapped = unwrapExpression(expression);
|
|
37
|
+
if (unwrapped.type !== "Identifier") return false;
|
|
38
|
+
const variable = resolveVariable(sourceCode, unwrapped);
|
|
39
|
+
if (variable === null || visitedVariables.has(variable)) return false;
|
|
40
|
+
const declarator = variableDeclarator(variable);
|
|
41
|
+
if (declarator === null || declarator.init === null || !isStableConstVariable(variable, declarator)) return false;
|
|
42
|
+
visitedVariables.add(variable);
|
|
43
|
+
return hasKnownEvidence(sourceCode, declarator.init, visitedVariables);
|
|
44
|
+
}
|
|
45
|
+
function annotationTarget(annotation, environment) {
|
|
46
|
+
return annotation === null || annotation === void 0 ? null : classifyWideningTarget(annotation.typeAnnotation, environment);
|
|
47
|
+
}
|
|
48
|
+
function enclosingFunction(node) {
|
|
49
|
+
let current = node.parent;
|
|
50
|
+
while (current !== null && current.type !== "Program") {
|
|
51
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionDeclaration" || current.type === "FunctionExpression") return current;
|
|
52
|
+
current = current.parent;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function sourceKeyName(sourceCode, key) {
|
|
57
|
+
if (key.type === "Identifier" || key.type === "PrivateIdentifier") return key.name;
|
|
58
|
+
if (key.type === "Literal") return String(key.value);
|
|
59
|
+
return sourceCode.getText(key);
|
|
60
|
+
}
|
|
61
|
+
function functionName(sourceCode, owner) {
|
|
62
|
+
if (owner === null) return "anonymous function";
|
|
63
|
+
if (owner.id !== null) return owner.id.name;
|
|
64
|
+
const parent = owner.parent;
|
|
65
|
+
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") return parent.id.name;
|
|
66
|
+
if (parent.type === "MethodDefinition") return sourceKeyName(sourceCode, parent.key);
|
|
67
|
+
return "anonymous function";
|
|
68
|
+
}
|
|
69
|
+
function isEmptyObjectExpression(expression) {
|
|
70
|
+
const unwrapped = unwrapExpression(expression);
|
|
71
|
+
return unwrapped.type === "ObjectExpression" && unwrapped.properties.length === 0;
|
|
72
|
+
}
|
|
73
|
+
function isDictionaryAccumulatorTarget(destination) {
|
|
74
|
+
return destination.kind === "open dictionary" || destination.kind === "generic container";
|
|
75
|
+
}
|
|
76
|
+
function hasParentAssertion(node) {
|
|
77
|
+
return node.parent?.type === "TSAsExpression" || node.parent?.type === "TSTypeAssertion";
|
|
78
|
+
}
|
|
79
|
+
/** Detect sound syntactic cases where a known value is explicitly widened and loses evidence. */
|
|
80
|
+
const noKnownValueWidening = define({
|
|
81
|
+
name: "no-known-value-widening",
|
|
82
|
+
meta: meta({
|
|
83
|
+
type: "problem",
|
|
84
|
+
description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
|
|
85
|
+
messages: { widening: "The known initializer supplying {{subject}} carries established type evidence, but the explicit {{target}} target type discards it. Preserve inference, use `satisfies`, or introduce/use a named owner contract; parse genuinely external data once at its boundary." }
|
|
86
|
+
}),
|
|
87
|
+
create: function* () {
|
|
88
|
+
const context = yield* RuleContext;
|
|
89
|
+
let environment = null;
|
|
90
|
+
const reportFlow = (expression, destination, subject, options = {}) => {
|
|
91
|
+
if (destination === null) return Effect.void;
|
|
92
|
+
if (options.allowEmptyDictionaryAccumulator === true && isDictionaryAccumulatorTarget(destination) && isEmptyObjectExpression(expression)) return Effect.void;
|
|
93
|
+
if (!hasKnownEvidence(context.sourceCode, expression)) return Effect.void;
|
|
94
|
+
return context.report(fromId({
|
|
95
|
+
node: expression,
|
|
96
|
+
messageId: "widening",
|
|
97
|
+
data: {
|
|
98
|
+
subject,
|
|
99
|
+
target: destination.kind
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
};
|
|
103
|
+
const targetFromAnnotation = (annotation) => environment === null ? null : annotationTarget(annotation, environment);
|
|
104
|
+
return {
|
|
105
|
+
Program: (node) => {
|
|
106
|
+
environment = createTypeEnvironment(node);
|
|
107
|
+
return Effect.void;
|
|
108
|
+
},
|
|
109
|
+
VariableDeclarator: (node) => {
|
|
110
|
+
if (node.init === null || node.id.type !== "Identifier") return Effect.void;
|
|
111
|
+
return reportFlow(node.init, targetFromAnnotation(node.id.typeAnnotation), `binding \`${node.id.name}\``, { allowEmptyDictionaryAccumulator: true });
|
|
112
|
+
},
|
|
113
|
+
PropertyDefinition: (node) => {
|
|
114
|
+
if (node.value === null) return Effect.void;
|
|
115
|
+
return reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
116
|
+
},
|
|
117
|
+
AccessorProperty: (node) => {
|
|
118
|
+
if (node.value === null) return Effect.void;
|
|
119
|
+
return reportFlow(node.value, targetFromAnnotation(node.typeAnnotation), `property \`${sourceKeyName(context.sourceCode, node.key)}\``);
|
|
120
|
+
},
|
|
121
|
+
AssignmentExpression: (node) => {
|
|
122
|
+
if (node.operator !== "=" || node.left.type !== "Identifier") return Effect.void;
|
|
123
|
+
const variable = resolveVariable(context.sourceCode, node.left);
|
|
124
|
+
if (variable === null) return Effect.void;
|
|
125
|
+
const declarator = variableDeclarator(variable);
|
|
126
|
+
if (declarator === null || declarator.id.type !== "Identifier") return Effect.void;
|
|
127
|
+
return reportFlow(node.right, targetFromAnnotation(declarator.id.typeAnnotation), `binding \`${declarator.id.name}\``);
|
|
128
|
+
},
|
|
129
|
+
ReturnStatement: (node) => {
|
|
130
|
+
if (node.argument === null) return Effect.void;
|
|
131
|
+
const owner = enclosingFunction(node);
|
|
132
|
+
return reportFlow(node.argument, targetFromAnnotation(owner?.returnType), `return value of \`${functionName(context.sourceCode, owner)}\``);
|
|
133
|
+
},
|
|
134
|
+
ArrowFunctionExpression: (node) => {
|
|
135
|
+
if (node.body.type === "BlockStatement") return Effect.void;
|
|
136
|
+
return reportFlow(node.body, targetFromAnnotation(node.returnType), `return value of \`${functionName(context.sourceCode, node)}\``);
|
|
137
|
+
},
|
|
138
|
+
TSAsExpression: (node) => environment === null || hasParentAssertion(node) ? Effect.void : reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion"),
|
|
139
|
+
TSTypeAssertion: (node) => environment === null || hasParentAssertion(node) ? Effect.void : reportFlow(node.expression, classifyWideningTarget(node.typeAnnotation, environment), "assertion")
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
//#endregion
|
|
144
|
+
export { noKnownValueWidening };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { narrow } from "../vendor/effect-oxlint/AST.js";
|
|
2
|
+
import { make } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
3
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
4
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
5
|
+
import { isGlobalReference } from "../vendor/effect-oxlint/SourceCode.js";
|
|
6
|
+
import "../vendor/effect-oxlint/index.js";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
//#region src/rules/no-nullish.ts
|
|
10
|
+
const message = "Avoid null and undefined. Use Option for presence or absence. Use a domain enum when the state has more than two cases.";
|
|
11
|
+
const noNullish = define({
|
|
12
|
+
name: "no-nullish",
|
|
13
|
+
meta: meta({
|
|
14
|
+
type: "problem",
|
|
15
|
+
description: "Use Option or a domain enum instead of null and undefined."
|
|
16
|
+
}),
|
|
17
|
+
create: function* () {
|
|
18
|
+
const ctx = yield* RuleContext;
|
|
19
|
+
const report = (node) => ctx.report(make({
|
|
20
|
+
node,
|
|
21
|
+
message
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
Identifier: (node) => Option.match(narrow(node, "Identifier"), {
|
|
25
|
+
onNone: () => Effect.void,
|
|
26
|
+
onSome: (identifier) => {
|
|
27
|
+
if (identifier.name !== "undefined") return Effect.void;
|
|
28
|
+
return isGlobalReference(identifier).pipe(Effect.flatMap((isGlobal) => {
|
|
29
|
+
if (!isGlobal) return Effect.void;
|
|
30
|
+
return report(identifier);
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
Literal: (node) => Option.match(narrow(node, "Literal"), {
|
|
35
|
+
onNone: () => Effect.void,
|
|
36
|
+
onSome: (literal) => literal.value === null && !("regex" in literal) ? report(literal) : Effect.void
|
|
37
|
+
}),
|
|
38
|
+
TSNullKeyword: (node) => Option.match(narrow(node, "TSNullKeyword"), {
|
|
39
|
+
onNone: () => Effect.void,
|
|
40
|
+
onSome: report
|
|
41
|
+
}),
|
|
42
|
+
TSUndefinedKeyword: (node) => Option.match(narrow(node, "TSUndefinedKeyword"), {
|
|
43
|
+
onNone: () => Effect.void,
|
|
44
|
+
onSome: report
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
//#endregion
|
|
50
|
+
export { noNullish };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-object-parameters.d.ts
|
|
4
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
5
|
+
declare const noObjectParameters: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noObjectParameters };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-object-parameters.ts
|
|
7
|
+
function parameterAnnotation(parameter) {
|
|
8
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
|
|
9
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
10
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
11
|
+
return parameter.typeAnnotation;
|
|
12
|
+
}
|
|
13
|
+
function parameterName(parameter, sourceCode) {
|
|
14
|
+
return parameter.type === "Identifier" ? parameter.name : sourceCode.getText(parameter).replace(/\s*:\s*object\s*$/u, "");
|
|
15
|
+
}
|
|
16
|
+
/** Ban the broad object type on function inputs, including local aliases to object. */
|
|
17
|
+
const noObjectParameters = define({
|
|
18
|
+
name: "no-object-parameters",
|
|
19
|
+
meta: meta({
|
|
20
|
+
type: "problem",
|
|
21
|
+
description: "Disallow object function parameters; inputs must use an owner-provided type and be parsed at their boundary.",
|
|
22
|
+
messages: { objectParameter: "Parameter `{{parameter}}` accepts the broad `object` type. Use the expected owner type or decode the external input at its boundary." }
|
|
23
|
+
}),
|
|
24
|
+
create: function* () {
|
|
25
|
+
const context = yield* RuleContext;
|
|
26
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
27
|
+
const resolvesToObject = (type, visited = /* @__PURE__ */ new Set()) => {
|
|
28
|
+
if (type.type === "TSObjectKeyword") return true;
|
|
29
|
+
if (type.type === "TSParenthesizedType") return resolvesToObject(type.typeAnnotation, visited);
|
|
30
|
+
if (type.type === "TSUnionType") return type.types.some((member) => resolvesToObject(member, visited));
|
|
31
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier" || type.typeArguments !== null && type.typeArguments !== void 0 && type.typeArguments.params.length > 0 || visited.has(type.typeName.name)) return false;
|
|
32
|
+
const alias = aliases.get(type.typeName.name);
|
|
33
|
+
if (alias === void 0) return false;
|
|
34
|
+
const nextVisited = new Set(visited);
|
|
35
|
+
nextVisited.add(type.typeName.name);
|
|
36
|
+
return resolvesToObject(alias, nextVisited);
|
|
37
|
+
};
|
|
38
|
+
const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
|
|
39
|
+
const annotation = parameterAnnotation(parameter);
|
|
40
|
+
if (annotation === null || annotation === void 0) return Effect.void;
|
|
41
|
+
if (!resolvesToObject(annotation.typeAnnotation)) return Effect.void;
|
|
42
|
+
return context.report(fromId({
|
|
43
|
+
node: annotation.typeAnnotation,
|
|
44
|
+
messageId: "objectParameter",
|
|
45
|
+
data: { parameter: parameterName(parameter, context.sourceCode) }
|
|
46
|
+
}));
|
|
47
|
+
}, { discard: true });
|
|
48
|
+
return {
|
|
49
|
+
Program: (node) => {
|
|
50
|
+
for (const statement of node.body) {
|
|
51
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
52
|
+
if (declaration?.type === "TSTypeAliasDeclaration" && (declaration.typeParameters === null || declaration.typeParameters === void 0)) aliases.set(declaration.id.name, declaration.typeAnnotation);
|
|
53
|
+
}
|
|
54
|
+
return Effect.void;
|
|
55
|
+
},
|
|
56
|
+
ArrowFunctionExpression: checkParameters,
|
|
57
|
+
FunctionDeclaration: checkParameters,
|
|
58
|
+
FunctionExpression: checkParameters,
|
|
59
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
60
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
61
|
+
TSConstructorType: checkParameters,
|
|
62
|
+
TSDeclareFunction: checkParameters,
|
|
63
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
64
|
+
TSFunctionType: checkParameters,
|
|
65
|
+
TSMethodSignature: checkParameters
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
//#endregion
|
|
70
|
+
export { noObjectParameters };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-runtime-typeof.d.ts
|
|
4
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
5
|
+
declare const noRuntimeTypeof: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noRuntimeTypeof };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-runtime-typeof.ts
|
|
7
|
+
/** Disallow runtime typeof checks that narrow unparsed values instead of decoding them. */
|
|
8
|
+
const noRuntimeTypeof = define({
|
|
9
|
+
name: "no-runtime-typeof",
|
|
10
|
+
meta: meta({
|
|
11
|
+
type: "problem",
|
|
12
|
+
description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
|
|
13
|
+
messages: { runtimeTypeof: "A runtime `typeof` check only narrows an unparsed representation; it does not establish the expected contract. Parse the value into a strongly typed domain type at the earliest possible point, as close as possible to the I/O boundary where the data originated." }
|
|
14
|
+
}),
|
|
15
|
+
create: function* () {
|
|
16
|
+
const context = yield* RuleContext;
|
|
17
|
+
return { UnaryExpression: (node) => node.operator === "typeof" ? context.report(fromId({
|
|
18
|
+
node,
|
|
19
|
+
messageId: "runtimeTypeof"
|
|
20
|
+
})) : Effect.void };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
//#endregion
|
|
24
|
+
export { noRuntimeTypeof };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-shape-in-symbol-names.d.ts
|
|
4
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
5
|
+
declare const noShapeInSymbolNames: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noShapeInSymbolNames };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-shape-in-symbol-names.ts
|
|
7
|
+
const forbiddenSymbolName = "shape";
|
|
8
|
+
function containsForbiddenSymbolName(name) {
|
|
9
|
+
return name.toLowerCase().includes(forbiddenSymbolName);
|
|
10
|
+
}
|
|
11
|
+
/** Ban the case-insensitive substring "shape" in every JavaScript and TypeScript symbol name. */
|
|
12
|
+
const noShapeInSymbolNames = define({
|
|
13
|
+
name: "no-shape-in-symbol-names",
|
|
14
|
+
meta: meta({
|
|
15
|
+
type: "problem",
|
|
16
|
+
description: "Disallow the case-insensitive substring \"shape\" in JavaScript, TypeScript, private, and JSX symbol names.",
|
|
17
|
+
messages: { forbiddenSymbolName: "Do not use the case-insensitive substring \"shape\" in symbol names (found \"{{name}}\")." }
|
|
18
|
+
}),
|
|
19
|
+
create: function* () {
|
|
20
|
+
const context = yield* RuleContext;
|
|
21
|
+
const reportForbiddenSymbolName = (node) => containsForbiddenSymbolName(node.name) ? context.report(fromId({
|
|
22
|
+
node,
|
|
23
|
+
messageId: "forbiddenSymbolName",
|
|
24
|
+
data: { name: node.name }
|
|
25
|
+
})) : Effect.void;
|
|
26
|
+
return {
|
|
27
|
+
Identifier: reportForbiddenSymbolName,
|
|
28
|
+
PrivateIdentifier: reportForbiddenSymbolName,
|
|
29
|
+
JSXIdentifier: reportForbiddenSymbolName
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
export { noShapeInSymbolNames };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-unknown-parameters.d.ts
|
|
4
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
5
|
+
declare const noUnknownParameters: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noUnknownParameters };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-unknown-parameters.ts
|
|
7
|
+
function parameterAnnotation(parameter) {
|
|
8
|
+
if (parameter.type === "TSParameterProperty") return parameterAnnotation(parameter.parameter);
|
|
9
|
+
if (parameter.type === "RestElement") return parameter.typeAnnotation ?? parameterAnnotation(parameter.argument);
|
|
10
|
+
if (parameter.type === "AssignmentPattern") return parameter.typeAnnotation ?? parameter.left.typeAnnotation;
|
|
11
|
+
return parameter.typeAnnotation;
|
|
12
|
+
}
|
|
13
|
+
function parameterName(parameter, sourceText) {
|
|
14
|
+
if (parameter.type === "TSParameterProperty") return parameterName(parameter.parameter, sourceText);
|
|
15
|
+
if (parameter.type === "AssignmentPattern") return parameterName(parameter.left, sourceText);
|
|
16
|
+
if (parameter.type === "RestElement") return parameterName(parameter.argument, sourceText);
|
|
17
|
+
return parameter.type === "Identifier" ? parameter.name : sourceText.replace(/\s*:\s*unknown\s*$/u, "");
|
|
18
|
+
}
|
|
19
|
+
/** Disallow unknown inputs except explicitly named error-cause enrichment. */
|
|
20
|
+
const noUnknownParameters = define({
|
|
21
|
+
name: "no-unknown-parameters",
|
|
22
|
+
meta: meta({
|
|
23
|
+
type: "problem",
|
|
24
|
+
description: "Disallow explicitly unknown function parameters except `cause`; decode unknown input at its I/O boundary instead.",
|
|
25
|
+
messages: { unknownParameter: "Parameter `{{parameter}}` accepts `unknown` without establishing its contract. Define the expected schema or parser so the value becomes a strongly typed domain type at the earliest possible point, as close as possible to the I/O boundary where the data originated." }
|
|
26
|
+
}),
|
|
27
|
+
create: function* () {
|
|
28
|
+
const context = yield* RuleContext;
|
|
29
|
+
const checkParameters = (node) => Effect.forEach(node.params, (parameter) => {
|
|
30
|
+
const annotation = parameterAnnotation(parameter);
|
|
31
|
+
if (annotation?.typeAnnotation.type !== "TSUnknownKeyword") return Effect.void;
|
|
32
|
+
const name = parameterName(parameter, context.sourceCode.getText(parameter));
|
|
33
|
+
if (name === "cause") return Effect.void;
|
|
34
|
+
return context.report(fromId({
|
|
35
|
+
node: annotation.typeAnnotation,
|
|
36
|
+
messageId: "unknownParameter",
|
|
37
|
+
data: { parameter: name }
|
|
38
|
+
}));
|
|
39
|
+
}, { discard: true });
|
|
40
|
+
return {
|
|
41
|
+
ArrowFunctionExpression: checkParameters,
|
|
42
|
+
FunctionDeclaration: checkParameters,
|
|
43
|
+
FunctionExpression: checkParameters,
|
|
44
|
+
TSCallSignatureDeclaration: checkParameters,
|
|
45
|
+
TSConstructSignatureDeclaration: checkParameters,
|
|
46
|
+
TSConstructorType: checkParameters,
|
|
47
|
+
TSDeclareFunction: checkParameters,
|
|
48
|
+
TSEmptyBodyFunctionExpression: checkParameters,
|
|
49
|
+
TSFunctionType: checkParameters,
|
|
50
|
+
TSMethodSignature: checkParameters
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
//#endregion
|
|
55
|
+
export { noUnknownParameters };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-unknown-type-aliases.d.ts
|
|
4
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
5
|
+
declare const noUnknownTypeAliases: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noUnknownTypeAliases };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
//#region src/rules/no-unknown-type-aliases.ts
|
|
7
|
+
function referencedAliasName(type) {
|
|
8
|
+
if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
|
|
9
|
+
if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
|
|
10
|
+
return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
|
|
11
|
+
}
|
|
12
|
+
/** Ban named aliases that merely conceal TypeScript's unknown top type. */
|
|
13
|
+
const noUnknownTypeAliases = define({
|
|
14
|
+
name: "no-unknown-type-aliases",
|
|
15
|
+
meta: meta({
|
|
16
|
+
type: "problem",
|
|
17
|
+
description: "Disallow type aliases whose resolved type is unknown; unknown must remain visible at an allowed boundary.",
|
|
18
|
+
messages: { unknownAlias: "Type alias `{{alias}}` only renames `unknown`. Keep `unknown` explicit on an allowed `cause` field or replace it with the parsed owner type." }
|
|
19
|
+
}),
|
|
20
|
+
create: function* () {
|
|
21
|
+
const context = yield* RuleContext;
|
|
22
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
23
|
+
const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
|
|
24
|
+
if (type.type === "TSUnknownKeyword") return true;
|
|
25
|
+
if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
|
|
26
|
+
const name = referencedAliasName(type);
|
|
27
|
+
if (name === null || visited.has(name)) return false;
|
|
28
|
+
const alias = aliases.get(name);
|
|
29
|
+
if (alias === void 0 || alias.typeParameters !== null && alias.typeParameters !== void 0) return false;
|
|
30
|
+
const nextVisited = new Set(visited);
|
|
31
|
+
nextVisited.add(name);
|
|
32
|
+
return resolvesToUnknown(alias.typeAnnotation, nextVisited);
|
|
33
|
+
};
|
|
34
|
+
return { Program: (node) => {
|
|
35
|
+
for (const statement of node.body) {
|
|
36
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
37
|
+
if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
|
|
38
|
+
}
|
|
39
|
+
return Effect.forEach(aliases.values(), (alias) => resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name])) ? context.report(fromId({
|
|
40
|
+
node: alias.id,
|
|
41
|
+
messageId: "unknownAlias",
|
|
42
|
+
data: { alias: alias.id.name }
|
|
43
|
+
})) : Effect.void, { discard: true });
|
|
44
|
+
} };
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { noUnknownTypeAliases };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-unsafe-dictionary-type.d.ts
|
|
4
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
5
|
+
declare const noUnsafeDictionaryType: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noUnsafeDictionaryType };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
|
|
2
|
+
import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
|
|
3
|
+
import { define, meta } from "../vendor/effect-oxlint/Rule.js";
|
|
4
|
+
import "../vendor/effect-oxlint/index.js";
|
|
5
|
+
import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment } from "./_anti-slop-dictionary-types.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
//#region src/rules/no-unsafe-dictionary-type.ts
|
|
8
|
+
function isTypeNode(node) {
|
|
9
|
+
return node.type.startsWith("TS") && node.type !== "TSTypeAnnotation";
|
|
10
|
+
}
|
|
11
|
+
function typeReferenceName(type) {
|
|
12
|
+
return type.typeName.type === "Identifier" ? type.typeName.name : null;
|
|
13
|
+
}
|
|
14
|
+
function isInsideTypeAliasDeclaration(node) {
|
|
15
|
+
let current = node.parent;
|
|
16
|
+
while (current !== null && current.type !== "Program") {
|
|
17
|
+
if (current.type === "TSTypeAliasDeclaration") return true;
|
|
18
|
+
current = current.parent;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function isPlainAliasConsumerUse(node, environment) {
|
|
23
|
+
if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
|
|
24
|
+
const name = typeReferenceName(node);
|
|
25
|
+
return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
|
|
26
|
+
}
|
|
27
|
+
function shouldReportType(node, environment) {
|
|
28
|
+
if (isPlainAliasConsumerUse(node, environment)) return false;
|
|
29
|
+
if (classifyUnsafeDictionary(node, environment) === null) return false;
|
|
30
|
+
let current = node.parent;
|
|
31
|
+
while (current !== null && current.type !== "Program") {
|
|
32
|
+
if (isTypeNode(current) && classifyUnsafeDictionary(current, environment) !== null) return false;
|
|
33
|
+
current = current.parent;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/** Disallow object-dictionary contracts whose direct value type is an unsafe escape hatch. */
|
|
38
|
+
const noUnsafeDictionaryType = define({
|
|
39
|
+
name: "no-unsafe-dictionary-type",
|
|
40
|
+
meta: meta({
|
|
41
|
+
type: "problem",
|
|
42
|
+
description: "Disallow object-dictionary contracts whose direct value type is unknown, any, object, {}, or a union/alias containing one of those escape hatches.",
|
|
43
|
+
messages: { unsafeDictionary: "This object dictionary's direct value type is an unsafe {{value}} escape hatch. Replace it with a concrete owner/schema-derived value type and parse external data at its boundary." }
|
|
44
|
+
}),
|
|
45
|
+
create: function* () {
|
|
46
|
+
const context = yield* RuleContext;
|
|
47
|
+
let environment = null;
|
|
48
|
+
const report = (node, value) => context.report(fromId({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "unsafeDictionary",
|
|
51
|
+
data: { value }
|
|
52
|
+
}));
|
|
53
|
+
const reportIfUnsafe = (node) => {
|
|
54
|
+
if (environment === null || !shouldReportType(node, environment)) return Effect.void;
|
|
55
|
+
const unsafe = classifyUnsafeDictionary(node, environment);
|
|
56
|
+
return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
Program: (node) => {
|
|
60
|
+
environment = createTypeEnvironment(node);
|
|
61
|
+
return Effect.void;
|
|
62
|
+
},
|
|
63
|
+
TSTypeReference: reportIfUnsafe,
|
|
64
|
+
TSTypeLiteral: reportIfUnsafe,
|
|
65
|
+
TSMappedType: reportIfUnsafe,
|
|
66
|
+
TSIndexSignature: (node) => {
|
|
67
|
+
if (environment === null || node.typeAnnotation === null || node.parent?.type === "TSTypeLiteral") return Effect.void;
|
|
68
|
+
const unsafe = classifyUnsafeDictionaryValue(node.typeAnnotation.typeAnnotation, environment);
|
|
69
|
+
return unsafe === null ? Effect.void : report(node, unsafe.unsafeValue);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
export { noUnsafeDictionaryType };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as _$_oxlint_plugins0 from "@oxlint/plugins";
|
|
2
|
+
|
|
3
|
+
//#region src/rules/no-widen-then-assert.d.ts
|
|
4
|
+
/** Detect immutable local bindings that erase a known type and are later asserted back to a narrower type. */
|
|
5
|
+
declare const noWidenThenAssert: _$_oxlint_plugins0.CreateRule;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { noWidenThenAssert };
|