eslint-soml 0.0.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/index.d.ts +33 -0
- package/index.js +47 -0
- package/language/soml-language.js +72 -0
- package/language/soml-source-code.js +277 -0
- package/license +9 -0
- package/package.json +83 -0
- package/readme.md +175 -0
- package/rules/format.js +134 -0
- package/rules/index.js +17 -0
- package/rules/key-name-casing.js +94 -0
- package/rules/no-block-string-trailing-whitespace.js +64 -0
- package/rules/no-empty-keys.js +43 -0
- package/rules/no-invisible-characters.js +90 -0
- package/rules/no-loss-of-precision.js +96 -0
- package/rules/no-mixed-number-types.js +81 -0
- package/rules/no-numeric-dotted-keys.js +81 -0
- package/rules/no-unnormalized-keys.js +62 -0
- package/rules/numeric-separators-style.js +136 -0
- package/rules/prefer-bare-keys.js +63 -0
- package/rules/prefer-literal-strings.js +85 -0
- package/rules/prefer-utc-instants.js +53 -0
- package/rules/rule/index.js +7 -0
- package/rules/rule/soml-context.js +47 -0
- package/rules/rule/soml-listeners.js +63 -0
- package/rules/rule/to-eslint-create.js +34 -0
- package/rules/rule/to-eslint-listener.js +42 -0
- package/rules/rule/to-eslint-problem.js +53 -0
- package/rules/rule/to-eslint-rule-fixer.js +56 -0
- package/rules/rule/to-eslint-rule.js +36 -0
- package/rules/rule/to-eslint-rules.js +9 -0
- package/rules/rule/utilities.js +29 -0
- package/rules/sort-keys.js +187 -0
- package/rules/spaced-comment.js +83 -0
- package/rules/utils/escape-string.js +30 -0
- package/rules/utils/get-documentation-url.js +15 -0
- package/rules/utils/index.js +2 -0
- package/rules/utils/invisible-characters.js +26 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {ESLint, Linter} from 'eslint';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
Node,
|
|
5
|
+
DocumentNode,
|
|
6
|
+
ObjectNode,
|
|
7
|
+
MemberNode,
|
|
8
|
+
ArrayNode,
|
|
9
|
+
KeyNode,
|
|
10
|
+
KeySegmentNode,
|
|
11
|
+
ValueNode,
|
|
12
|
+
StringNode,
|
|
13
|
+
IntegerNode,
|
|
14
|
+
FloatNode,
|
|
15
|
+
BooleanNode,
|
|
16
|
+
NullNode,
|
|
17
|
+
InstantNode,
|
|
18
|
+
DurationNode,
|
|
19
|
+
Token,
|
|
20
|
+
Comment,
|
|
21
|
+
Position,
|
|
22
|
+
SourceLocation,
|
|
23
|
+
} from 'soml-lang';
|
|
24
|
+
|
|
25
|
+
declare const eslintSoml: ESLint.Plugin & {
|
|
26
|
+
configs: {
|
|
27
|
+
recommended: Linter.Config;
|
|
28
|
+
unopinionated: Linter.Config;
|
|
29
|
+
all: Linter.Config;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default eslintSoml;
|
package/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as rawRules from './rules/index.js';
|
|
2
|
+
import {toEslintRules} from './rules/rule/index.js';
|
|
3
|
+
import SomlLanguage from './language/soml-language.js';
|
|
4
|
+
import packageJson from './package.json' with {type: 'json'};
|
|
5
|
+
|
|
6
|
+
const rules = toEslintRules(rawRules);
|
|
7
|
+
|
|
8
|
+
const createRules = shouldEnable => Object.fromEntries(Object.entries(rules).map(([id, rule]) => [
|
|
9
|
+
`soml/${id}`,
|
|
10
|
+
shouldEnable(rule) ? 'error' : 'off',
|
|
11
|
+
]));
|
|
12
|
+
|
|
13
|
+
const recommendedRules = createRules(rule => Boolean(rule.meta.docs.recommended));
|
|
14
|
+
const unopinionatedRules = createRules(rule => rule.meta.docs.recommended === 'unopinionated');
|
|
15
|
+
const allRules = createRules(() => true);
|
|
16
|
+
|
|
17
|
+
const createConfig = (rules, flatConfigName) => ({
|
|
18
|
+
name: flatConfigName,
|
|
19
|
+
files: ['**/*.soml'],
|
|
20
|
+
plugins: {
|
|
21
|
+
soml,
|
|
22
|
+
},
|
|
23
|
+
language: 'soml/soml',
|
|
24
|
+
rules,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const soml = {
|
|
28
|
+
meta: {
|
|
29
|
+
// `eslint-doc-generator` derives the rule prefix from this name, expecting either the `eslint-plugin-<prefix>` convention or the prefix itself. The package name does not follow that convention, so this is the prefix.
|
|
30
|
+
name: 'soml',
|
|
31
|
+
version: packageJson.version,
|
|
32
|
+
},
|
|
33
|
+
languages: {
|
|
34
|
+
soml: new SomlLanguage(),
|
|
35
|
+
},
|
|
36
|
+
rules,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const configs = {
|
|
40
|
+
recommended: createConfig(recommendedRules, 'soml/recommended'),
|
|
41
|
+
unopinionated: createConfig(unopinionatedRules, 'soml/unopinionated'),
|
|
42
|
+
all: createConfig(allRules, 'soml/all'),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
soml.configs = configs;
|
|
46
|
+
|
|
47
|
+
export default soml;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {parseTree, ParseError, visitorKeys} from 'soml-lang';
|
|
2
|
+
import SomlSourceCode from './soml-source-code.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@import {File, OkParseResult, ParseResult} from '@eslint/core';
|
|
6
|
+
@import {DocumentNode} from 'soml-lang';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
A 1-based line and column, with the column in UTF-16 code units like every other ESLint column. ESLint does not adjust a parse error's position by `columnStart`. `ParseError#column` counts code points instead, so it is not used.
|
|
11
|
+
*/
|
|
12
|
+
function locate(text, offset) {
|
|
13
|
+
const before = text.slice(0, offset);
|
|
14
|
+
return {line: before.split('\n').length, column: offset - before.lastIndexOf('\n')};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
The SOML language for ESLint.
|
|
19
|
+
*/
|
|
20
|
+
export default class SomlLanguage {
|
|
21
|
+
fileType = 'text';
|
|
22
|
+
lineStart = 1;
|
|
23
|
+
columnStart = 0;
|
|
24
|
+
nodeTypeKey = 'type';
|
|
25
|
+
visitorKeys = visitorKeys;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
There are no language options.
|
|
29
|
+
*/
|
|
30
|
+
validateLanguageOptions() {}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
@param {File} file
|
|
34
|
+
@returns {ParseResult<DocumentNode>}
|
|
35
|
+
*/
|
|
36
|
+
parse(file) {
|
|
37
|
+
// ESLint removes a BOM before this point, so it is checked here.
|
|
38
|
+
if (file.bom) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
errors: [{message: 'A byte order mark (BOM) is not allowed', line: 1, column: 1}],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const text = /** @type {string} */ (file.body);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return {ok: true, ast: parseTree(text)};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!(error instanceof ParseError)) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// `reason` is the message without the position and the code frame, which ESLint shows on its own.
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
errors: [{message: error.reason, ...locate(text, error.offset)}],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
@param {File} file
|
|
64
|
+
@param {OkParseResult<DocumentNode>} parseResult
|
|
65
|
+
*/
|
|
66
|
+
createSourceCode(file, parseResult) {
|
|
67
|
+
return new SomlSourceCode({
|
|
68
|
+
text: /** @type {string} */ (file.body),
|
|
69
|
+
ast: parseResult.ast,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextSourceCodeBase,
|
|
3
|
+
VisitNodeStep,
|
|
4
|
+
ConfigCommentParser,
|
|
5
|
+
Directive,
|
|
6
|
+
} from '@eslint/plugin-kit';
|
|
7
|
+
import {visitorKeys} from 'soml-lang';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
@import {DirectiveType, FileProblem, RulesConfig, SourceLocation} from '@eslint/core';
|
|
11
|
+
@import {DocumentNode, Node, Token, Comment} from 'soml-lang';
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const commentParser = new ConfigCommentParser();
|
|
15
|
+
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/v;
|
|
16
|
+
|
|
17
|
+
const isComment = item => item.type === 'Line' || item.type === 'Block';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
The source code of a SOML document, which rules get as `context.sourceCode`.
|
|
21
|
+
*/
|
|
22
|
+
export default class SomlSourceCode extends TextSourceCodeBase {
|
|
23
|
+
/**
|
|
24
|
+
@type {VisitNodeStep[] | undefined}
|
|
25
|
+
*/
|
|
26
|
+
#steps;
|
|
27
|
+
#parents = new WeakMap();
|
|
28
|
+
/**
|
|
29
|
+
Every token and comment, in source order.
|
|
30
|
+
|
|
31
|
+
@type {Array<Token | Comment>}
|
|
32
|
+
*/
|
|
33
|
+
#items;
|
|
34
|
+
/**
|
|
35
|
+
@type {Comment[] | undefined}
|
|
36
|
+
*/
|
|
37
|
+
#inlineConfigComments;
|
|
38
|
+
/**
|
|
39
|
+
@type {DocumentNode}
|
|
40
|
+
*/
|
|
41
|
+
ast;
|
|
42
|
+
visitorKeys = visitorKeys;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
@param {{text: string, ast: DocumentNode}} options
|
|
46
|
+
*/
|
|
47
|
+
constructor({text, ast}) {
|
|
48
|
+
// SOML has no carriage returns, so a line ends only at a line feed.
|
|
49
|
+
super({text, ast, lineEndingPattern: /\n/v});
|
|
50
|
+
this.ast = ast;
|
|
51
|
+
this.#items = [...ast.tokens, ...ast.comments].toSorted((first, second) => first.range[0] - second.range[0]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
The index of the first token or comment that starts at or after `offset`. They never overlap, so their ends are in order too.
|
|
56
|
+
*/
|
|
57
|
+
#indexAt(offset) {
|
|
58
|
+
const items = this.#items;
|
|
59
|
+
let low = 0;
|
|
60
|
+
let high = items.length;
|
|
61
|
+
|
|
62
|
+
while (low < high) {
|
|
63
|
+
const middle = Math.floor((low + high) / 2);
|
|
64
|
+
|
|
65
|
+
if (items[middle].range[0] < offset) {
|
|
66
|
+
low = middle + 1;
|
|
67
|
+
} else {
|
|
68
|
+
high = middle;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return low;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/*
|
|
76
|
+
Moves from `index` by `step` until it finds a token, or a comment when they are included. Returns `null` when there is none, like ESLint's own `SourceCode`, so rules can use the usual `=== null` check.
|
|
77
|
+
*/
|
|
78
|
+
#find(index, step, includeComments, isInside = () => true) {
|
|
79
|
+
for (; index >= 0 && index < this.#items.length; index += step) {
|
|
80
|
+
const item = this.#items[index];
|
|
81
|
+
|
|
82
|
+
if (!isInside(item)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (includeComments || !isComment(item)) {
|
|
87
|
+
return item;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
@returns {Iterable<VisitNodeStep>}
|
|
96
|
+
*/
|
|
97
|
+
traverse() {
|
|
98
|
+
if (this.#steps) {
|
|
99
|
+
return this.#steps;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const steps = [];
|
|
103
|
+
const parents = this.#parents;
|
|
104
|
+
|
|
105
|
+
const visit = (node, parent) => {
|
|
106
|
+
if (parent) {
|
|
107
|
+
parents.set(node, parent);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
steps.push(new VisitNodeStep({target: node, phase: 1, args: [node, parent]}));
|
|
111
|
+
|
|
112
|
+
for (const key of visitorKeys[node.type]) {
|
|
113
|
+
const child = node[key];
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(child)) {
|
|
116
|
+
for (const item of child) {
|
|
117
|
+
visit(item, node);
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
visit(child, node);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
steps.push(new VisitNodeStep({target: node, phase: 2, args: [node, parent]}));
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
visit(this.ast);
|
|
128
|
+
this.#steps = steps;
|
|
129
|
+
return steps;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
@param {Node} node
|
|
134
|
+
@returns {Node | undefined}
|
|
135
|
+
*/
|
|
136
|
+
getParent(node) {
|
|
137
|
+
if (!this.#steps) {
|
|
138
|
+
this.traverse();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.#parents.get(node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
@returns {readonly Comment[]}
|
|
146
|
+
*/
|
|
147
|
+
getAllComments() {
|
|
148
|
+
return this.ast.comments;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
@returns {Comment[]}
|
|
153
|
+
*/
|
|
154
|
+
getInlineConfigNodes() {
|
|
155
|
+
this.#inlineConfigComments ??= this.ast.comments.filter(comment => INLINE_CONFIG.test(comment.value));
|
|
156
|
+
return this.#inlineConfigComments;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
@returns {{problems: FileProblem[], directives: Directive[]}}
|
|
161
|
+
*/
|
|
162
|
+
getDisableDirectives() {
|
|
163
|
+
const problems = [];
|
|
164
|
+
const directives = [];
|
|
165
|
+
|
|
166
|
+
for (const comment of this.getInlineConfigNodes()) {
|
|
167
|
+
const {label, value, justification} = commentParser.parseDirective(comment.value);
|
|
168
|
+
|
|
169
|
+
// An `eslint-disable-line` comment may not span lines, because it would be unclear which line it applies to.
|
|
170
|
+
if (label === 'eslint-disable-line' && comment.loc.start.line !== comment.loc.end.line) {
|
|
171
|
+
problems.push({
|
|
172
|
+
ruleId: null,
|
|
173
|
+
message: `${label} comment should not span multiple lines.`,
|
|
174
|
+
loc: comment.loc,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (label !== 'eslint') {
|
|
180
|
+
directives.push(new Directive({
|
|
181
|
+
type: /** @type {DirectiveType} */ (label.slice('eslint-'.length)),
|
|
182
|
+
node: comment,
|
|
183
|
+
value,
|
|
184
|
+
justification,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {problems, directives};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
@returns {{problems: FileProblem[], configs: Array<{config: {rules: RulesConfig}, loc: SourceLocation}>}}
|
|
194
|
+
*/
|
|
195
|
+
applyInlineConfig() {
|
|
196
|
+
const problems = [];
|
|
197
|
+
const configs = [];
|
|
198
|
+
|
|
199
|
+
for (const comment of this.getInlineConfigNodes()) {
|
|
200
|
+
const {label, value} = commentParser.parseDirective(comment.value);
|
|
201
|
+
|
|
202
|
+
if (label !== 'eslint') {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const result = commentParser.parseJSONLikeConfig(value);
|
|
207
|
+
|
|
208
|
+
if (result.ok) {
|
|
209
|
+
configs.push({config: {rules: result.config}, loc: comment.loc});
|
|
210
|
+
} else {
|
|
211
|
+
problems.push({
|
|
212
|
+
ruleId: null,
|
|
213
|
+
message: result.error.message,
|
|
214
|
+
loc: comment.loc,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {configs, problems};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
The token before a node, token, or comment.
|
|
224
|
+
|
|
225
|
+
@param {Node | Token | Comment} nodeOrToken
|
|
226
|
+
@param {{includeComments?: boolean}} [options]
|
|
227
|
+
@returns {Token | Comment | null}
|
|
228
|
+
*/
|
|
229
|
+
getTokenBefore(nodeOrToken, {includeComments = false} = {}) {
|
|
230
|
+
return this.#find(this.#indexAt(nodeOrToken.range[0]) - 1, -1, includeComments);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
The token after a node, token, or comment.
|
|
235
|
+
|
|
236
|
+
@param {Node | Token | Comment} nodeOrToken
|
|
237
|
+
@param {{includeComments?: boolean}} [options]
|
|
238
|
+
@returns {Token | Comment | null}
|
|
239
|
+
*/
|
|
240
|
+
getTokenAfter(nodeOrToken, {includeComments = false} = {}) {
|
|
241
|
+
return this.#find(this.#indexAt(nodeOrToken.range[1]), 1, includeComments);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
The first token inside a node.
|
|
246
|
+
|
|
247
|
+
@param {Node} node
|
|
248
|
+
@param {{includeComments?: boolean}} [options]
|
|
249
|
+
@returns {Token | Comment | null}
|
|
250
|
+
*/
|
|
251
|
+
getFirstToken(node, {includeComments = false} = {}) {
|
|
252
|
+
return this.#find(this.#indexAt(node.range[0]), 1, includeComments, item => item.range[1] <= node.range[1]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
The last token inside a node.
|
|
257
|
+
|
|
258
|
+
@param {Node} node
|
|
259
|
+
@param {{includeComments?: boolean}} [options]
|
|
260
|
+
@returns {Token | Comment | null}
|
|
261
|
+
*/
|
|
262
|
+
getLastToken(node, {includeComments = false} = {}) {
|
|
263
|
+
return this.#find(this.#indexAt(node.range[1]) - 1, -1, includeComments, item => item.range[0] >= node.range[0]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
Every token inside a node, in source order.
|
|
268
|
+
|
|
269
|
+
@param {Node} node
|
|
270
|
+
@param {{includeComments?: boolean}} [options]
|
|
271
|
+
@returns {Array<Token | Comment>}
|
|
272
|
+
*/
|
|
273
|
+
getTokens(node, {includeComments = false} = {}) {
|
|
274
|
+
const tokens = this.#items.slice(this.#indexAt(node.range[0]), this.#indexAt(node.range[1]));
|
|
275
|
+
return includeComments ? tokens : tokens.filter(item => !isComment(item));
|
|
276
|
+
}
|
|
277
|
+
}
|
package/license
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-soml",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "ESLint plugin for SOML, a config format for humans",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": "github:sindresorhus/eslint-soml",
|
|
7
|
+
"funding": "https://github.com/sponsors/sindresorhus",
|
|
8
|
+
"author": "Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"create-rule": "node ./scripts/create-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
|
|
20
|
+
"create-rules-index-file": "node ./scripts/create-rules-index-file.js",
|
|
21
|
+
"fix": "run-p --continue-on-error \"fix:*\"",
|
|
22
|
+
"fix:eslint-docs": "eslint-doc-generator",
|
|
23
|
+
"fix:js": "eslint --fix",
|
|
24
|
+
"fix:markdown": "markdownlint --fix \"**/*.md\"",
|
|
25
|
+
"fix:snapshots": "node --test --test-update-snapshots \"test/*.js\"",
|
|
26
|
+
"lint": "run-p --continue-on-error \"lint:*\"",
|
|
27
|
+
"lint:eslint-docs": "eslint-doc-generator --check",
|
|
28
|
+
"lint:js": "eslint",
|
|
29
|
+
"lint:markdown": "markdownlint \"**/*.md\"",
|
|
30
|
+
"rename-rule": "node ./scripts/rename-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
|
|
31
|
+
"sync-conformance": "node ./scripts/sync-conformance.js",
|
|
32
|
+
"test": "run-s --continue-on-error lint test:js test:types",
|
|
33
|
+
"test:js": "node --test \"test/*.js\"",
|
|
34
|
+
"test:types": "tsc --project test/types/tsconfig.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"index.js",
|
|
38
|
+
"index.d.ts",
|
|
39
|
+
"language",
|
|
40
|
+
"rules"
|
|
41
|
+
],
|
|
42
|
+
"keywords": [
|
|
43
|
+
"eslint",
|
|
44
|
+
"eslintplugin",
|
|
45
|
+
"eslint-plugin",
|
|
46
|
+
"soml",
|
|
47
|
+
"config",
|
|
48
|
+
"configuration",
|
|
49
|
+
"linter",
|
|
50
|
+
"lint",
|
|
51
|
+
"style"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@eslint/plugin-kit": "^0.7.3",
|
|
55
|
+
"natural-compare": "^1.4.0",
|
|
56
|
+
"soml-lang": "^0.0.2"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@babel/code-frame": "^8.0.6",
|
|
60
|
+
"@eslint-community/eslint-utils": "^4.10.1",
|
|
61
|
+
"@eslint/core": "^1.2.1",
|
|
62
|
+
"@typescript-eslint/types": "^8.71.0",
|
|
63
|
+
"enquirer": "^2.4.1",
|
|
64
|
+
"eslint": "^10.12.0",
|
|
65
|
+
"eslint-config-xo": "^4.0.1",
|
|
66
|
+
"eslint-doc-generator": "^3.7.1",
|
|
67
|
+
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
68
|
+
"eslint-plugin-jsdoc": "^65.0.2",
|
|
69
|
+
"globals": "^17.13.0",
|
|
70
|
+
"indent-string": "^5.0.0",
|
|
71
|
+
"markdownlint-cli": "^0.49.1",
|
|
72
|
+
"nano-spawn": "^2.1.0",
|
|
73
|
+
"node-style-text": "^2.1.2",
|
|
74
|
+
"npm-run-all2": "^9.0.3",
|
|
75
|
+
"open-editor": "^6.0.0",
|
|
76
|
+
"outdent": "^0.8.0",
|
|
77
|
+
"typescript": "^6.0.3",
|
|
78
|
+
"yaml": "^2.9.1"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"eslint": ">=10.4"
|
|
82
|
+
}
|
|
83
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# eslint-soml
|
|
2
|
+
|
|
3
|
+
> ESLint plugin for [SOML](https://github.com/soml-lang/soml), a config format for humans
|
|
4
|
+
|
|
5
|
+
Lint `.soml` files with ESLint. The plugin parses them with [`soml-lang`](https://github.com/sindresorhus/soml-js), the reference parser, so ESLint reports every syntax error the specification defines, and `# eslint-disable` comments work as in JavaScript.
|
|
6
|
+
|
|
7
|
+
[**Propose a new rule ➡**](.github/contributing.md)
|
|
8
|
+
|
|
9
|
+
*We do not accept pull requests because of too much AI slop.*
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install --save-dev eslint eslint-soml
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Requires Node.js 26, ESLint `>=10.4`, [flat config](https://eslint.org/docs/latest/use/configure/configuration-files), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm).**
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Use a [preset config](#preset-configs):
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import soml from 'eslint-soml';
|
|
25
|
+
import {defineConfig} from 'eslint/config';
|
|
26
|
+
|
|
27
|
+
export default defineConfig([
|
|
28
|
+
soml.configs.recommended,
|
|
29
|
+
]);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or configure each rule. Then set `language: 'soml/soml'`, as shown below.
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import soml from 'eslint-soml';
|
|
36
|
+
import {defineConfig} from 'eslint/config';
|
|
37
|
+
|
|
38
|
+
export default defineConfig([
|
|
39
|
+
{
|
|
40
|
+
files: ['**/*.soml'],
|
|
41
|
+
plugins: {
|
|
42
|
+
soml,
|
|
43
|
+
},
|
|
44
|
+
language: 'soml/soml',
|
|
45
|
+
rules: {
|
|
46
|
+
'soml/prefer-literal-strings': 'error',
|
|
47
|
+
'soml/…': 'error',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Disable a rule with a comment, in either comment style:
|
|
54
|
+
|
|
55
|
+
```soml
|
|
56
|
+
# eslint-disable-next-line soml/prefer-bare-keys
|
|
57
|
+
'name': 'soml'
|
|
58
|
+
|
|
59
|
+
'404': 'Not found' /* eslint-disable-line soml/prefer-bare-keys */
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Rules
|
|
63
|
+
|
|
64
|
+
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` -->
|
|
65
|
+
<!-- begin auto-generated rules list -->
|
|
66
|
+
|
|
67
|
+
💼 [Configurations](https://github.com/sindresorhus/eslint-soml#preset-configs) enabled in.\
|
|
68
|
+
✅ Set in the `recommended` [configuration](https://github.com/sindresorhus/eslint-soml#preset-configs).\
|
|
69
|
+
☑️ Set in the `unopinionated` [configuration](https://github.com/sindresorhus/eslint-soml#preset-configs).\
|
|
70
|
+
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
71
|
+
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
72
|
+
|
|
73
|
+
| Name | Description | 💼 | 🔧 | 💡 |
|
|
74
|
+
| :--------------------------------------------------------------------------------------- | :------------------------------------------------------------ | :--- | :- | :- |
|
|
75
|
+
| [format](docs/rules/format.md) | Enforce the layout of the SOML formatter. | ✅ | 🔧 | |
|
|
76
|
+
| [key-name-casing](docs/rules/key-name-casing.md) | Enforce a casing style for keys. | | | |
|
|
77
|
+
| [no-block-string-trailing-whitespace](docs/rules/no-block-string-trailing-whitespace.md) | Disallow trailing whitespace in block strings. | ✅ ☑️ | | |
|
|
78
|
+
| [no-empty-keys](docs/rules/no-empty-keys.md) | Disallow empty keys. | ✅ ☑️ | | |
|
|
79
|
+
| [no-invisible-characters](docs/rules/no-invisible-characters.md) | Disallow invisible characters in strings, keys, and comments. | ✅ ☑️ | 🔧 | |
|
|
80
|
+
| [no-loss-of-precision](docs/rules/no-loss-of-precision.md) | Disallow floats that cannot be stored exactly as written. | ✅ ☑️ | | |
|
|
81
|
+
| [no-mixed-number-types](docs/rules/no-mixed-number-types.md) | Disallow arrays that mix ints and floats. | ✅ | | 💡 |
|
|
82
|
+
| [no-numeric-dotted-keys](docs/rules/no-numeric-dotted-keys.md) | Disallow dotted keys that read as one number. | ✅ ☑️ | | 💡 |
|
|
83
|
+
| [no-unnormalized-keys](docs/rules/no-unnormalized-keys.md) | Disallow keys that are not in a Unicode normalization form. | ✅ ☑️ | | |
|
|
84
|
+
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the grouping of digits with `_` in decimal numbers. | ✅ | 🔧 | |
|
|
85
|
+
| [prefer-bare-keys](docs/rules/prefer-bare-keys.md) | Prefer bare keys when a key does not need quotes. | ✅ | 🔧 | |
|
|
86
|
+
| [prefer-literal-strings](docs/rules/prefer-literal-strings.md) | Prefer literal strings, `'…'`, when a string has no escapes. | ✅ | 🔧 | |
|
|
87
|
+
| [prefer-utc-instants](docs/rules/prefer-utc-instants.md) | Prefer instants in UTC. | | 🔧 | |
|
|
88
|
+
| [sort-keys](docs/rules/sort-keys.md) | Require object keys to be sorted. | | 🔧 | |
|
|
89
|
+
| [spaced-comment](docs/rules/spaced-comment.md) | Require a space after `#` and `/*`, and before `*/`. | ✅ | 🔧 | |
|
|
90
|
+
|
|
91
|
+
<!-- end auto-generated rules list -->
|
|
92
|
+
|
|
93
|
+
## Preset configs
|
|
94
|
+
|
|
95
|
+
See the [ESLint docs](https://eslint.org/docs/latest/use/configure/configuration-files) for more information about extending config files.
|
|
96
|
+
|
|
97
|
+
**Note**: The preset configs apply to `**/*.soml` files. They also set `language: 'soml/soml'`.
|
|
98
|
+
|
|
99
|
+
### Recommended config
|
|
100
|
+
|
|
101
|
+
This plugin exports a `recommended` config that enforces good practices.
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import soml from 'eslint-soml';
|
|
105
|
+
import {defineConfig} from 'eslint/config';
|
|
106
|
+
|
|
107
|
+
export default defineConfig([
|
|
108
|
+
// …
|
|
109
|
+
soml.configs.recommended,
|
|
110
|
+
{
|
|
111
|
+
files: ['**/*.soml'],
|
|
112
|
+
rules: {
|
|
113
|
+
'soml/sort-keys': 'error',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Unopinionated config
|
|
120
|
+
|
|
121
|
+
This plugin exports an `unopinionated` config with only the rules that catch bugs. Every syntax error is reported by every config, because it comes from the parser.
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import soml from 'eslint-soml';
|
|
125
|
+
import {defineConfig} from 'eslint/config';
|
|
126
|
+
|
|
127
|
+
export default defineConfig([
|
|
128
|
+
// …
|
|
129
|
+
soml.configs.unopinionated,
|
|
130
|
+
]);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### All config
|
|
134
|
+
|
|
135
|
+
This plugin exports an `all` config that enables every rule.
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
import soml from 'eslint-soml';
|
|
139
|
+
import {defineConfig} from 'eslint/config';
|
|
140
|
+
|
|
141
|
+
export default defineConfig([
|
|
142
|
+
// …
|
|
143
|
+
soml.configs.all,
|
|
144
|
+
{
|
|
145
|
+
files: ['**/*.soml'],
|
|
146
|
+
rules: {
|
|
147
|
+
'soml/prefer-utc-instants': 'off',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Editor integration
|
|
154
|
+
|
|
155
|
+
In Visual Studio Code, the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) only checks the languages listed in its `eslint.validate` setting. Add `soml`. This needs an extension that registers the `soml` language for `.soml` files.
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"eslint.validate": [
|
|
160
|
+
"soml"
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Writing rules
|
|
166
|
+
|
|
167
|
+
Rules get the tree from [`parseTree()`](https://github.com/sindresorhus/soml-js#parsetreetext) in `soml-lang`. Its readme lists every node type. The node types are exported as TypeScript types from this package too.
|
|
168
|
+
|
|
169
|
+
`context.sourceCode` has `getParent()`, `getTokenBefore()`, `getTokenAfter()`, `getFirstToken()`, `getLastToken()`, and `getTokens()`, each token method with an `includeComments` option, and `getAllComments()`.
|
|
170
|
+
|
|
171
|
+
## Related
|
|
172
|
+
|
|
173
|
+
- [soml-lang](https://github.com/sindresorhus/soml-js) - The reference parser and canonical serializer for SOML
|
|
174
|
+
- [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) - More than 300 powerful ESLint rules
|
|
175
|
+
- [eslint-package-json](https://github.com/sindresorhus/eslint-package-json) - Powerful ESLint rules for `package.json`
|