@jameswyse/oxc-config 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 +21 -0
- package/README.md +213 -0
- package/REFERENCE.md +134 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/lint.d.ts +33 -0
- package/dist/lint.js +493 -0
- package/dist/rules/companion-has-owner.d.ts +1 -0
- package/dist/rules/companion-has-owner.js +74 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.d.ts +1 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.js +127 -0
- package/dist/rules/electron-no-remote-module.d.ts +1 -0
- package/dist/rules/electron-no-remote-module.js +112 -0
- package/dist/rules/electron-secure-web-preferences.d.ts +1 -0
- package/dist/rules/electron-secure-web-preferences.js +108 -0
- package/dist/rules/explicit-api-states.d.ts +1 -0
- package/dist/rules/explicit-api-states.js +143 -0
- package/dist/rules/import-boundaries.d.ts +1 -0
- package/dist/rules/import-boundaries.js +99 -0
- package/dist/rules/no-ambient-clock.d.ts +1 -0
- package/dist/rules/no-ambient-clock.js +35 -0
- package/dist/rules/no-ambient-random.d.ts +1 -0
- package/dist/rules/no-ambient-random.js +71 -0
- package/dist/rules/no-comments.d.ts +1 -0
- package/dist/rules/no-comments.js +64 -0
- package/dist/rules/no-deep-ternary.d.ts +1 -0
- package/dist/rules/no-deep-ternary.js +32 -0
- package/dist/rules/no-edit-proof-assertions.d.ts +1 -0
- package/dist/rules/no-edit-proof-assertions.js +120 -0
- package/dist/rules/no-env-access.d.ts +1 -0
- package/dist/rules/no-env-access.js +35 -0
- package/dist/rules/no-fixed-test-delay.d.ts +1 -0
- package/dist/rules/no-fixed-test-delay.js +130 -0
- package/dist/rules/no-forwarding-module.d.ts +1 -0
- package/dist/rules/no-forwarding-module.js +158 -0
- package/dist/rules/no-generic-file-suffix.d.ts +1 -0
- package/dist/rules/no-generic-file-suffix.js +74 -0
- package/dist/rules/no-pass-through-function.d.ts +1 -0
- package/dist/rules/no-pass-through-function.js +106 -0
- package/dist/rules/no-swallowed-errors.d.ts +1 -0
- package/dist/rules/no-swallowed-errors.js +168 -0
- package/dist/rules/no-test-timeout-overrides.d.ts +1 -0
- package/dist/rules/no-test-timeout-overrides.js +105 -0
- package/dist/rules/no-vacuous-test-assertions.d.ts +1 -0
- package/dist/rules/no-vacuous-test-assertions.js +71 -0
- package/dist/rules/server-action-guard.d.ts +1 -0
- package/dist/rules/server-action-guard.js +109 -0
- package/dist/rules/statement-spacing.d.ts +1 -0
- package/dist/rules/statement-spacing.js +29 -0
- package/dist/rules/type-guard-name.d.ts +1 -0
- package/dist/rules/type-guard-name.js +59 -0
- package/dist/shared/index.d.ts +26 -0
- package/dist/shared/index.js +260 -0
- package/dist/shared/rule-options.d.ts +2 -0
- package/dist/shared/rule-options.js +8 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.js +35 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +48 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.d.ts +9 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.js +65 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.js +40 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.js +58 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.js +41 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +333 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +74 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.js +59 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.js +124 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.js +26 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.js +26 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +63 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +37 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +61 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.js +38 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +143 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.js +288 -0
- package/dist/vendor/anti-slop/shared/array-method.d.ts +10 -0
- package/dist/vendor/anti-slop/shared/array-method.js +95 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.d.ts +23 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.js +408 -0
- package/dist/vendor/anti-slop/shared/function-parameters.d.ts +6 -0
- package/dist/vendor/anti-slop/shared/function-parameters.js +41 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.d.ts +4 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.js +32 -0
- package/dist/vendor/anti-slop/shared/reflect-method.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/reflect-method.js +24 -0
- package/dist/vendor/anti-slop/shared/scope.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/scope.js +12 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +21 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +147 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.d.ts +14 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.js +19 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.d.ts +91 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.js +493 -0
- package/package.json +89 -0
- package/src/vendor/anti-slop/LICENSE +21 -0
- package/src/vendor/eslint-stylistic/LICENSE +22 -0
- package/tsconfig.base.json +10 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { childNodes, propertyName, variableFor } from "../shared/index.js";
|
|
3
|
+
const exposedArgument = new Map([
|
|
4
|
+
["exposeInMainWorld", 1],
|
|
5
|
+
["exposeInIsolatedWorld", 2],
|
|
6
|
+
]);
|
|
7
|
+
const listenerMethods = new Set([
|
|
8
|
+
"on",
|
|
9
|
+
"once",
|
|
10
|
+
"addListener",
|
|
11
|
+
"prependListener",
|
|
12
|
+
"prependOnceListener",
|
|
13
|
+
]);
|
|
14
|
+
function electronName(sourceCode, node) {
|
|
15
|
+
if (node.type === "MemberExpression") {
|
|
16
|
+
return propertyName(node);
|
|
17
|
+
}
|
|
18
|
+
if (node.type !== "Identifier") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
22
|
+
if (definition?.type === "ImportBinding" && definition.node.type === "ImportSpecifier") {
|
|
23
|
+
const { imported } = definition.node;
|
|
24
|
+
return imported.type === "Literal" ? imported.value : imported.name;
|
|
25
|
+
}
|
|
26
|
+
return node.name;
|
|
27
|
+
}
|
|
28
|
+
function isPropertyPosition(node) {
|
|
29
|
+
const parent = node.parent;
|
|
30
|
+
return ((parent?.type === "MemberExpression" && parent.property === node && !parent.computed) ||
|
|
31
|
+
(parent?.type === "Property" && parent.key === node && parent.value !== node));
|
|
32
|
+
}
|
|
33
|
+
function isWithin(node, root) {
|
|
34
|
+
for (let current = node; current; current = current.parent) {
|
|
35
|
+
if (current === root) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isParameterOf(sourceCode, node, root) {
|
|
42
|
+
return (node?.type === "Identifier" &&
|
|
43
|
+
Boolean(variableFor(sourceCode, node)?.defs.some((definition) => definition.type === "Parameter" && isWithin(definition.node, root))));
|
|
44
|
+
}
|
|
45
|
+
function resolveConstant(sourceCode, node) {
|
|
46
|
+
if (node.type !== "Identifier") {
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
const definitions = variableFor(sourceCode, node)?.defs ?? [];
|
|
50
|
+
const definition = definitions.length === 1 ? definitions[0] : null;
|
|
51
|
+
return definition?.type === "Variable" &&
|
|
52
|
+
definition.parent?.type === "VariableDeclaration" &&
|
|
53
|
+
definition.parent.kind === "const" &&
|
|
54
|
+
definition.node.type === "VariableDeclarator" &&
|
|
55
|
+
definition.node.id.type === "Identifier" &&
|
|
56
|
+
definition.node.init
|
|
57
|
+
? definition.node.init
|
|
58
|
+
: node;
|
|
59
|
+
}
|
|
60
|
+
function walk(node, visitorKeys, visit) {
|
|
61
|
+
visit(node);
|
|
62
|
+
for (const child of childNodes(node, visitorKeys)) {
|
|
63
|
+
walk(child, visitorKeys, visit);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export const electronNoRawIpcRendererExposure = defineRule({
|
|
67
|
+
meta: {
|
|
68
|
+
type: "problem",
|
|
69
|
+
docs: {
|
|
70
|
+
description: "Disallow exposing ipcRenderer, its methods or arbitrary channels through contextBridge.",
|
|
71
|
+
},
|
|
72
|
+
schema: [],
|
|
73
|
+
messages: {
|
|
74
|
+
exposed: "Expose narrow functions that call fixed IPC channels, never ipcRenderer or its methods.",
|
|
75
|
+
channel: "Use a fixed IPC channel. Forwarding a renderer-supplied channel lets the renderer reach every IPC handler.",
|
|
76
|
+
listener: "Wrap the renderer callback and pass it only the payload. Passing it to ipcRenderer.{{method}} hands the renderer the IPC event and its sender.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
create(context) {
|
|
80
|
+
const sourceCode = context.sourceCode;
|
|
81
|
+
function isIpcRenderer(node) {
|
|
82
|
+
return ((node.type === "Identifier" || node.type === "MemberExpression") &&
|
|
83
|
+
!isPropertyPosition(node) &&
|
|
84
|
+
electronName(sourceCode, node) === "ipcRenderer");
|
|
85
|
+
}
|
|
86
|
+
function checkCall(call, method, root) {
|
|
87
|
+
const [channel, listener] = call.arguments;
|
|
88
|
+
if (channel?.type === "SpreadElement" || isParameterOf(sourceCode, channel, root)) {
|
|
89
|
+
context.report({ node: channel, messageId: "channel" });
|
|
90
|
+
}
|
|
91
|
+
if (listenerMethods.has(method) && isParameterOf(sourceCode, listener, root)) {
|
|
92
|
+
context.report({ node: listener, messageId: "listener", data: { method } });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function check(node, root) {
|
|
96
|
+
if (!isIpcRenderer(node)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const member = node.parent;
|
|
100
|
+
const method = member?.type === "MemberExpression" && member.object === node ? propertyName(member) : null;
|
|
101
|
+
const call = member?.parent;
|
|
102
|
+
if (method && call?.type === "CallExpression" && call.callee === member) {
|
|
103
|
+
checkCall(call, method, root);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
context.report({ node, messageId: "exposed" });
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
CallExpression(node) {
|
|
110
|
+
const callee = node.callee;
|
|
111
|
+
if (callee.type !== "MemberExpression") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const name = propertyName(callee);
|
|
115
|
+
const index = name === null ? undefined : exposedArgument.get(name);
|
|
116
|
+
if (index === undefined || electronName(sourceCode, callee.object) !== "contextBridge") {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const exposed = node.arguments[index];
|
|
120
|
+
if (exposed) {
|
|
121
|
+
const root = resolveConstant(sourceCode, exposed);
|
|
122
|
+
walk(root, sourceCode.visitorKeys, (child) => check(child, root));
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const electronNoRemoteModule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, propertyName, variableFor } from "../shared/index.js";
|
|
3
|
+
function moduleName(node) {
|
|
4
|
+
return node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
|
|
5
|
+
}
|
|
6
|
+
function isRemotePackage(name) {
|
|
7
|
+
return name === "@electron/remote" || (name?.startsWith("@electron/remote/") ?? false);
|
|
8
|
+
}
|
|
9
|
+
function isElectron(name) {
|
|
10
|
+
return name === "electron" || (name?.startsWith("electron/") ?? false);
|
|
11
|
+
}
|
|
12
|
+
function requiredModule(sourceCode, node) {
|
|
13
|
+
return node?.type === "CallExpression" && isGlobal(sourceCode, node.callee, "require")
|
|
14
|
+
? moduleName(node.arguments[0])
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
function isElectronModule(sourceCode, node) {
|
|
18
|
+
if (isElectron(requiredModule(sourceCode, node))) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (node?.type !== "Identifier") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const [definition, ...otherDefinitions] = variableFor(sourceCode, node)?.defs ?? [];
|
|
25
|
+
if (definition === undefined || otherDefinitions.length > 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const { node: definitionNode, parent } = definition;
|
|
29
|
+
if (definition.type === "ImportBinding") {
|
|
30
|
+
return (definitionNode.type !== "ImportSpecifier" &&
|
|
31
|
+
parent?.type === "ImportDeclaration" &&
|
|
32
|
+
isElectron(parent.source.value));
|
|
33
|
+
}
|
|
34
|
+
return (definition.type === "Variable" &&
|
|
35
|
+
parent?.type === "VariableDeclaration" &&
|
|
36
|
+
parent.kind === "const" &&
|
|
37
|
+
definitionNode.type === "VariableDeclarator" &&
|
|
38
|
+
definitionNode.id.type === "Identifier" &&
|
|
39
|
+
isElectron(requiredModule(sourceCode, definitionNode.init)));
|
|
40
|
+
}
|
|
41
|
+
function specifierName(node) {
|
|
42
|
+
return node.type === "Identifier" ? node.name : node.value;
|
|
43
|
+
}
|
|
44
|
+
export const electronNoRemoteModule = defineRule({
|
|
45
|
+
meta: {
|
|
46
|
+
type: "problem",
|
|
47
|
+
docs: { description: "Disallow Electron's remote module and @electron/remote." },
|
|
48
|
+
schema: [],
|
|
49
|
+
messages: {
|
|
50
|
+
remote: "Do not use Electron remote APIs. Expose a narrow, validated IPC operation instead.",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
create(context) {
|
|
54
|
+
const sourceCode = context.sourceCode;
|
|
55
|
+
function checkSource(node, source) {
|
|
56
|
+
if (isRemotePackage(moduleName(source))) {
|
|
57
|
+
context.report({ node, messageId: "remote" });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
ImportDeclaration(node) {
|
|
64
|
+
if (checkSource(node, node.source) || !isElectron(node.source.value)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const specifier of node.specifiers) {
|
|
68
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
69
|
+
specifierName(specifier.imported) === "remote") {
|
|
70
|
+
context.report({ node: specifier, messageId: "remote" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
ExportNamedDeclaration(node) {
|
|
75
|
+
if (checkSource(node, node.source) || !isElectron(moduleName(node.source))) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const specifier of node.specifiers) {
|
|
79
|
+
if (specifierName(specifier.local) === "remote") {
|
|
80
|
+
context.report({ node: specifier, messageId: "remote" });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
ExportAllDeclaration(node) {
|
|
85
|
+
checkSource(node, node.source);
|
|
86
|
+
},
|
|
87
|
+
ImportExpression(node) {
|
|
88
|
+
checkSource(node, node.source);
|
|
89
|
+
},
|
|
90
|
+
CallExpression(node) {
|
|
91
|
+
if (isGlobal(sourceCode, node.callee, "require")) {
|
|
92
|
+
checkSource(node, node.arguments[0]);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
MemberExpression(node) {
|
|
96
|
+
if (propertyName(node) === "remote" && isElectronModule(sourceCode, node.object)) {
|
|
97
|
+
context.report({ node, messageId: "remote" });
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
VariableDeclarator(node) {
|
|
101
|
+
if (node.id.type !== "ObjectPattern" || !isElectronModule(sourceCode, node.init)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const property of node.id.properties) {
|
|
105
|
+
if (property.type === "Property" && propertyName(property) === "remote") {
|
|
106
|
+
context.report({ node: property, messageId: "remote" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const electronSecureWebPreferences: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { variableFor } from "../shared/index.js";
|
|
3
|
+
const hardenedPreferences = [
|
|
4
|
+
["contextIsolation", true],
|
|
5
|
+
["sandbox", true],
|
|
6
|
+
["nodeIntegration", false],
|
|
7
|
+
];
|
|
8
|
+
const dangerousPreferences = [
|
|
9
|
+
["allowRunningInsecureContent", false],
|
|
10
|
+
["enableBlinkFeatures", ""],
|
|
11
|
+
["enableRemoteModule", false],
|
|
12
|
+
["experimentalFeatures", false],
|
|
13
|
+
["nodeIntegrationInSubFrames", false],
|
|
14
|
+
["nodeIntegrationInWorker", false],
|
|
15
|
+
["webSecurity", true],
|
|
16
|
+
["webviewTag", false],
|
|
17
|
+
];
|
|
18
|
+
function unwrap(node) {
|
|
19
|
+
if (node?.type === "TSAsExpression" ||
|
|
20
|
+
node?.type === "TSSatisfiesExpression" ||
|
|
21
|
+
node?.type === "TSNonNullExpression" ||
|
|
22
|
+
node?.type === "TSTypeAssertion") {
|
|
23
|
+
return unwrap(node.expression);
|
|
24
|
+
}
|
|
25
|
+
return node;
|
|
26
|
+
}
|
|
27
|
+
function keyName(property) {
|
|
28
|
+
if (!property.computed && property.key.type === "Identifier") {
|
|
29
|
+
return property.key.name;
|
|
30
|
+
}
|
|
31
|
+
return property.key.type === "Literal" ? String(property.key.value) : null;
|
|
32
|
+
}
|
|
33
|
+
function setting(object, name) {
|
|
34
|
+
let property = null;
|
|
35
|
+
let known = true;
|
|
36
|
+
for (const candidate of object.properties) {
|
|
37
|
+
const key = candidate.type === "Property" ? keyName(candidate) : null;
|
|
38
|
+
if (candidate.type === "Property" && key === name) {
|
|
39
|
+
property = candidate;
|
|
40
|
+
known = true;
|
|
41
|
+
}
|
|
42
|
+
else if (key === null) {
|
|
43
|
+
known = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { property, known };
|
|
47
|
+
}
|
|
48
|
+
function hasValue(property, expected) {
|
|
49
|
+
const value = unwrap(property?.value);
|
|
50
|
+
return value?.type === "Literal" && value.value === expected;
|
|
51
|
+
}
|
|
52
|
+
function objectFor(sourceCode, node) {
|
|
53
|
+
const value = unwrap(node);
|
|
54
|
+
if (value?.type === "ObjectExpression") {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (value?.type !== "Identifier") {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const definitions = variableFor(sourceCode, value)?.defs ?? [];
|
|
61
|
+
const definition = definitions.length === 1 ? definitions[0] : null;
|
|
62
|
+
return definition?.type === "Variable" &&
|
|
63
|
+
definition.parent?.type === "VariableDeclaration" &&
|
|
64
|
+
definition.parent.kind === "const" &&
|
|
65
|
+
definition.node.type === "VariableDeclarator" &&
|
|
66
|
+
definition.node.id.type === "Identifier"
|
|
67
|
+
? objectFor(sourceCode, definition.node.init)
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
export const electronSecureWebPreferences = defineRule({
|
|
71
|
+
meta: {
|
|
72
|
+
type: "problem",
|
|
73
|
+
docs: { description: "Require hardened Electron webPreferences." },
|
|
74
|
+
schema: [],
|
|
75
|
+
messages: {
|
|
76
|
+
insecure: "Set webPreferences contextIsolation: true, sandbox: true and nodeIntegration: false explicitly, with nothing spread after them.",
|
|
77
|
+
dangerous: 'Do not enable the dangerous Electron webPreference "{{name}}".',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
create(context) {
|
|
81
|
+
const sourceCode = context.sourceCode;
|
|
82
|
+
return {
|
|
83
|
+
ObjectExpression(node) {
|
|
84
|
+
const { property } = setting(node, "webPreferences");
|
|
85
|
+
if (!property) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const webPreferences = objectFor(sourceCode, property.value);
|
|
89
|
+
if (!webPreferences) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const hardened = hardenedPreferences.every(([name, expected]) => {
|
|
93
|
+
const preference = setting(webPreferences, name);
|
|
94
|
+
return preference.known && hasValue(preference.property, expected);
|
|
95
|
+
});
|
|
96
|
+
if (!hardened) {
|
|
97
|
+
context.report({ node: property.value, messageId: "insecure" });
|
|
98
|
+
}
|
|
99
|
+
for (const [name, safe] of dangerousPreferences) {
|
|
100
|
+
const preference = setting(webPreferences, name).property;
|
|
101
|
+
if (preference && !hasValue(preference, safe)) {
|
|
102
|
+
context.report({ node: preference, messageId: "dangerous", data: { name } });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const explicitApiStates: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { propertyName } from "../shared/index.js";
|
|
3
|
+
const absentTypes = new Set(["TSUndefinedKeyword", "TSNullKeyword"]);
|
|
4
|
+
function isBooleanType(type) {
|
|
5
|
+
if (type?.type === "TSBooleanKeyword") {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
if (type?.type !== "TSUnionType") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const present = type.types.filter((member) => !absentTypes.has(member.type));
|
|
12
|
+
return present.length === 1 && isBooleanType(present[0]);
|
|
13
|
+
}
|
|
14
|
+
function booleanParameter(parameter) {
|
|
15
|
+
const target = parameter.type === "TSParameterProperty" ? parameter.parameter : parameter;
|
|
16
|
+
if (target.type === "Identifier") {
|
|
17
|
+
return isBooleanType(target.typeAnnotation?.typeAnnotation) ? target : null;
|
|
18
|
+
}
|
|
19
|
+
if (target.type !== "AssignmentPattern" || target.left.type !== "Identifier") {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const annotation = target.left.typeAnnotation?.typeAnnotation;
|
|
23
|
+
const isBoolean = annotation
|
|
24
|
+
? isBooleanType(annotation)
|
|
25
|
+
: target.right.type === "Literal" && typeof target.right.value === "boolean";
|
|
26
|
+
return isBoolean ? target.left : null;
|
|
27
|
+
}
|
|
28
|
+
function isSetter(name, fn, parameter) {
|
|
29
|
+
return (/^(?:set|update)\p{Lu}/u.test(name.slice(name.lastIndexOf(".") + 1)) &&
|
|
30
|
+
fn.params.at(-1) === parameter);
|
|
31
|
+
}
|
|
32
|
+
function functionValue(node) {
|
|
33
|
+
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression"
|
|
34
|
+
? node
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
function isPublicMember(member) {
|
|
38
|
+
return (!member.computed &&
|
|
39
|
+
member.key.type !== "PrivateIdentifier" &&
|
|
40
|
+
member.accessibility !== "private" &&
|
|
41
|
+
member.accessibility !== "protected");
|
|
42
|
+
}
|
|
43
|
+
function classMembers(declaration) {
|
|
44
|
+
const name = declaration.id?.name ?? "default";
|
|
45
|
+
return declaration.body.body.flatMap((member) => {
|
|
46
|
+
if ((member.type !== "MethodDefinition" && member.type !== "PropertyDefinition") ||
|
|
47
|
+
!isPublicMember(member)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
if (member.type === "MethodDefinition" && member.kind !== "set" && member.kind !== "get") {
|
|
51
|
+
return [{ name: `${name}.${propertyName(member)}`, fn: member.value }];
|
|
52
|
+
}
|
|
53
|
+
const value = member.type === "PropertyDefinition" ? functionValue(member.value) : null;
|
|
54
|
+
return value ? [{ name: `${name}.${propertyName(member)}`, fn: value }] : [];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function exportedFunctions(declaration) {
|
|
58
|
+
if (!declaration) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
if (declaration.type === "FunctionDeclaration" ||
|
|
62
|
+
declaration.type === "FunctionExpression" ||
|
|
63
|
+
declaration.type === "ArrowFunctionExpression") {
|
|
64
|
+
return [{ name: declaration.id?.name ?? "default", fn: declaration }];
|
|
65
|
+
}
|
|
66
|
+
if (declaration.type === "ClassDeclaration" || declaration.type === "ClassExpression") {
|
|
67
|
+
return classMembers(declaration);
|
|
68
|
+
}
|
|
69
|
+
if (declaration.type === "VariableDeclaration") {
|
|
70
|
+
return declaration.declarations.flatMap((declarator) => {
|
|
71
|
+
const fn = functionValue(declarator.init);
|
|
72
|
+
return fn && declarator.id.type === "Identifier" ? [{ name: declarator.id.name, fn }] : [];
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
export const explicitApiStates = defineRule({
|
|
78
|
+
meta: {
|
|
79
|
+
type: "suggestion",
|
|
80
|
+
docs: {
|
|
81
|
+
description: "Avoid boolean control parameters on exported functions and public methods.",
|
|
82
|
+
},
|
|
83
|
+
schema: [],
|
|
84
|
+
messages: {
|
|
85
|
+
boolean: "Parameter '{{parameter}}' makes `{{name}}` switch on a positional boolean. Take named options or split it into explicit variants.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
create(context) {
|
|
89
|
+
function check({ name, fn }) {
|
|
90
|
+
if (!fn.body) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const parameter of fn.params) {
|
|
94
|
+
const identifier = booleanParameter(parameter);
|
|
95
|
+
if (identifier && !isSetter(name, fn, parameter)) {
|
|
96
|
+
context.report({
|
|
97
|
+
node: identifier,
|
|
98
|
+
messageId: "boolean",
|
|
99
|
+
data: { parameter: identifier.name, name },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
Program(program) {
|
|
106
|
+
const exportedNames = new Set();
|
|
107
|
+
for (const statement of program.body) {
|
|
108
|
+
if (statement.type === "ExportNamedDeclaration" && !statement.source) {
|
|
109
|
+
if (statement.exportKind === "type") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of exportedFunctions(statement.declaration)) {
|
|
113
|
+
check(entry);
|
|
114
|
+
}
|
|
115
|
+
for (const specifier of statement.specifiers) {
|
|
116
|
+
if (specifier.exportKind !== "type" && specifier.local.type === "Identifier") {
|
|
117
|
+
exportedNames.add(specifier.local.name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (statement.type === "ExportDefaultDeclaration") {
|
|
122
|
+
if (statement.declaration.type === "Identifier") {
|
|
123
|
+
exportedNames.add(statement.declaration.name);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
for (const entry of exportedFunctions(statement.declaration)) {
|
|
127
|
+
check(entry);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const statement of program.body) {
|
|
133
|
+
const owner = statement.type === "ClassDeclaration" ? statement.id?.name : undefined;
|
|
134
|
+
for (const entry of exportedFunctions(statement)) {
|
|
135
|
+
if (exportedNames.has(owner ?? entry.name)) {
|
|
136
|
+
check(entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const importBoundaries: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { projectPath, matchesAny } from "../shared/index.js";
|
|
4
|
+
function isStringList(value) {
|
|
5
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6
|
+
}
|
|
7
|
+
function parseBoundary(value) {
|
|
8
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
9
|
+
throw new TypeError("Each import boundary must be an object.");
|
|
10
|
+
}
|
|
11
|
+
const { files, ignore = [], deny, message } = value;
|
|
12
|
+
if (!isStringList(files) ||
|
|
13
|
+
!isStringList(ignore) ||
|
|
14
|
+
!isStringList(deny) ||
|
|
15
|
+
typeof message !== "string") {
|
|
16
|
+
throw new TypeError("Each import boundary needs files, deny and message settings.");
|
|
17
|
+
}
|
|
18
|
+
return { files, ignore, deny, message };
|
|
19
|
+
}
|
|
20
|
+
function parseBoundaries(options) {
|
|
21
|
+
const [option] = options;
|
|
22
|
+
if (typeof option !== "object" ||
|
|
23
|
+
option === null ||
|
|
24
|
+
Array.isArray(option) ||
|
|
25
|
+
!Array.isArray(option.boundaries)) {
|
|
26
|
+
throw new TypeError("Configure import boundaries with a boundaries array.");
|
|
27
|
+
}
|
|
28
|
+
return option.boundaries.map(parseBoundary);
|
|
29
|
+
}
|
|
30
|
+
export const importBoundaries = defineRule({
|
|
31
|
+
meta: {
|
|
32
|
+
type: "problem",
|
|
33
|
+
docs: { description: "Keep modules from importing across configured boundaries." },
|
|
34
|
+
schema: [
|
|
35
|
+
{
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
boundaries: {
|
|
39
|
+
type: "array",
|
|
40
|
+
items: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
files: { type: "array", items: { type: "string" } },
|
|
44
|
+
ignore: { type: "array", items: { type: "string" } },
|
|
45
|
+
deny: { type: "array", items: { type: "string" } },
|
|
46
|
+
message: { type: "string" },
|
|
47
|
+
},
|
|
48
|
+
required: ["files", "deny", "message"],
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["boundaries"],
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
messages: { boundary: "Do not import '{{specifier}}' here. {{message}}" },
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
const file = projectPath(context.cwd, context.filename);
|
|
61
|
+
const active = parseBoundaries(context.options).filter((boundary) => matchesAny(file, boundary.files) && !matchesAny(file, boundary.ignore));
|
|
62
|
+
if (active.length === 0) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
function check(source, typeOnly) {
|
|
66
|
+
if (typeOnly || source?.type !== "Literal" || typeof source.value !== "string") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const specifier = source.value;
|
|
70
|
+
const target = specifier.startsWith(".")
|
|
71
|
+
? projectPath(context.cwd, resolvePath(dirname(context.filename), specifier))
|
|
72
|
+
: specifier;
|
|
73
|
+
const boundary = active.find(({ deny }) => matchesAny(target, deny));
|
|
74
|
+
if (boundary) {
|
|
75
|
+
context.report({
|
|
76
|
+
node: source,
|
|
77
|
+
messageId: "boundary",
|
|
78
|
+
data: { specifier, message: boundary.message },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ImportDeclaration(node) {
|
|
84
|
+
check(node.source, node.importKind === "type" ||
|
|
85
|
+
(node.specifiers.length > 0 &&
|
|
86
|
+
node.specifiers.every((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind === "type")));
|
|
87
|
+
},
|
|
88
|
+
ExportNamedDeclaration(node) {
|
|
89
|
+
check(node.source, node.exportKind === "type");
|
|
90
|
+
},
|
|
91
|
+
ExportAllDeclaration(node) {
|
|
92
|
+
check(node.source, node.exportKind === "type");
|
|
93
|
+
},
|
|
94
|
+
ImportExpression(node) {
|
|
95
|
+
check(node.source, false);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noAmbientClock: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { propertyName, isGlobalValue, isInjectionSeam } from "../shared/index.js";
|
|
3
|
+
export const noAmbientClock = defineRule({
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: { description: "Read the current date and time only in a clock owner." },
|
|
7
|
+
schema: [],
|
|
8
|
+
messages: {
|
|
9
|
+
clock: "Accept the current time from the caller or an injected clock. If this module owns the clock, add it to the `clock` owners.",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
create(context) {
|
|
13
|
+
const sourceCode = context.sourceCode;
|
|
14
|
+
function check(node, date) {
|
|
15
|
+
if (isGlobalValue(sourceCode, date, "Date") && !isInjectionSeam(node)) {
|
|
16
|
+
context.report({ node, messageId: "clock" });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
MemberExpression(node) {
|
|
21
|
+
if (propertyName(node) === "now") {
|
|
22
|
+
check(node, node.object);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
NewExpression(node) {
|
|
26
|
+
if (node.arguments.length === 0) {
|
|
27
|
+
check(node, node.callee);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
CallExpression(node) {
|
|
31
|
+
check(node, node.callee);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noAmbientRandom: import("@oxlint/plugins").Rule;
|