@utoo/lint 0.3.9 → 0.4.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/README.md +11 -3
- package/bin/eslint.js +4 -0
- package/bin/fishlint-lint-staged.js +0 -0
- package/bin/fishlint.js +0 -0
- package/index.cjs +687 -290
- package/index.d.ts +11 -0
- package/index.js +687 -290
- package/lib/config-module.cjs +54 -0
- package/lib/estree-parser.cjs +662 -0
- package/lib/load-config.js +74 -1
- package/lib/native-rule-options.cjs +99 -0
- package/lib/plugin-runner.cjs +342 -0
- package/package.json +13 -10
- package/schema.json +24 -0
package/lib/load-config.js
CHANGED
|
@@ -2,15 +2,88 @@ import { dirname } from "node:path";
|
|
|
2
2
|
import { writeFileSync } from "node:fs";
|
|
3
3
|
import { createJiti } from "jiti";
|
|
4
4
|
|
|
5
|
+
import { nativeRules } from "./native-rule-options.cjs";
|
|
6
|
+
|
|
5
7
|
const configPath = process.argv[2];
|
|
6
8
|
|
|
9
|
+
// Config modules may mount live ESLint plugin objects, which contain functions
|
|
10
|
+
// and frequently reference themselves (`plugin.configs.recommended.plugins`).
|
|
11
|
+
// Serialize them down to the rule names the wrapper needs to route rules; the
|
|
12
|
+
// plugin objects themselves are re-imported in-process when rules run.
|
|
13
|
+
function serializablePlugins(plugins, seen) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [name, plugin] of Object.entries(plugins)) {
|
|
16
|
+
if (!plugin || typeof plugin !== "object") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const rules = {};
|
|
20
|
+
for (const [ruleName, rule] of Object.entries(plugin.rules ?? {})) {
|
|
21
|
+
rules[ruleName] = { meta: serializable(rule?.meta ?? {}, seen) ?? {} };
|
|
22
|
+
}
|
|
23
|
+
result[name] = {
|
|
24
|
+
...(plugin.meta && typeof plugin.meta === "object" ? { meta: serializable(plugin.meta, seen) ?? {} } : {}),
|
|
25
|
+
rules
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function serializable(value, seen = new WeakSet()) {
|
|
32
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "number") {
|
|
36
|
+
return Number.isFinite(value) ? value : null;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === "bigint") {
|
|
39
|
+
return value.toString();
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== "object") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value.toJSON === "function") {
|
|
45
|
+
return serializable(value.toJSON(), seen);
|
|
46
|
+
}
|
|
47
|
+
if (seen.has(value)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
seen.add(value);
|
|
51
|
+
try {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map((item) => {
|
|
54
|
+
const item2 = serializable(item, seen);
|
|
55
|
+
return item2 === undefined ? null : item2;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const [key, item] of Object.entries(value)) {
|
|
60
|
+
if (key === "plugins" && item && typeof item === "object" && !Array.isArray(item)) {
|
|
61
|
+
result.plugins = serializablePlugins(item, seen);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Rule options such as `maxDepth: Infinity` have a JSON spelling the
|
|
65
|
+
// native binary understands; rewrite them before Infinity becomes null.
|
|
66
|
+
// Other non-finite values stay null here because this loader cannot
|
|
67
|
+
// tell native rules from plugin rules; the wrapper reports the native
|
|
68
|
+
// ones with the rule and option name.
|
|
69
|
+
const serialized = serializable(key === "rules" ? nativeRules(item, { strict: false }) : item, seen);
|
|
70
|
+
if (serialized !== undefined) {
|
|
71
|
+
result[key] = serialized;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
} finally {
|
|
76
|
+
seen.delete(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
7
80
|
try {
|
|
8
81
|
if (!configPath) {
|
|
9
82
|
throw new TypeError("missing config path");
|
|
10
83
|
}
|
|
11
84
|
const jiti = createJiti(dirname(configPath));
|
|
12
85
|
const value = await jiti.import(configPath, { default: true });
|
|
13
|
-
const json = JSON.stringify(value);
|
|
86
|
+
const json = JSON.stringify(serializable(value));
|
|
14
87
|
if (json === undefined) {
|
|
15
88
|
throw new TypeError("config did not export a JSON-serializable value");
|
|
16
89
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// JSON has no representation for Infinity or NaN, so JSON.stringify turns them
|
|
4
|
+
// into null and the native binary rejects the rule option with an opaque
|
|
5
|
+
// UnsupportedRuleConfigValue error. Options with a documented unlimited
|
|
6
|
+
// spelling are rewritten to it. Anything else fails with a message that names
|
|
7
|
+
// the rule and option, or becomes null again in lenient mode (used by the
|
|
8
|
+
// config loader subprocess, which cannot tell native rules from plugin rules).
|
|
9
|
+
const UNLIMITED = "∞";
|
|
10
|
+
const INFINITE_OPTION_SPELLINGS = {
|
|
11
|
+
"import/no-cycle": { maxDepth: UNLIMITED }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function nativeRuleConfig(ruleId, ruleConfig, options = {}) {
|
|
15
|
+
if (!Array.isArray(ruleConfig)) {
|
|
16
|
+
return ruleConfig;
|
|
17
|
+
}
|
|
18
|
+
const strict = options.strict !== false;
|
|
19
|
+
const spellings = INFINITE_OPTION_SPELLINGS[ruleId] ?? {};
|
|
20
|
+
return ruleConfig.map((item, index) => {
|
|
21
|
+
if (index === 0) {
|
|
22
|
+
return item;
|
|
23
|
+
}
|
|
24
|
+
if (!isPlainObject(item)) {
|
|
25
|
+
return nativeRuleOptionValue(ruleId, item, `[${index}]`, strict);
|
|
26
|
+
}
|
|
27
|
+
const seen = new WeakSet([item]);
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(item).map(([key, value]) => [
|
|
30
|
+
key,
|
|
31
|
+
value === Infinity && Object.hasOwn(spellings, key)
|
|
32
|
+
? spellings[key]
|
|
33
|
+
: nativeRuleOptionValue(ruleId, value, key, strict, seen)
|
|
34
|
+
])
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nativeRuleOptionValue(ruleId, value, path, strict, seen = new WeakSet()) {
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
if (Number.isFinite(value)) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (!strict) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
`Rule "${ruleId}" option "${path}" is ${String(value)}, which cannot be written to the native JSON config.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (value === null || typeof value !== "object" || seen.has(value)) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
seen.add(value);
|
|
55
|
+
try {
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return value.map((item, index) => nativeRuleOptionValue(ruleId, item, `${path}[${index}]`, strict, seen));
|
|
58
|
+
}
|
|
59
|
+
if (!isPlainObject(value)) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
return Object.fromEntries(
|
|
63
|
+
Object.entries(value).map(([key, item]) => [
|
|
64
|
+
key,
|
|
65
|
+
nativeRuleOptionValue(ruleId, item, `${path}.${key}`, strict, seen)
|
|
66
|
+
])
|
|
67
|
+
);
|
|
68
|
+
} finally {
|
|
69
|
+
seen.delete(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nativeRules(rules, options = {}) {
|
|
74
|
+
if (!isPlainObject(rules)) {
|
|
75
|
+
return rules;
|
|
76
|
+
}
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(rules).map(([ruleId, ruleConfig]) => [ruleId, nativeRuleConfig(ruleId, ruleConfig, options)])
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function nativeFlatConfigEntries(entries, options = {}) {
|
|
83
|
+
if (!Array.isArray(entries)) {
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
return entries.map((entry) =>
|
|
87
|
+
isPlainObject(entry) && entry.rules ? { ...entry, rules: nativeRules(entry.rules, options) } : entry
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const prototype = Object.getPrototypeOf(value);
|
|
96
|
+
return prototype === Object.prototype || prototype === null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { nativeFlatConfigEntries, nativeRuleConfig, nativeRules };
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Runs ESLint plugin rule listeners over an ESTree AST.
|
|
4
|
+
//
|
|
5
|
+
// Listener keys are esquery selectors (`Identifier`, `CallExpression > Literal`,
|
|
6
|
+
// `Program:exit`, ...). This mirrors ESLint's NodeEventGenerator: selectors are
|
|
7
|
+
// parsed once, indexed by the node types they can match, and applied in
|
|
8
|
+
// specificity order while the AST is traversed with the parser's visitor keys.
|
|
9
|
+
//
|
|
10
|
+
// CommonJS on purpose: shared by the ESM and CommonJS entry points.
|
|
11
|
+
|
|
12
|
+
const esquery = require("esquery");
|
|
13
|
+
|
|
14
|
+
const NON_CHILD_KEYS = new Set(["type", "start", "end", "range", "loc", "parent", "comments", "tokens"]);
|
|
15
|
+
|
|
16
|
+
function isNode(value) {
|
|
17
|
+
return value !== null && typeof value === "object" && typeof value.type === "string";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fallbackKeys(node) {
|
|
21
|
+
return Object.keys(node).filter((key) => !NON_CHILD_KEYS.has(key));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function childKeys(node, visitorKeys) {
|
|
25
|
+
return visitorKeys?.[node.type] ?? fallbackKeys(node);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function possibleTypes(selector) {
|
|
29
|
+
switch (selector.type) {
|
|
30
|
+
case "identifier":
|
|
31
|
+
return [selector.value];
|
|
32
|
+
case "matches": {
|
|
33
|
+
const types = [];
|
|
34
|
+
for (const child of selector.selectors) {
|
|
35
|
+
const childTypes = possibleTypes(child);
|
|
36
|
+
if (!childTypes) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
types.push(...childTypes);
|
|
40
|
+
}
|
|
41
|
+
return types;
|
|
42
|
+
}
|
|
43
|
+
case "compound": {
|
|
44
|
+
const typesForComponents = selector.selectors.map(possibleTypes).filter(Boolean);
|
|
45
|
+
return typesForComponents.length === 0 ? null : typesForComponents[0];
|
|
46
|
+
}
|
|
47
|
+
case "child":
|
|
48
|
+
case "descendant":
|
|
49
|
+
case "sibling":
|
|
50
|
+
case "adjacent":
|
|
51
|
+
return possibleTypes(selector.right);
|
|
52
|
+
case "class":
|
|
53
|
+
if (selector.name === "function") {
|
|
54
|
+
return ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"];
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
default:
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function countClassAttributes(selector) {
|
|
63
|
+
switch (selector.type) {
|
|
64
|
+
case "child":
|
|
65
|
+
case "descendant":
|
|
66
|
+
case "sibling":
|
|
67
|
+
case "adjacent":
|
|
68
|
+
return countClassAttributes(selector.left) + countClassAttributes(selector.right);
|
|
69
|
+
case "compound":
|
|
70
|
+
case "not":
|
|
71
|
+
case "matches":
|
|
72
|
+
return selector.selectors.reduce((sum, child) => sum + countClassAttributes(child), 0);
|
|
73
|
+
case "attribute":
|
|
74
|
+
case "field":
|
|
75
|
+
case "nth-child":
|
|
76
|
+
case "nth-last-child":
|
|
77
|
+
return 1;
|
|
78
|
+
default:
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countIdentifiers(selector) {
|
|
84
|
+
switch (selector.type) {
|
|
85
|
+
case "child":
|
|
86
|
+
case "descendant":
|
|
87
|
+
case "sibling":
|
|
88
|
+
case "adjacent":
|
|
89
|
+
return countIdentifiers(selector.left) + countIdentifiers(selector.right);
|
|
90
|
+
case "compound":
|
|
91
|
+
case "not":
|
|
92
|
+
case "matches":
|
|
93
|
+
return selector.selectors.reduce((sum, child) => sum + countIdentifiers(child), 0);
|
|
94
|
+
case "identifier":
|
|
95
|
+
return 1;
|
|
96
|
+
default:
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function compareSpecificity(left, right) {
|
|
102
|
+
return left.attributeCount - right.attributeCount
|
|
103
|
+
|| left.identifierCount - right.identifierCount
|
|
104
|
+
|| (left.rawSelector <= right.rawSelector ? -1 : 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const selectorCache = new Map();
|
|
108
|
+
|
|
109
|
+
function parseSelector(rawSelector) {
|
|
110
|
+
if (selectorCache.has(rawSelector)) {
|
|
111
|
+
return selectorCache.get(rawSelector);
|
|
112
|
+
}
|
|
113
|
+
const isExit = rawSelector.endsWith(":exit");
|
|
114
|
+
const source = isExit ? rawSelector.slice(0, -":exit".length) : rawSelector;
|
|
115
|
+
let parsedSelector;
|
|
116
|
+
try {
|
|
117
|
+
parsedSelector = esquery.parse(source.replace(/:exit$/u, ""));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const wrapped = new SyntaxError(`Syntax error in selector "${source}": ${error.message}`);
|
|
120
|
+
wrapped.cause = error;
|
|
121
|
+
throw wrapped;
|
|
122
|
+
}
|
|
123
|
+
const selector = {
|
|
124
|
+
rawSelector,
|
|
125
|
+
isExit,
|
|
126
|
+
parsedSelector,
|
|
127
|
+
listenerTypes: possibleTypes(parsedSelector),
|
|
128
|
+
attributeCount: countClassAttributes(parsedSelector),
|
|
129
|
+
identifierCount: countIdentifiers(parsedSelector)
|
|
130
|
+
};
|
|
131
|
+
selectorCache.set(rawSelector, selector);
|
|
132
|
+
return selector;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function indexSelectors(selectors) {
|
|
136
|
+
const byType = new Map();
|
|
137
|
+
const anyType = [];
|
|
138
|
+
for (const selector of selectors) {
|
|
139
|
+
if (selector.listenerTypes) {
|
|
140
|
+
for (const type of selector.listenerTypes) {
|
|
141
|
+
if (!byType.has(type)) {
|
|
142
|
+
byType.set(type, []);
|
|
143
|
+
}
|
|
144
|
+
byType.get(type).push(selector);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
anyType.push(selector);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const list of byType.values()) {
|
|
151
|
+
list.sort(compareSpecificity);
|
|
152
|
+
}
|
|
153
|
+
anyType.sort(compareSpecificity);
|
|
154
|
+
return { byType, anyType };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function wrapListenerError(error, entry, node) {
|
|
158
|
+
if (error && typeof error === "object" && !error.utooLintRuleId) {
|
|
159
|
+
const location = node?.loc?.start ? ` at ${node.loc.start.line}:${node.loc.start.column + 1}` : "";
|
|
160
|
+
const wrapped = new Error(`Error while running ESLint plugin rule "${entry.ruleId}"${location}: ${error.message}`);
|
|
161
|
+
wrapped.cause = error;
|
|
162
|
+
wrapped.stack = `${wrapped.message}\n${error.stack ?? ""}`;
|
|
163
|
+
wrapped.utooLintRuleId = entry.ruleId;
|
|
164
|
+
return wrapped;
|
|
165
|
+
}
|
|
166
|
+
return error;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Traverse `program` and invoke matching listeners.
|
|
171
|
+
*
|
|
172
|
+
* `listenersBySelector` maps a raw selector string to entries of shape
|
|
173
|
+
* `{ ruleId, listener }`. Listeners for the same selector are invoked in
|
|
174
|
+
* registration order; different selectors are ordered by specificity.
|
|
175
|
+
*/
|
|
176
|
+
function runRuleListeners(program, listenersBySelector, options = {}) {
|
|
177
|
+
const visitorKeys = options.visitorKeys ?? null;
|
|
178
|
+
const esqueryOptions = {
|
|
179
|
+
visitorKeys: visitorKeys ?? undefined,
|
|
180
|
+
fallback: fallbackKeys
|
|
181
|
+
};
|
|
182
|
+
const selectors = [];
|
|
183
|
+
const entriesBySelector = new Map();
|
|
184
|
+
for (const [rawSelector, entries] of listenersBySelector) {
|
|
185
|
+
const selector = parseSelector(rawSelector);
|
|
186
|
+
selectors.push(selector);
|
|
187
|
+
entriesBySelector.set(selector, entries);
|
|
188
|
+
}
|
|
189
|
+
const enter = indexSelectors(selectors.filter((selector) => !selector.isExit));
|
|
190
|
+
const exit = indexSelectors(selectors.filter((selector) => selector.isExit));
|
|
191
|
+
const ancestry = [];
|
|
192
|
+
|
|
193
|
+
function apply(index, node) {
|
|
194
|
+
const typed = index.byType.get(node.type) ?? [];
|
|
195
|
+
const untyped = index.anyType;
|
|
196
|
+
let typedIndex = 0;
|
|
197
|
+
let untypedIndex = 0;
|
|
198
|
+
while (typedIndex < typed.length || untypedIndex < untyped.length) {
|
|
199
|
+
let selector;
|
|
200
|
+
if (untypedIndex >= untyped.length || (typedIndex < typed.length && compareSpecificity(typed[typedIndex], untyped[untypedIndex]) < 0)) {
|
|
201
|
+
selector = typed[typedIndex];
|
|
202
|
+
typedIndex += 1;
|
|
203
|
+
} else {
|
|
204
|
+
selector = untyped[untypedIndex];
|
|
205
|
+
untypedIndex += 1;
|
|
206
|
+
}
|
|
207
|
+
if (!esquery.matches(node, selector.parsedSelector, ancestry, esqueryOptions)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const entry of entriesBySelector.get(selector)) {
|
|
211
|
+
try {
|
|
212
|
+
entry.listener(node);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw wrapListenerError(error, entry, node);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function visit(node) {
|
|
221
|
+
apply(enter, node);
|
|
222
|
+
ancestry.unshift(node);
|
|
223
|
+
for (const key of childKeys(node, visitorKeys)) {
|
|
224
|
+
const value = node[key];
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
for (const item of value) {
|
|
227
|
+
if (isNode(item)) {
|
|
228
|
+
visit(item);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} else if (isNode(value)) {
|
|
232
|
+
visit(value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
ancestry.shift();
|
|
236
|
+
apply(exit, node);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
visit(program);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isPlainObject(value) {
|
|
243
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function deepMergeDefaults(defaults, value) {
|
|
247
|
+
if (value === undefined) {
|
|
248
|
+
return structuredCloneSafe(defaults);
|
|
249
|
+
}
|
|
250
|
+
if (isPlainObject(defaults) && isPlainObject(value)) {
|
|
251
|
+
const result = { ...value };
|
|
252
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
253
|
+
result[key] = deepMergeDefaults(defaultValue, value[key]);
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function structuredCloneSafe(value) {
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
return value.map(structuredCloneSafe);
|
|
263
|
+
}
|
|
264
|
+
if (isPlainObject(value)) {
|
|
265
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, structuredCloneSafe(item)]));
|
|
266
|
+
}
|
|
267
|
+
return value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function schemaItemAt(schema, index) {
|
|
271
|
+
if (Array.isArray(schema)) {
|
|
272
|
+
return schema[index];
|
|
273
|
+
}
|
|
274
|
+
if (!isPlainObject(schema)) {
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(schema.items)) {
|
|
278
|
+
return schema.items[index];
|
|
279
|
+
}
|
|
280
|
+
if (isPlainObject(schema.items)) {
|
|
281
|
+
return schema.items;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
284
|
+
return schema.prefixItems[index];
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Applies JSON-schema `default` values the way ESLint's validator does: only
|
|
290
|
+
// properties of objects the user actually passed are filled in.
|
|
291
|
+
function applySchemaDefaults(schema, value) {
|
|
292
|
+
if (!isPlainObject(schema)) {
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
if (isPlainObject(value) && isPlainObject(schema.properties)) {
|
|
296
|
+
const result = { ...value };
|
|
297
|
+
for (const [key, propertySchema] of Object.entries(schema.properties)) {
|
|
298
|
+
if (!isPlainObject(propertySchema)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (result[key] === undefined && propertySchema.default !== undefined) {
|
|
302
|
+
result[key] = structuredCloneSafe(propertySchema.default);
|
|
303
|
+
}
|
|
304
|
+
if (result[key] !== undefined) {
|
|
305
|
+
result[key] = applySchemaDefaults(propertySchema, result[key]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(value) && isPlainObject(schema.items)) {
|
|
311
|
+
return value.map((item) => applySchemaDefaults(schema.items, item));
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Compute the effective `context.options` for a rule: user options with
|
|
318
|
+
* `meta.defaultOptions` merged in (ESLint >= 9.15) and JSON-schema defaults
|
|
319
|
+
* filled in for the object options that were provided.
|
|
320
|
+
*/
|
|
321
|
+
function applyRuleOptionDefaults(rule, options = []) {
|
|
322
|
+
const meta = rule?.meta ?? {};
|
|
323
|
+
let result = Array.isArray(options) ? options.slice() : [];
|
|
324
|
+
if (Array.isArray(meta.defaultOptions)) {
|
|
325
|
+
const merged = [];
|
|
326
|
+
const length = Math.max(meta.defaultOptions.length, result.length);
|
|
327
|
+
for (let index = 0; index < length; index += 1) {
|
|
328
|
+
merged.push(deepMergeDefaults(meta.defaultOptions[index], result[index]));
|
|
329
|
+
}
|
|
330
|
+
result = merged;
|
|
331
|
+
}
|
|
332
|
+
if (meta.schema) {
|
|
333
|
+
result = result.map((value, index) => applySchemaDefaults(schemaItemAt(meta.schema, index), value));
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = {
|
|
339
|
+
applyRuleOptionDefaults,
|
|
340
|
+
parseSelector,
|
|
341
|
+
runRuleListeners
|
|
342
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@utoo/lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "High-performance JavaScript and TypeScript linter powered by Yuku",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -50,9 +50,7 @@
|
|
|
50
50
|
"./package.json": "./package.json"
|
|
51
51
|
},
|
|
52
52
|
"bin": {
|
|
53
|
-
"
|
|
54
|
-
"fishlint": "./bin/fishlint.js",
|
|
55
|
-
"fishlint-lint-staged": "./bin/fishlint-lint-staged.js",
|
|
53
|
+
"utlint": "./bin/utoo-lint.js",
|
|
56
54
|
"utoo-lint": "./bin/utoo-lint.js"
|
|
57
55
|
},
|
|
58
56
|
"files": [
|
|
@@ -76,16 +74,21 @@
|
|
|
76
74
|
"use-at-your-own-risk.js"
|
|
77
75
|
],
|
|
78
76
|
"optionalDependencies": {
|
|
79
|
-
"@utoo/lint-darwin-arm64": "0.
|
|
80
|
-
"@utoo/lint-darwin-x64": "0.
|
|
81
|
-
"@utoo/lint-linux-arm64": "0.
|
|
82
|
-
"@utoo/lint-linux-x64": "0.
|
|
83
|
-
"@utoo/lint-win32-x64": "0.
|
|
77
|
+
"@utoo/lint-darwin-arm64": "0.4.1",
|
|
78
|
+
"@utoo/lint-darwin-x64": "0.4.1",
|
|
79
|
+
"@utoo/lint-linux-arm64": "0.4.1",
|
|
80
|
+
"@utoo/lint-linux-x64": "0.4.1",
|
|
81
|
+
"@utoo/lint-win32-x64": "0.4.1"
|
|
84
82
|
},
|
|
85
83
|
"dependencies": {
|
|
86
84
|
"@eslint/eslintrc": "2.1.4",
|
|
87
85
|
"@eslint/js": "8.57.1",
|
|
88
|
-
"
|
|
86
|
+
"@typescript-eslint/scope-manager": "8.61.0",
|
|
87
|
+
"@typescript-eslint/visitor-keys": "8.61.0",
|
|
88
|
+
"eslint-scope": "9.1.2",
|
|
89
|
+
"esquery": "1.7.0",
|
|
90
|
+
"jiti": "2.7.0",
|
|
91
|
+
"yuku-parser": "0.10.1"
|
|
89
92
|
},
|
|
90
93
|
"publishConfig": {
|
|
91
94
|
"access": "public",
|
package/schema.json
CHANGED
|
@@ -82,6 +82,30 @@
|
|
|
82
82
|
}
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
|
+
"languageOptions": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": true,
|
|
88
|
+
"properties": {
|
|
89
|
+
"globals": {
|
|
90
|
+
"description": "Globals declared for matching files, using ESLint flat config values. \"readonly\" accepts reads, \"writable\" also accepts writes, and \"off\" removes a built-in global.",
|
|
91
|
+
"type": "object",
|
|
92
|
+
"additionalProperties": {
|
|
93
|
+
"oneOf": [
|
|
94
|
+
{
|
|
95
|
+
"type": "string",
|
|
96
|
+
"enum": ["readonly", "readable", "writable", "writeable", "off"]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"type": "null"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
85
109
|
"ignores": {
|
|
86
110
|
"description": "File globs ignored by this config entry.",
|
|
87
111
|
"oneOf": [
|