eslint-soml 0.0.3 β 0.0.5
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 +209 -0
- package/index.js +1 -0
- package/language/soml-source-code.js +102 -2
- package/package.json +8 -8
- package/readme.md +120 -31
- package/rules/index.js +5 -0
- package/rules/no-block-string-edge-blank-lines.js +4 -8
- package/rules/no-block-string-trailing-whitespace.js +6 -11
- package/rules/no-confusable-keys.js +20 -8
- package/rules/no-decimal-file-modes.js +8 -5
- package/rules/no-dotted-key-collisions.js +66 -0
- package/rules/no-duplicate-array-items.js +3 -3
- package/rules/no-escapes-in-literal-strings.js +77 -0
- package/rules/no-escapes-in-paths.js +5 -6
- package/rules/no-invisible-characters.js +8 -2
- package/rules/no-loss-of-precision.js +1 -1
- package/rules/no-misplaced-block-string-delimiters.js +56 -40
- package/rules/no-quoted-values-in-arrays.js +120 -0
- package/rules/no-sizes-as-durations.js +2 -2
- package/rules/no-surrounding-whitespace-in-keys.js +53 -0
- package/rules/prefer-block-strings.js +129 -0
- package/rules/prefer-literal-strings.js +4 -4
- package/rules/rule/to-eslint-problem.js +1 -0
- package/rules/sort-keys.js +10 -8
- package/rules/spaced-comment.js +2 -2
- package/rules/utils/escape-string.js +12 -4
- package/rules/utils/get-key-words.js +2 -2
- package/rules/utils/get-lines.js +24 -0
- package/rules/utils/get-meant-escape.js +46 -0
- package/rules/utils/index.js +2 -0
- package/rules/utils/invisible-characters.js +3 -2
- package/rules/utils/is-disabled-anywhere.js +1 -1
- package/rules/utils/reorder-members.js +9 -3
package/index.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import type {ESLint, Linter} from 'eslint';
|
|
2
|
+
import type {LanguageOptions, RuleVisitor} from '@eslint/core';
|
|
3
|
+
import type {
|
|
4
|
+
CustomRuleDefinitionType,
|
|
5
|
+
CustomRuleTypeDefinitions,
|
|
6
|
+
CustomRuleVisitorWithExit,
|
|
7
|
+
TextSourceCodeBase,
|
|
8
|
+
} from '@eslint/plugin-kit';
|
|
9
|
+
import type {
|
|
10
|
+
Node,
|
|
11
|
+
DocumentNode,
|
|
12
|
+
ObjectNode,
|
|
13
|
+
MemberNode,
|
|
14
|
+
ArrayNode,
|
|
15
|
+
KeyNode,
|
|
16
|
+
StringNode,
|
|
17
|
+
IntegerNode,
|
|
18
|
+
FloatNode,
|
|
19
|
+
BooleanNode,
|
|
20
|
+
NullNode,
|
|
21
|
+
InstantNode,
|
|
22
|
+
DurationNode,
|
|
23
|
+
Token,
|
|
24
|
+
Comment,
|
|
25
|
+
} from 'soml-lang';
|
|
2
26
|
|
|
3
27
|
export type {
|
|
4
28
|
Node,
|
|
@@ -22,6 +46,191 @@ export type {
|
|
|
22
46
|
SourceLocation,
|
|
23
47
|
} from 'soml-lang';
|
|
24
48
|
|
|
49
|
+
/**
|
|
50
|
+
A node, a token, or a comment of a SOML document.
|
|
51
|
+
*/
|
|
52
|
+
export type SomlSyntaxElement = Node | Token | Comment;
|
|
53
|
+
|
|
54
|
+
type TokenOptions = {
|
|
55
|
+
/**
|
|
56
|
+
Whether comments count as tokens.
|
|
57
|
+
|
|
58
|
+
@default false
|
|
59
|
+
*/
|
|
60
|
+
readonly includeComments?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type TokenOptionsWithoutComments = TokenOptions & {readonly includeComments?: false};
|
|
64
|
+
type TokenOptionsWithComments = TokenOptions & {readonly includeComments: true};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
The source code of a SOML document, which a rule gets as `context.sourceCode`.
|
|
68
|
+
|
|
69
|
+
It has the methods of `TextSourceCodeBase` from `@eslint/plugin-kit`, such as `getText()`, `getLoc()`, `getRange()`, and `getLocFromIndex()`, and the methods below. Token methods find tokens in source order and take only the `includeComments` option. Single-token methods return `null` when there is no token; `getTokens()` returns an array.
|
|
70
|
+
|
|
71
|
+
@example
|
|
72
|
+
```
|
|
73
|
+
import type {SomlRuleDefinition} from 'eslint-soml';
|
|
74
|
+
|
|
75
|
+
const rule: SomlRuleDefinition = {
|
|
76
|
+
meta: {
|
|
77
|
+
type: 'problem',
|
|
78
|
+
messages: {
|
|
79
|
+
trailingComma: 'Remove the comma after the last item.',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
create(context) {
|
|
83
|
+
return {
|
|
84
|
+
Array(node) {
|
|
85
|
+
const lastToken = context.sourceCode.getLastToken(node);
|
|
86
|
+
const beforeLast = lastToken && context.sourceCode.getTokenBefore(lastToken);
|
|
87
|
+
|
|
88
|
+
if (beforeLast?.value === ',') {
|
|
89
|
+
context.report({loc: beforeLast.loc, messageId: 'trailingComma'});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
*/
|
|
97
|
+
export interface SomlSourceCode extends TextSourceCodeBase<{
|
|
98
|
+
LangOptions: LanguageOptions;
|
|
99
|
+
RootNode: DocumentNode;
|
|
100
|
+
SyntaxElementWithLoc: SomlSyntaxElement;
|
|
101
|
+
ConfigNode: Comment;
|
|
102
|
+
}> {
|
|
103
|
+
/**
|
|
104
|
+
Marks the source code as SOML, so a rule that works on several languages can tell, as with `context.sourceCode.parserServices?.isSOML`.
|
|
105
|
+
*/
|
|
106
|
+
readonly parserServices: {readonly isSOML: true};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
The parent of a node, or `undefined` for the `Document` node. Nodes have no `parent` property.
|
|
110
|
+
*/
|
|
111
|
+
getParent(node: Node): Node | undefined;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
Every ancestor node, from the root to the parent. Tokens and comments have no ancestors.
|
|
115
|
+
|
|
116
|
+
@example
|
|
117
|
+
```
|
|
118
|
+
const parents = context.sourceCode.getAncestors(node).map(ancestor => context.sourceCode.getParent(ancestor));
|
|
119
|
+
```
|
|
120
|
+
*/
|
|
121
|
+
getAncestors(node: SomlSyntaxElement): Node[];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
Every comment, in source order.
|
|
125
|
+
*/
|
|
126
|
+
getAllComments(): readonly Comment[];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
The token before a node, token, or comment.
|
|
130
|
+
*/
|
|
131
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options?: TokenOptionsWithoutComments): Token | null;
|
|
132
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options: TokenOptionsWithComments): Token | Comment | null;
|
|
133
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options?: TokenOptions): Token | Comment | null;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
The token after a node, token, or comment.
|
|
137
|
+
*/
|
|
138
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options?: TokenOptionsWithoutComments): Token | null;
|
|
139
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options: TokenOptionsWithComments): Token | Comment | null;
|
|
140
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options?: TokenOptions): Token | Comment | null;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
The first token inside a node.
|
|
144
|
+
*/
|
|
145
|
+
getFirstToken(node: Node, options?: TokenOptionsWithoutComments): Token | null;
|
|
146
|
+
getFirstToken(node: Node, options: TokenOptionsWithComments): Token | Comment | null;
|
|
147
|
+
getFirstToken(node: Node, options?: TokenOptions): Token | Comment | null;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
The last token inside a node.
|
|
151
|
+
*/
|
|
152
|
+
getLastToken(node: Node, options?: TokenOptionsWithoutComments): Token | null;
|
|
153
|
+
getLastToken(node: Node, options: TokenOptionsWithComments): Token | Comment | null;
|
|
154
|
+
getLastToken(node: Node, options?: TokenOptions): Token | Comment | null;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
Every token inside a node, in source order.
|
|
158
|
+
*/
|
|
159
|
+
getTokens(node: Node, options?: TokenOptionsWithoutComments): Token[];
|
|
160
|
+
getTokens(node: Node, options: TokenOptionsWithComments): Array<Token | Comment>;
|
|
161
|
+
getTokens(node: Node, options?: TokenOptions): Array<Token | Comment>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type ValueParent = MemberNode | ArrayNode;
|
|
165
|
+
type CollectionParent = DocumentNode | ValueParent;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
The listeners that a rule for SOML returns, with the type of each node and of its parent.
|
|
169
|
+
*/
|
|
170
|
+
export interface SomlRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
|
171
|
+
Document?(node: DocumentNode, parent: undefined): void;
|
|
172
|
+
Object?(node: ObjectNode, parent: CollectionParent): void;
|
|
173
|
+
Member?(node: MemberNode, parent: ObjectNode): void;
|
|
174
|
+
Key?(node: KeyNode, parent: MemberNode): void;
|
|
175
|
+
Array?(node: ArrayNode, parent: CollectionParent): void;
|
|
176
|
+
String?(node: StringNode, parent: ValueParent): void;
|
|
177
|
+
Integer?(node: IntegerNode, parent: ValueParent): void;
|
|
178
|
+
Float?(node: FloatNode, parent: ValueParent): void;
|
|
179
|
+
Boolean?(node: BooleanNode, parent: ValueParent): void;
|
|
180
|
+
Null?(node: NullNode, parent: ValueParent): void;
|
|
181
|
+
Instant?(node: InstantNode, parent: ValueParent): void;
|
|
182
|
+
Duration?(node: DurationNode, parent: ValueParent): void;
|
|
183
|
+
}> {}
|
|
184
|
+
|
|
185
|
+
export type SomlRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
The type of a rule for SOML files, which gets a `SomlSourceCode` and returns a `SomlRuleVisitor`.
|
|
189
|
+
|
|
190
|
+
@example
|
|
191
|
+
```
|
|
192
|
+
import type {SomlRuleDefinition} from 'eslint-soml';
|
|
193
|
+
|
|
194
|
+
const rule: SomlRuleDefinition = {
|
|
195
|
+
meta: {
|
|
196
|
+
type: 'suggestion',
|
|
197
|
+
messages: {
|
|
198
|
+
tooLong: 'The key `{{key}}` is longer than 40 characters.',
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
create(context) {
|
|
202
|
+
return {
|
|
203
|
+
Key(node) {
|
|
204
|
+
if (node.value.length > 40) {
|
|
205
|
+
context.report({node, messageId: 'tooLong', data: {key: node.value}});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
```
|
|
212
|
+
*/
|
|
213
|
+
export type SomlRuleDefinition<Options extends Partial<SomlRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
|
|
214
|
+
LangOptions: LanguageOptions;
|
|
215
|
+
Code: SomlSourceCode;
|
|
216
|
+
Visitor: SomlRuleVisitor;
|
|
217
|
+
Node: SomlSyntaxElement;
|
|
218
|
+
}, Options>;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
The `context` of a rule for SOML files, for a helper that a rule passes it to.
|
|
222
|
+
|
|
223
|
+
@example
|
|
224
|
+
```
|
|
225
|
+
import type {SomlRuleContext, StringNode} from 'eslint-soml';
|
|
226
|
+
|
|
227
|
+
function isWrittenEscaped(context: SomlRuleContext, node: StringNode): boolean {
|
|
228
|
+
return context.sourceCode.getText(node).startsWith('"');
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
*/
|
|
232
|
+
export type SomlRuleContext<Options extends Partial<SomlRuleDefinitionTypeOptions> = {}> = Parameters<SomlRuleDefinition<Options>['create']>[0];
|
|
233
|
+
|
|
25
234
|
declare const eslintSoml: ESLint.Plugin & {
|
|
26
235
|
configs: {
|
|
27
236
|
recommended: Linter.Config;
|
package/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const soml = {
|
|
|
28
28
|
meta: {
|
|
29
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
30
|
name: 'soml',
|
|
31
|
+
namespace: 'soml',
|
|
31
32
|
version: packageJson.version,
|
|
32
33
|
},
|
|
33
34
|
languages: {
|
|
@@ -13,6 +13,15 @@ import {visitorKeys} from 'soml-lang';
|
|
|
13
13
|
|
|
14
14
|
const commentParser = new ConfigCommentParser();
|
|
15
15
|
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/v;
|
|
16
|
+
const RULE_CONFIG = /^\s*eslint(?:\s|$)/v;
|
|
17
|
+
// What separates a directive from its justification, as ESLint splits it.
|
|
18
|
+
const JUSTIFICATION = /\s-{2,}\s/v;
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Whether a comment is a directive. A `#` comment is the usual comment style, so one that starts with `eslint` in prose, as in `# eslint version to install`, is not a rule configuration. A rule configuration always has a `:`, so a `#` comment without one before its justification is not a directive.
|
|
22
|
+
*/
|
|
23
|
+
const isInlineConfig = comment => INLINE_CONFIG.test(comment.value)
|
|
24
|
+
&& !(comment.type === 'Line' && RULE_CONFIG.test(comment.value) && !comment.value.split(JUSTIFICATION, 1)[0].includes(':'));
|
|
16
25
|
|
|
17
26
|
const isComment = item => item.type === 'Line' || item.type === 'Block';
|
|
18
27
|
|
|
@@ -58,10 +67,20 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
58
67
|
*/
|
|
59
68
|
#inlineConfigComments;
|
|
60
69
|
/**
|
|
70
|
+
Where each line starts, found once, when it is first needed.
|
|
71
|
+
|
|
72
|
+
@type {number[] | undefined}
|
|
73
|
+
*/
|
|
74
|
+
#lineStarts;
|
|
75
|
+
/**
|
|
61
76
|
@type {DocumentNode}
|
|
62
77
|
*/
|
|
63
78
|
ast;
|
|
64
79
|
visitorKeys = visitorKeys;
|
|
80
|
+
/**
|
|
81
|
+
Marks the source code as SOML, so a rule that works on several languages can tell, as `eslint-plugin-yml` and `eslint-plugin-toml` do with `isYAML` and `isTOML`.
|
|
82
|
+
*/
|
|
83
|
+
parserServices = {isSOML: true};
|
|
65
84
|
|
|
66
85
|
/**
|
|
67
86
|
@param {{text: string, ast: DocumentNode}} options
|
|
@@ -113,6 +132,86 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
113
132
|
return null;
|
|
114
133
|
}
|
|
115
134
|
|
|
135
|
+
/**
|
|
136
|
+
The offsets where each line starts, shared by both location conversions.
|
|
137
|
+
|
|
138
|
+
@returns {number[]}
|
|
139
|
+
*/
|
|
140
|
+
#getLineStarts() {
|
|
141
|
+
if (!this.#lineStarts) {
|
|
142
|
+
this.#lineStarts = [0];
|
|
143
|
+
|
|
144
|
+
for (let lineFeed = this.text.indexOf('\n'); lineFeed !== -1; lineFeed = this.text.indexOf('\n', lineFeed + 1)) {
|
|
145
|
+
this.#lineStarts.push(lineFeed + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return this.#lineStarts;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
The 1-based line and 0-based column of an offset.
|
|
154
|
+
|
|
155
|
+
This replaces the method of `TextSourceCodeBase`, which searches the rest of the text for a line feed again for every offset on the last line of a file with no line feed at the end, so a rule with many problems on that line takes quadratic time.
|
|
156
|
+
|
|
157
|
+
@param {number} index
|
|
158
|
+
@returns {{line: number, column: number}}
|
|
159
|
+
*/
|
|
160
|
+
getLocFromIndex(index) {
|
|
161
|
+
if (typeof index !== 'number') {
|
|
162
|
+
throw new TypeError('Expected `index` to be a number.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (index < 0 || index > this.text.length) {
|
|
166
|
+
throw new RangeError(`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The last line that starts at or before `index`.
|
|
170
|
+
const lineStarts = this.#getLineStarts();
|
|
171
|
+
let low = 0;
|
|
172
|
+
let high = lineStarts.length - 1;
|
|
173
|
+
|
|
174
|
+
while (low < high) {
|
|
175
|
+
const middle = Math.ceil((low + high) / 2);
|
|
176
|
+
|
|
177
|
+
if (lineStarts[middle] <= index) {
|
|
178
|
+
low = middle;
|
|
179
|
+
} else {
|
|
180
|
+
high = middle - 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {line: low + 1, column: index - lineStarts[low]};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
The offset of a 1-based line and 0-based column. Use the cached line starts so repeated locations on the last line do not search for a line feed again.
|
|
189
|
+
|
|
190
|
+
@param {{line: number, column: number}} location
|
|
191
|
+
@returns {number}
|
|
192
|
+
*/
|
|
193
|
+
getIndexFromLoc(location) {
|
|
194
|
+
if (location === null || typeof location !== 'object' || typeof location.line !== 'number' || typeof location.column !== 'number') {
|
|
195
|
+
throw new TypeError('Expected `loc` to be an object with numeric `line` and `column` properties.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const lineStarts = this.#getLineStarts();
|
|
199
|
+
if (location.line < 1 || location.line > lineStarts.length) {
|
|
200
|
+
throw new RangeError(`Line number out of range (line ${location.line} requested). Valid range: 1-${lineStarts.length}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lineStartIndex = lineStarts[location.line - 1];
|
|
204
|
+
const isLastLine = location.line === lineStarts.length;
|
|
205
|
+
const lineEndIndex = isLastLine ? this.text.length : lineStarts[location.line];
|
|
206
|
+
const positionIndex = lineStartIndex + location.column;
|
|
207
|
+
|
|
208
|
+
if (location.column < 0 || (isLastLine ? positionIndex > lineEndIndex : positionIndex >= lineEndIndex)) {
|
|
209
|
+
throw new RangeError(`Column number out of range (column ${location.column} requested). Valid range for line ${location.line}: 0-${lineEndIndex - lineStartIndex + (isLastLine ? 0 : -1)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return positionIndex;
|
|
213
|
+
}
|
|
214
|
+
|
|
116
215
|
/**
|
|
117
216
|
@returns {Iterable<VisitNodeStep>}
|
|
118
217
|
*/
|
|
@@ -161,7 +260,8 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
161
260
|
return this.#parents.get(node);
|
|
162
261
|
}
|
|
163
262
|
|
|
164
|
-
//
|
|
263
|
+
// When ESLint ships https://github.com/eslint/rfcs/pull/150, add a `comments` property holding `this.ast.comments` and use it in the rules. The new API deprecates `getAllComments()` and enables `no-irregular-whitespace` across languages.
|
|
264
|
+
|
|
165
265
|
/**
|
|
166
266
|
@returns {readonly Comment[]}
|
|
167
267
|
*/
|
|
@@ -173,7 +273,7 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
173
273
|
@returns {Comment[]}
|
|
174
274
|
*/
|
|
175
275
|
getInlineConfigNodes() {
|
|
176
|
-
this.#inlineConfigComments ??= this.ast.comments.filter(comment =>
|
|
276
|
+
this.#inlineConfigComments ??= this.ast.comments.filter(comment => isInlineConfig(comment));
|
|
177
277
|
return this.#inlineConfigComments;
|
|
178
278
|
}
|
|
179
279
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-soml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "ESLint plugin for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:soml-lang/eslint-soml",
|
|
@@ -51,22 +51,22 @@
|
|
|
51
51
|
"style"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@eslint/core": "^1.2.1",
|
|
54
55
|
"@eslint/plugin-kit": "^0.7.3",
|
|
55
|
-
"soml-lang": "^0.0.
|
|
56
|
+
"soml-lang": "^0.0.7"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@babel/code-frame": "^8.0.6",
|
|
59
60
|
"@eslint-community/eslint-utils": "^4.10.1",
|
|
60
|
-
"@eslint/
|
|
61
|
-
"@typescript-eslint/types": "^8.71.1",
|
|
61
|
+
"@typescript-eslint/types": "^8.71.2",
|
|
62
62
|
"enquirer": "^2.4.1",
|
|
63
|
-
"eslint": "^10.
|
|
64
|
-
"eslint-config-xo": "^
|
|
63
|
+
"eslint": "^10.13.0",
|
|
64
|
+
"eslint-config-xo": "^7.0.0",
|
|
65
65
|
"eslint-doc-generator": "^3.7.1",
|
|
66
66
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
67
|
-
"eslint-plugin-jsdoc": "^65.
|
|
67
|
+
"eslint-plugin-jsdoc": "^65.3.11",
|
|
68
68
|
"indent-string": "^5.0.0",
|
|
69
|
-
"markdownlint-cli": "^0.
|
|
69
|
+
"markdownlint-cli": "^0.50.0",
|
|
70
70
|
"nano-spawn": "^2.1.0",
|
|
71
71
|
"node-style-text": "^2.1.2",
|
|
72
72
|
"npm-run-all2": "^9.0.3",
|
package/readme.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install --save-dev eslint eslint-soml
|
|
|
16
16
|
|
|
17
17
|
**Requires Node.js 22.13 or later, 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
18
|
|
|
19
|
-
The
|
|
19
|
+
The rules of this plugin do not need `Temporal`, so they work on every supported Node.js version, also for files with instants and durations. Only reading the `value` of an `Instant` or `Duration` node needs it, as in your own rule or in a `no-restricted-syntax` selector that matches on `value`, such as `[value=1]`. Node.js 26 and later have `Temporal` built in. On older versions, load a [`Temporal` polyfill](https://github.com/fullcalendar/temporal-polyfill) first: make `import 'temporal-polyfill/global';` the first import in `eslint.config.js`, before `eslint-soml`.
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
@@ -61,7 +61,7 @@ Disable a rule with a comment, in either comment style:
|
|
|
61
61
|
'404': 'Not found' /* eslint-disable-line soml/prefer-bare-keys */
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
The [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-disable-directives) and [`reportUnusedInlineConfigs`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-inline-configs) linter options work for `.soml` files, and so do the rules of [`@eslint-community/eslint-plugin-eslint-comments`](https://github.com/eslint-community/eslint-plugin-eslint-comments), such as `no-unlimited-disable`.
|
|
64
|
+
The [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-disable-directives) and [`reportUnusedInlineConfigs`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-inline-configs) linter options work for `.soml` files, and so do the rules of [`@eslint-community/eslint-plugin-eslint-comments`](https://github.com/eslint-community/eslint-plugin-eslint-comments), such as `no-unlimited-disable`. Those rules follow JavaScript, where a line comment holds only `eslint-disable-line` and `eslint-disable-next-line`, so some of them, such as `no-unlimited-disable` and `no-use`, do not check `# eslint-disable` and `# eslint-enable`. Write those as `/* β¦ */` comments to have them checked.
|
|
65
65
|
|
|
66
66
|
ESLint's [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) rule works too. For example, to disallow a key named `password`:
|
|
67
67
|
|
|
@@ -86,6 +86,86 @@ export default defineConfig([
|
|
|
86
86
|
]);
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
A selector can also check a value under a given key, which catches settings that are risky in production:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
'no-restricted-syntax': [
|
|
93
|
+
'error',
|
|
94
|
+
{
|
|
95
|
+
selector: 'Member:has(> Key[value=/^(allowed-origins|cors-origins?)$/]) String[value="*"]',
|
|
96
|
+
message: 'Allow only known origins.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
selector: 'Member:has(> Key[value=/^(verify-ssl|ssl-verify|tls-verify)$/]) > Boolean[value=false]',
|
|
100
|
+
message: 'Do not turn off TLS verification.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
selector: 'Member:has(> Key[value=/^(host|bind|listen)$/]) > String[value="0.0.0.0"]',
|
|
104
|
+
message: 'Bind to a specific address.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// Only at the top level.
|
|
108
|
+
selector: 'Document > Object > Member > Key[value="debug"]',
|
|
109
|
+
message: 'Set `debug` in the environment, not in the file.',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A selector can also say what the program that reads a file does not support. SOML allows each of these values, so use only the ones your program needs:
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
'no-restricted-syntax': [
|
|
118
|
+
'error',
|
|
119
|
+
{
|
|
120
|
+
// Such as a program that reads ints as JavaScript numbers.
|
|
121
|
+
selector: ':matches(Integer[value>9007199254740991], Integer[value<-9007199254740991])',
|
|
122
|
+
message: 'This program needs ints in the JavaScript safe integer range.',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
// Such as a program that writes the value as JSON, where `infinity` becomes `null`.
|
|
126
|
+
selector: ':matches(Float[value="Infinity"], Float[value="-Infinity"])',
|
|
127
|
+
message: 'This program needs finite numbers.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
// Such as a program that uses the value as a file name.
|
|
131
|
+
selector: ':matches(String, Key)[value=/\\u0000/]',
|
|
132
|
+
message: 'This program does not support the NUL character.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
// Such as a program that merges the value into an object with `Object.assign()`.
|
|
136
|
+
selector: 'Key[value="__proto__"]',
|
|
137
|
+
message: 'This program does not support the key `__proto__`.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
selector: ':matches(Instant, Duration)',
|
|
141
|
+
message: 'This program does not support instants and durations.',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### SOML in Markdown
|
|
147
|
+
|
|
148
|
+
To lint the `` ```soml `` code blocks in Markdown files, add the processor of [`@eslint/markdown`](https://github.com/eslint/markdown) before the preset. The blocks are linted as `.soml` files, and `--fix` writes the fixes back, also in an indented block:
|
|
149
|
+
|
|
150
|
+
```js
|
|
151
|
+
import markdown from '@eslint/markdown';
|
|
152
|
+
import soml from 'eslint-soml';
|
|
153
|
+
import {defineConfig} from 'eslint/config';
|
|
154
|
+
|
|
155
|
+
export default defineConfig([
|
|
156
|
+
{
|
|
157
|
+
files: ['**/*.md'],
|
|
158
|
+
plugins: {
|
|
159
|
+
markdown,
|
|
160
|
+
},
|
|
161
|
+
processor: 'markdown/markdown',
|
|
162
|
+
},
|
|
163
|
+
soml.configs.recommended,
|
|
164
|
+
]);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
A block that shows a mistake on purpose can be skipped with `<!-- eslint-skip -->` on the line before it.
|
|
168
|
+
|
|
89
169
|
## Rules
|
|
90
170
|
|
|
91
171
|
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` -->
|
|
@@ -97,35 +177,40 @@ export default defineConfig([
|
|
|
97
177
|
π§ Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
98
178
|
π‘ Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
99
179
|
|
|
100
|
-
| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β | Description
|
|
101
|
-
| :--------------------------------------------------------------------------------------------------- |
|
|
102
|
-
| [format](docs/rules/format.md) | Enforce the layout of the SOML formatter.
|
|
103
|
-
| [key-name-casing](docs/rules/key-name-casing.md) | Enforce a casing style for keys.
|
|
104
|
-
| [no-block-string-edge-blank-lines](docs/rules/no-block-string-edge-blank-lines.md) | Disallow blank lines at the start and end of block strings.
|
|
105
|
-
| [no-block-string-trailing-whitespace](docs/rules/no-block-string-trailing-whitespace.md) | Disallow trailing whitespace in block strings.
|
|
106
|
-
| [no-confusable-keys](docs/rules/no-confusable-keys.md) | Disallow keys that differ only in case,
|
|
107
|
-
| [no-decimal-file-modes](docs/rules/no-decimal-file-modes.md) | Disallow file modes written as decimal numbers.
|
|
108
|
-
| [no-
|
|
109
|
-
| [no-
|
|
110
|
-
| [no-
|
|
111
|
-
| [no-
|
|
112
|
-
| [no-
|
|
113
|
-
| [no-
|
|
114
|
-
| [no-
|
|
115
|
-
| [no-
|
|
116
|
-
| [no-
|
|
117
|
-
| [no-
|
|
118
|
-
| [no-
|
|
119
|
-
| [no-
|
|
120
|
-
| [no-
|
|
121
|
-
| [no-
|
|
122
|
-
| [no-
|
|
123
|
-
| [
|
|
124
|
-
| [
|
|
125
|
-
| [
|
|
126
|
-
| [
|
|
127
|
-
| [
|
|
128
|
-
| [
|
|
180
|
+
| NameΒ Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β Β | Description | πΌ | π§ | π‘ |
|
|
181
|
+
| :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------ | :--- | :- | :- |
|
|
182
|
+
| [format](docs/rules/format.md) | Enforce the layout of the SOML formatter. | β
| π§ | |
|
|
183
|
+
| [key-name-casing](docs/rules/key-name-casing.md) | Enforce a casing style for keys. | | | |
|
|
184
|
+
| [no-block-string-edge-blank-lines](docs/rules/no-block-string-edge-blank-lines.md) | Disallow blank lines at the start and end of block strings. | β
| π§ | |
|
|
185
|
+
| [no-block-string-trailing-whitespace](docs/rules/no-block-string-trailing-whitespace.md) | Disallow trailing whitespace in block strings. | β
βοΈ | | |
|
|
186
|
+
| [no-confusable-keys](docs/rules/no-confusable-keys.md) | Disallow keys that differ only in case, `-`, `_`, and `.`, or surrounding whitespace. | β
βοΈ | | |
|
|
187
|
+
| [no-decimal-file-modes](docs/rules/no-decimal-file-modes.md) | Disallow file modes written as decimal numbers. | β
βοΈ | | π‘ |
|
|
188
|
+
| [no-dotted-key-collisions](docs/rules/no-dotted-key-collisions.md) | Disallow quoted keys with a `.` that read as a path into an object next to them. | β
| | |
|
|
189
|
+
| [no-doubled-backslashes-in-literal-strings](docs/rules/no-doubled-backslashes-in-literal-strings.md) | Disallow doubled backslashes in literal strings, such as `'^\\d+$'`. | β
| | π‘ |
|
|
190
|
+
| [no-duplicate-array-items](docs/rules/no-duplicate-array-items.md) | Disallow the same string twice in an array. | | | π‘ |
|
|
191
|
+
| [no-empty-keys](docs/rules/no-empty-keys.md) | Disallow empty and whitespace-only keys. | β
βοΈ | | |
|
|
192
|
+
| [no-escapes-in-literal-strings](docs/rules/no-escapes-in-literal-strings.md) | Disallow escapes in literal strings, such as `'Line one\nLine two'`. | β
| | π‘ |
|
|
193
|
+
| [no-escapes-in-paths](docs/rules/no-escapes-in-paths.md) | Disallow `\n` and `\t` escapes in Windows paths. | β
βοΈ | | π‘ |
|
|
194
|
+
| [no-float-trailing-zeros](docs/rules/no-float-trailing-zeros.md) | Disallow trailing zeros in the fraction of a float. | β
| | π‘ |
|
|
195
|
+
| [no-invisible-characters](docs/rules/no-invisible-characters.md) | Disallow invisible characters in strings, keys, and comments. | β
βοΈ | π§ | |
|
|
196
|
+
| [no-loss-of-precision](docs/rules/no-loss-of-precision.md) | Disallow floats whose written digits are lost when they are stored. | β
βοΈ | | |
|
|
197
|
+
| [no-misplaced-block-string-delimiters](docs/rules/no-misplaced-block-string-delimiters.md) | Disallow a block string delimiter at the end of a content line. | β
βοΈ | | |
|
|
198
|
+
| [no-mixed-number-types](docs/rules/no-mixed-number-types.md) | Disallow arrays that mix ints and floats. | β
| | π‘ |
|
|
199
|
+
| [no-negative-zero](docs/rules/no-negative-zero.md) | Disallow a sign on zero. | β
| π§ | |
|
|
200
|
+
| [no-placeholder-values](docs/rules/no-placeholder-values.md) | Disallow placeholder values, such as `'changeme'`. | β
| | |
|
|
201
|
+
| [no-quoted-keywords](docs/rules/no-quoted-keywords.md) | Disallow the strings `'true'`, `'false'`, and `'null'`. | β
| | π‘ |
|
|
202
|
+
| [no-quoted-values-in-arrays](docs/rules/no-quoted-values-in-arrays.md) | Disallow quoted values in an array of that type, such as `'443'` in `[80, '443']`. | β
| | π‘ |
|
|
203
|
+
| [no-sizes-as-durations](docs/rules/no-sizes-as-durations.md) | Disallow sizes that are durations, such as `memory: 512m`. | β
βοΈ | | π‘ |
|
|
204
|
+
| [no-surrounding-whitespace-in-keys](docs/rules/no-surrounding-whitespace-in-keys.md) | Disallow whitespace at the start and end of keys. | β
| | |
|
|
205
|
+
| [no-unnormalized-keys](docs/rules/no-unnormalized-keys.md) | Disallow keys that are not in a Unicode normalization form. | β
βοΈ | | |
|
|
206
|
+
| [no-zero-duration-parts](docs/rules/no-zero-duration-parts.md) | Disallow duration parts that are zero. | β
| π§ | |
|
|
207
|
+
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the grouping of digits with `_` in decimal numbers. | β
| π§ | |
|
|
208
|
+
| [prefer-bare-keys](docs/rules/prefer-bare-keys.md) | Prefer bare keys when a key does not need quotes. | β
| π§ | |
|
|
209
|
+
| [prefer-block-strings](docs/rules/prefer-block-strings.md) | Prefer block strings for strings of three lines or more. | β
| π§ | |
|
|
210
|
+
| [prefer-literal-strings](docs/rules/prefer-literal-strings.md) | Prefer literal strings, `'β¦'`, when a string does not need escapes. | β
| π§ | |
|
|
211
|
+
| [prefer-utc-instants](docs/rules/prefer-utc-instants.md) | Prefer instants in UTC. | | π§ | |
|
|
212
|
+
| [sort-keys](docs/rules/sort-keys.md) | Require object keys to be sorted. | | π§ | |
|
|
213
|
+
| [spaced-comment](docs/rules/spaced-comment.md) | Require a space after `#` and `/*`, and before `*/`. | β
| π§ | |
|
|
129
214
|
|
|
130
215
|
<!-- end auto-generated rules list -->
|
|
131
216
|
|
|
@@ -201,6 +286,10 @@ In Visual Studio Code, the [ESLint extension](https://marketplace.visualstudio.c
|
|
|
201
286
|
}
|
|
202
287
|
```
|
|
203
288
|
|
|
289
|
+
## Writing rules
|
|
290
|
+
|
|
291
|
+
See [how to write rules for SOML files](docs/writing-rules.md).
|
|
292
|
+
|
|
204
293
|
## Related
|
|
205
294
|
|
|
206
295
|
- [soml-javascript](https://github.com/soml-lang/soml-javascript) - The reference parser and serializer for SOML
|
package/rules/index.js
CHANGED
|
@@ -6,9 +6,11 @@ export {default as 'no-block-string-edge-blank-lines'} from './no-block-string-e
|
|
|
6
6
|
export {default as 'no-block-string-trailing-whitespace'} from './no-block-string-trailing-whitespace.js';
|
|
7
7
|
export {default as 'no-confusable-keys'} from './no-confusable-keys.js';
|
|
8
8
|
export {default as 'no-decimal-file-modes'} from './no-decimal-file-modes.js';
|
|
9
|
+
export {default as 'no-dotted-key-collisions'} from './no-dotted-key-collisions.js';
|
|
9
10
|
export {default as 'no-doubled-backslashes-in-literal-strings'} from './no-doubled-backslashes-in-literal-strings.js';
|
|
10
11
|
export {default as 'no-duplicate-array-items'} from './no-duplicate-array-items.js';
|
|
11
12
|
export {default as 'no-empty-keys'} from './no-empty-keys.js';
|
|
13
|
+
export {default as 'no-escapes-in-literal-strings'} from './no-escapes-in-literal-strings.js';
|
|
12
14
|
export {default as 'no-escapes-in-paths'} from './no-escapes-in-paths.js';
|
|
13
15
|
export {default as 'no-float-trailing-zeros'} from './no-float-trailing-zeros.js';
|
|
14
16
|
export {default as 'no-invisible-characters'} from './no-invisible-characters.js';
|
|
@@ -18,11 +20,14 @@ export {default as 'no-mixed-number-types'} from './no-mixed-number-types.js';
|
|
|
18
20
|
export {default as 'no-negative-zero'} from './no-negative-zero.js';
|
|
19
21
|
export {default as 'no-placeholder-values'} from './no-placeholder-values.js';
|
|
20
22
|
export {default as 'no-quoted-keywords'} from './no-quoted-keywords.js';
|
|
23
|
+
export {default as 'no-quoted-values-in-arrays'} from './no-quoted-values-in-arrays.js';
|
|
21
24
|
export {default as 'no-sizes-as-durations'} from './no-sizes-as-durations.js';
|
|
25
|
+
export {default as 'no-surrounding-whitespace-in-keys'} from './no-surrounding-whitespace-in-keys.js';
|
|
22
26
|
export {default as 'no-unnormalized-keys'} from './no-unnormalized-keys.js';
|
|
23
27
|
export {default as 'no-zero-duration-parts'} from './no-zero-duration-parts.js';
|
|
24
28
|
export {default as 'numeric-separators-style'} from './numeric-separators-style.js';
|
|
25
29
|
export {default as 'prefer-bare-keys'} from './prefer-bare-keys.js';
|
|
30
|
+
export {default as 'prefer-block-strings'} from './prefer-block-strings.js';
|
|
26
31
|
export {default as 'prefer-literal-strings'} from './prefer-literal-strings.js';
|
|
27
32
|
export {default as 'prefer-utc-instants'} from './prefer-utc-instants.js';
|
|
28
33
|
export {default as 'sort-keys'} from './sort-keys.js';
|