eslint-soml 0.0.4 → 0.0.6
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 +25 -2
- package/index.js +1 -0
- package/language/soml-source-code.js +47 -10
- package/package.json +8 -8
- package/readme.md +68 -69
- package/rules/index.js +5 -0
- 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 +12 -11
- 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-misplaced-block-string-delimiters.js +32 -17
- package/rules/no-quoted-values-in-arrays.js +121 -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 +3 -3
- package/rules/sort-keys.js +3 -3
- package/rules/spaced-comment.js +2 -2
- package/rules/utils/escape-string.js +12 -4
- package/rules/utils/get-meant-escape.js +46 -0
- package/rules/utils/index.js +1 -0
- package/rules/utils/reorder-members.js +9 -3
package/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
FloatNode,
|
|
19
19
|
BooleanNode,
|
|
20
20
|
NullNode,
|
|
21
|
+
DateNode,
|
|
21
22
|
InstantNode,
|
|
22
23
|
DurationNode,
|
|
23
24
|
Token,
|
|
@@ -37,6 +38,7 @@ export type {
|
|
|
37
38
|
FloatNode,
|
|
38
39
|
BooleanNode,
|
|
39
40
|
NullNode,
|
|
41
|
+
DateNode,
|
|
40
42
|
InstantNode,
|
|
41
43
|
DurationNode,
|
|
42
44
|
DurationPart,
|
|
@@ -66,7 +68,7 @@ type TokenOptionsWithComments = TokenOptions & {readonly includeComments: true};
|
|
|
66
68
|
/**
|
|
67
69
|
The source code of a SOML document, which a rule gets as `context.sourceCode`.
|
|
68
70
|
|
|
69
|
-
It has the methods of `TextSourceCodeBase` from `@eslint/plugin-kit`, such as `getText()`, `getLoc()`, `getRange()`, and `getLocFromIndex()`, and the methods below.
|
|
71
|
+
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
72
|
|
|
71
73
|
@example
|
|
72
74
|
```
|
|
@@ -110,6 +112,16 @@ export interface SomlSourceCode extends TextSourceCodeBase<{
|
|
|
110
112
|
*/
|
|
111
113
|
getParent(node: Node): Node | undefined;
|
|
112
114
|
|
|
115
|
+
/**
|
|
116
|
+
Every ancestor node, from the root to the parent. Tokens and comments have no ancestors.
|
|
117
|
+
|
|
118
|
+
@example
|
|
119
|
+
```
|
|
120
|
+
const parents = context.sourceCode.getAncestors(node).map(ancestor => context.sourceCode.getParent(ancestor));
|
|
121
|
+
```
|
|
122
|
+
*/
|
|
123
|
+
getAncestors(node: SomlSyntaxElement): Node[];
|
|
124
|
+
|
|
113
125
|
/**
|
|
114
126
|
Every comment, in source order.
|
|
115
127
|
*/
|
|
@@ -158,7 +170,7 @@ type CollectionParent = DocumentNode | ValueParent;
|
|
|
158
170
|
The listeners that a rule for SOML returns, with the type of each node and of its parent.
|
|
159
171
|
*/
|
|
160
172
|
export interface SomlRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
|
161
|
-
Document?(node: DocumentNode): void;
|
|
173
|
+
Document?(node: DocumentNode, parent: undefined): void;
|
|
162
174
|
Object?(node: ObjectNode, parent: CollectionParent): void;
|
|
163
175
|
Member?(node: MemberNode, parent: ObjectNode): void;
|
|
164
176
|
Key?(node: KeyNode, parent: MemberNode): void;
|
|
@@ -168,6 +180,17 @@ export interface SomlRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<
|
|
|
168
180
|
Float?(node: FloatNode, parent: ValueParent): void;
|
|
169
181
|
Boolean?(node: BooleanNode, parent: ValueParent): void;
|
|
170
182
|
Null?(node: NullNode, parent: ValueParent): void;
|
|
183
|
+
/**
|
|
184
|
+
Visits a calendar date without needing Temporal to read its components.
|
|
185
|
+
|
|
186
|
+
@example
|
|
187
|
+
```
|
|
188
|
+
Date(node) {
|
|
189
|
+
const year: number = node.year;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
*/
|
|
193
|
+
Date?(node: DateNode, parent: ValueParent): void;
|
|
171
194
|
Instant?(node: InstantNode, parent: ValueParent): void;
|
|
172
195
|
Duration?(node: DurationNode, parent: ValueParent): void;
|
|
173
196
|
}> {}
|
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: {
|
|
@@ -132,6 +132,23 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
132
132
|
return null;
|
|
133
133
|
}
|
|
134
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
|
+
|
|
135
152
|
/**
|
|
136
153
|
The 1-based line and 0-based column of an offset.
|
|
137
154
|
|
|
@@ -149,16 +166,8 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
149
166
|
throw new RangeError(`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`);
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
if (!this.#lineStarts) {
|
|
153
|
-
this.#lineStarts = [0];
|
|
154
|
-
|
|
155
|
-
for (let lineFeed = this.text.indexOf('\n'); lineFeed !== -1; lineFeed = this.text.indexOf('\n', lineFeed + 1)) {
|
|
156
|
-
this.#lineStarts.push(lineFeed + 1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
169
|
// The last line that starts at or before `index`.
|
|
161
|
-
const lineStarts = this.#
|
|
170
|
+
const lineStarts = this.#getLineStarts();
|
|
162
171
|
let low = 0;
|
|
163
172
|
let high = lineStarts.length - 1;
|
|
164
173
|
|
|
@@ -175,6 +184,34 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
175
184
|
return {line: low + 1, column: index - lineStarts[low]};
|
|
176
185
|
}
|
|
177
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
|
+
|
|
178
215
|
/**
|
|
179
216
|
@returns {Iterable<VisitNodeStep>}
|
|
180
217
|
*/
|
|
@@ -223,7 +260,7 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
223
260
|
return this.#parents.get(node);
|
|
224
261
|
}
|
|
225
262
|
|
|
226
|
-
//
|
|
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.
|
|
227
264
|
|
|
228
265
|
/**
|
|
229
266
|
@returns {readonly Comment[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-soml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "ESLint plugin for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:soml-lang/eslint-soml",
|
|
@@ -53,23 +53,23 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@eslint/core": "^1.2.1",
|
|
55
55
|
"@eslint/plugin-kit": "^0.7.3",
|
|
56
|
-
"soml-lang": "^0.0.
|
|
56
|
+
"soml-lang": "^0.0.8"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/code-frame": "^8.0.6",
|
|
60
60
|
"@eslint-community/eslint-utils": "^4.10.1",
|
|
61
|
-
"@typescript-eslint/types": "^8.
|
|
61
|
+
"@typescript-eslint/types": "^8.72.1",
|
|
62
62
|
"enquirer": "^2.4.1",
|
|
63
|
-
"eslint": "^10.
|
|
64
|
-
"eslint-config-xo": "^
|
|
63
|
+
"eslint": "^10.14.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.2
|
|
67
|
+
"eslint-plugin-jsdoc": "^65.4.2",
|
|
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
|
-
"npm-run-all2": "^9.0.
|
|
72
|
+
"npm-run-all2": "^9.0.4",
|
|
73
73
|
"open-editor": "^6.0.0",
|
|
74
74
|
"outdent": "^0.8.0",
|
|
75
75
|
"typescript": "^6.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 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
|
|
19
|
+
The rules of this plugin do not need `Temporal`, so they work on every supported Node.js version, also for files with dates, instants, and durations. Only reading the `value` of a `Date`, `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
|
|
|
@@ -111,6 +111,38 @@ A selector can also check a value under a given key, which catches settings that
|
|
|
111
111
|
],
|
|
112
112
|
```
|
|
113
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(Date, Instant, Duration)',
|
|
141
|
+
message: 'This program does not support dates, instants, and durations.',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
```
|
|
145
|
+
|
|
114
146
|
### SOML in Markdown
|
|
115
147
|
|
|
116
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:
|
|
@@ -145,35 +177,40 @@ A block that shows a mistake on purpose can be skipped with `<!-- eslint-skip --
|
|
|
145
177
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
146
178
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
147
179
|
|
|
148
|
-
| Name | Description
|
|
149
|
-
| :--------------------------------------------------------------------------------------------------- |
|
|
150
|
-
| [format](docs/rules/format.md) | Enforce the layout of the SOML formatter.
|
|
151
|
-
| [key-name-casing](docs/rules/key-name-casing.md) | Enforce a casing style for keys.
|
|
152
|
-
| [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.
|
|
153
|
-
| [no-block-string-trailing-whitespace](docs/rules/no-block-string-trailing-whitespace.md) | Disallow trailing whitespace in block strings.
|
|
154
|
-
| [no-confusable-keys](docs/rules/no-confusable-keys.md) | Disallow keys that differ only in case,
|
|
155
|
-
| [no-decimal-file-modes](docs/rules/no-decimal-file-modes.md) | Disallow file modes written as decimal numbers.
|
|
156
|
-
| [no-
|
|
157
|
-
| [no-
|
|
158
|
-
| [no-
|
|
159
|
-
| [no-
|
|
160
|
-
| [no-
|
|
161
|
-
| [no-
|
|
162
|
-
| [no-
|
|
163
|
-
| [no-
|
|
164
|
-
| [no-
|
|
165
|
-
| [no-
|
|
166
|
-
| [no-
|
|
167
|
-
| [no-
|
|
168
|
-
| [no-
|
|
169
|
-
| [no-
|
|
170
|
-
| [no-
|
|
171
|
-
| [
|
|
172
|
-
| [
|
|
173
|
-
| [
|
|
174
|
-
| [
|
|
175
|
-
| [
|
|
176
|
-
| [
|
|
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 duplicate strings and dates 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 `*/`. | ✅ | 🔧 | |
|
|
177
214
|
|
|
178
215
|
<!-- end auto-generated rules list -->
|
|
179
216
|
|
|
@@ -251,45 +288,7 @@ In Visual Studio Code, the [ESLint extension](https://marketplace.visualstudio.c
|
|
|
251
288
|
|
|
252
289
|
## Writing rules
|
|
253
290
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
```js
|
|
257
|
-
/**
|
|
258
|
-
@type {import('eslint-soml').SomlRuleDefinition}
|
|
259
|
-
*/
|
|
260
|
-
const rule = {
|
|
261
|
-
meta: {
|
|
262
|
-
type: 'suggestion',
|
|
263
|
-
messages: {
|
|
264
|
-
tooLong: 'The key `{{key}}` is longer than 40 characters.',
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
create(context) {
|
|
268
|
-
return {
|
|
269
|
-
Key(node) {
|
|
270
|
-
if (node.value.length > 40) {
|
|
271
|
-
context.report({node, messageId: 'tooLong', data: {key: node.value}});
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
};
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
The nodes hold decoded values, and the source text keeps the spelling:
|
|
280
|
-
|
|
281
|
-
- `Key` and `String` have the decoded `value`, so `a`, `'a'`, and `"\u{61}"` all have the value `a`. Their `style` is how they are written: `'bare'` (a key only), `'literal'` for `'…'`, or `'escaped'` for `"…"`. A `String` also has `block`, which is `true` for `'''` and `"""`.
|
|
282
|
-
- `Integer` has a `bigint` `value` and its `radix`, so `0xFF` and `255` have the same value. `Float` and `Boolean` have a `value`, and `Null` has none.
|
|
283
|
-
- `context.sourceCode.getText(node)` gives the source text, such as `0xFF` or `"\u{61}"`, for a rule about spelling.
|
|
284
|
-
|
|
285
|
-
`context.sourceCode` has the methods of `TextSourceCodeBase` from `@eslint/plugin-kit`, such as `getText()`, `getLoc()`, `getRange()`, and `getLocFromIndex()`, and these:
|
|
286
|
-
|
|
287
|
-
- `getParent(node)` returns the parent of a node, or `undefined` for the `Document` node. Nodes have no `parent` property. A listener also gets the parent as its second argument.
|
|
288
|
-
- `getAllComments()` returns every comment, in source order.
|
|
289
|
-
- `getTokenBefore()`, `getTokenAfter()`, `getFirstToken()`, `getLastToken()`, and `getTokens()` find tokens in source order. They take only the `includeComments` option, and return `null` when there is no token, like ESLint's own `SourceCode`.
|
|
290
|
-
- `parserServices.isSOML` is `true`, so a rule that works on several languages can tell a SOML file, with `context.sourceCode.parserServices?.isSOML`.
|
|
291
|
-
|
|
292
|
-
Reading the `value` of an `Instant` or a `Duration` node needs `Temporal`, and so does spreading such a node. Check the type of a node before you read its `value`, and use `parts` or the source text of a duration when you can.
|
|
291
|
+
See [how to write rules for SOML files](docs/writing-rules.md).
|
|
293
292
|
|
|
294
293
|
## Related
|
|
295
294
|
|
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';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {trimWhitespace} from './utils/index.js';
|
|
1
|
+
import {escapeInvisibleCharacters, trimWhitespace} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -7,13 +7,25 @@ import {trimWhitespace} from './utils/index.js';
|
|
|
7
7
|
|
|
8
8
|
const MESSAGE_ID = 'no-confusable-keys';
|
|
9
9
|
const messages = {
|
|
10
|
-
[MESSAGE_ID]: '`{{key}}` and `{{otherKey}}` are one key to a program that ignores case,
|
|
10
|
+
[MESSAGE_ID]: '`{{key}}` and `{{otherKey}}` are one key to a program that ignores case or surrounding whitespace, or that treats `-`, `_`, and `.` as the same.',
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/*
|
|
14
|
-
What many programs compare keys by: case-insensitive, with
|
|
14
|
+
What many programs compare keys by: case-insensitive, with `-`, `_`, and `.` the same, and without surrounding whitespace. Libraries that read settings from environment variables treat `.` as `_`: Spring reads `LOG_LEVEL` as `log.level`, and Viper, with its usual replacer, as both `log.level` and `log-level`.
|
|
15
15
|
*/
|
|
16
|
-
const
|
|
16
|
+
const foldCharacter = character => {
|
|
17
|
+
const upper = character.toUpperCase();
|
|
18
|
+
const lower = upper.toLowerCase();
|
|
19
|
+
// A form that is more than one code point, such as the `SS` that `ß` uppercases to, is not a case of one letter, so keep the character as it is.
|
|
20
|
+
return [...upper].length === 1 && [...lower].length === 1 ? lower : character;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
Lowercasing alone leaves some case variants apart, because several code points can share one uppercase form: `ς` (final sigma) does not lowercase to `σ`, `ſ` does not lowercase to `s`, and `ı` does not lowercase to `i`. Uppercasing and lowercasing each code point again reaches the form the others share. Lowercase first, so a document that a JavaScript `toLowerCase()` reader would merge, such as `İ` and `i̇`, still folds together.
|
|
25
|
+
*/
|
|
26
|
+
const fold = key => [...trimWhitespace(key).toLowerCase().replaceAll(/[\-.]/gv, '_')]
|
|
27
|
+
.map(character => foldCharacter(character))
|
|
28
|
+
.join('');
|
|
17
29
|
|
|
18
30
|
/**
|
|
19
31
|
@param {SomlContext} context
|
|
@@ -37,10 +49,10 @@ const create = context => {
|
|
|
37
49
|
yield {
|
|
38
50
|
node: key,
|
|
39
51
|
messageId: MESSAGE_ID,
|
|
40
|
-
// The source text shows the quotes, so a key with whitespace stays readable.
|
|
52
|
+
// The source text shows the quotes, so a key with whitespace stays readable, and a bidirectional control is escaped.
|
|
41
53
|
data: {
|
|
42
|
-
key: context.sourceCode.getText(key),
|
|
43
|
-
otherKey: context.sourceCode.getText(firstKey),
|
|
54
|
+
key: escapeInvisibleCharacters(context.sourceCode.getText(key)),
|
|
55
|
+
otherKey: escapeInvisibleCharacters(context.sourceCode.getText(firstKey)),
|
|
44
56
|
},
|
|
45
57
|
};
|
|
46
58
|
}
|
|
@@ -55,7 +67,7 @@ const config = {
|
|
|
55
67
|
meta: {
|
|
56
68
|
type: 'problem',
|
|
57
69
|
docs: {
|
|
58
|
-
description: 'Disallow keys that differ only in case,
|
|
70
|
+
description: 'Disallow keys that differ only in case, `-`, `_`, and `.`, or surrounding whitespace.',
|
|
59
71
|
recommended: 'unopinionated',
|
|
60
72
|
},
|
|
61
73
|
messages,
|
|
@@ -8,8 +8,8 @@ import {getKeyWords} from './utils/index.js';
|
|
|
8
8
|
const MESSAGE_ID_ERROR = 'no-decimal-file-modes/error';
|
|
9
9
|
const MESSAGE_ID_SUGGESTION = 'no-decimal-file-modes/suggestion';
|
|
10
10
|
const messages = {
|
|
11
|
-
[MESSAGE_ID_ERROR]: '`{{number}}` is a decimal number, so this file mode is `{{octal}}`. A file mode is octal, as in `0o{{
|
|
12
|
-
[MESSAGE_ID_SUGGESTION]: 'Write it as `0o{{
|
|
11
|
+
[MESSAGE_ID_ERROR]: '`{{number}}` is a decimal number, so this file mode is `{{octal}}`. A file mode is octal, as in `0o{{digits}}`.',
|
|
12
|
+
[MESSAGE_ID_SUGGESTION]: 'Write it as `0o{{digits}}`.',
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/*
|
|
@@ -53,10 +53,12 @@ const create = context => {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const number = sourceCode.getText(value);
|
|
56
|
+
// The spec permits `_` between digits, so the digits alone decide what the number is.
|
|
57
|
+
const digits = number.replaceAll('_', '');
|
|
56
58
|
const decimal = Number(value.value);
|
|
57
59
|
|
|
58
60
|
// The digits look octal, and the value makes no sense as a mode, but the same digits read as octal do.
|
|
59
|
-
if (!OCTAL_DIGITS.test(
|
|
61
|
+
if (!OCTAL_DIGITS.test(digits) || isSensibleMode(decimal) || !isSensibleMode(Number.parseInt(digits, 8))) {
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -66,12 +68,13 @@ const create = context => {
|
|
|
66
68
|
data: {
|
|
67
69
|
number,
|
|
68
70
|
octal: `0o${decimal.toString(8)}`,
|
|
71
|
+
digits,
|
|
69
72
|
},
|
|
70
73
|
suggest: [
|
|
71
74
|
{
|
|
72
75
|
messageId: MESSAGE_ID_SUGGESTION,
|
|
73
|
-
data: {
|
|
74
|
-
fix: fixer => fixer.replaceText(value, `0o${
|
|
76
|
+
data: {digits},
|
|
77
|
+
fix: fixer => fixer.replaceText(value, `0o${digits}`),
|
|
75
78
|
},
|
|
76
79
|
],
|
|
77
80
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {escapeInvisibleCharacters} from './utils/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
@import {SomlContext} from './rule/soml-context.js';
|
|
5
|
+
@import {SomlRule} from './rule/to-eslint-rule.js';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MESSAGE_ID = 'no-dotted-key-collisions';
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: '`{{key}}` is a key of its own, not `{{rest}}` in the object `{{object}}`, because SOML does not split keys at `.`. Move it into `{{object}}`.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
@param {SomlContext} context
|
|
15
|
+
*/
|
|
16
|
+
const create = context => {
|
|
17
|
+
context.on('Object', function * (node) {
|
|
18
|
+
// Only a sibling that is an object shows that the author nests with braces, and then expects `'a.b'` to go into `a`, as in TOML. A flat map of dotted names, such as Spark or log4j settings, or a host next to its domain, has no such object.
|
|
19
|
+
const objectKeys = new Set(node.members
|
|
20
|
+
.filter(member => member.value.type === 'Object')
|
|
21
|
+
.map(member => member.key.value));
|
|
22
|
+
|
|
23
|
+
for (const {key, value} of node.members) {
|
|
24
|
+
const dotIndex = key.value.indexOf('.');
|
|
25
|
+
|
|
26
|
+
// A key that starts with `.` names a file, such as `'.eslintrc'`, not a path. A key whose value is an object is more likely a name in a map of objects, such as the logger `'uvicorn.error'` next to `uvicorn`, or the host `'bastion.internal'` next to `bastion`.
|
|
27
|
+
if (dotIndex <= 0 || value.type === 'Object') {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const object = key.value.slice(0, dotIndex);
|
|
32
|
+
|
|
33
|
+
if (!objectKeys.has(object)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
yield {
|
|
38
|
+
node: key,
|
|
39
|
+
messageId: MESSAGE_ID,
|
|
40
|
+
data: {
|
|
41
|
+
key: escapeInvisibleCharacters(context.sourceCode.getText(key)),
|
|
42
|
+
object: escapeInvisibleCharacters(object),
|
|
43
|
+
rest: escapeInvisibleCharacters(key.value.slice(dotIndex + 1)),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
@type {SomlRule}
|
|
52
|
+
*/
|
|
53
|
+
const config = {
|
|
54
|
+
create,
|
|
55
|
+
meta: {
|
|
56
|
+
type: 'problem',
|
|
57
|
+
docs: {
|
|
58
|
+
description: 'Disallow quoted keys with a `.` that read as a path into an object next to them.',
|
|
59
|
+
recommended: true,
|
|
60
|
+
},
|
|
61
|
+
messages,
|
|
62
|
+
languages: ['soml/soml'],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default config;
|