eslint-soml 0.0.4 → 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 +12 -2
- package/index.js +1 -0
- package/language/soml-source-code.js +47 -10
- package/package.json +7 -7
- package/readme.md +67 -68
- 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 +2 -2
- 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 +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 +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
|
@@ -66,7 +66,7 @@ type TokenOptionsWithComments = TokenOptions & {readonly includeComments: true};
|
|
|
66
66
|
/**
|
|
67
67
|
The source code of a SOML document, which a rule gets as `context.sourceCode`.
|
|
68
68
|
|
|
69
|
-
It has the methods of `TextSourceCodeBase` from `@eslint/plugin-kit`, such as `getText()`, `getLoc()`, `getRange()`, and `getLocFromIndex()`, and the methods below.
|
|
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
70
|
|
|
71
71
|
@example
|
|
72
72
|
```
|
|
@@ -110,6 +110,16 @@ export interface SomlSourceCode extends TextSourceCodeBase<{
|
|
|
110
110
|
*/
|
|
111
111
|
getParent(node: Node): Node | undefined;
|
|
112
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
|
+
|
|
113
123
|
/**
|
|
114
124
|
Every comment, in source order.
|
|
115
125
|
*/
|
|
@@ -158,7 +168,7 @@ type CollectionParent = DocumentNode | ValueParent;
|
|
|
158
168
|
The listeners that a rule for SOML returns, with the type of each node and of its parent.
|
|
159
169
|
*/
|
|
160
170
|
export interface SomlRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
|
161
|
-
Document?(node: DocumentNode): void;
|
|
171
|
+
Document?(node: DocumentNode, parent: undefined): void;
|
|
162
172
|
Object?(node: ObjectNode, parent: CollectionParent): void;
|
|
163
173
|
Member?(node: MemberNode, parent: ObjectNode): void;
|
|
164
174
|
Key?(node: KeyNode, parent: MemberNode): void;
|
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.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",
|
|
@@ -53,20 +53,20 @@
|
|
|
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.7"
|
|
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.71.
|
|
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
|
@@ -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(Instant, Duration)',
|
|
141
|
+
message: 'This program does not support 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 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 `*/`. | ✅ | 🔧 | |
|
|
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;
|
|
@@ -72,8 +72,8 @@ const create = context => {
|
|
|
72
72
|
node: item,
|
|
73
73
|
messageId: MESSAGE_ID_ERROR,
|
|
74
74
|
data: {
|
|
75
|
-
// A block string spans lines, so it is shown as a one-line string with the same value.
|
|
76
|
-
item: item.block ?
|
|
75
|
+
// A block string spans lines, so it is shown as a one-line string with the same value. A bidirectional control is escaped, or it could reorder the message, whichever spelling the string uses.
|
|
76
|
+
item: escapeInvisibleCharacters(item.block ? stringifyValue(item.value) : sourceCode.getText(item)),
|
|
77
77
|
line: sourceCode.getLoc(first).start.line,
|
|
78
78
|
},
|
|
79
79
|
// Removing an item changes the value, so it is a suggestion. It is left out when it would remove a comment.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {parse} from 'soml-lang';
|
|
2
|
+
import {canBeLiteral, getMeantEscape, toEscapedString} from './utils/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
@import {SomlContext} from './rule/soml-context.js';
|
|
6
|
+
@import {SomlRule} from './rule/to-eslint-rule.js';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MESSAGE_ID_ERROR = 'no-escapes-in-literal-strings/error';
|
|
10
|
+
const MESSAGE_ID_SUGGESTION = 'no-escapes-in-literal-strings/suggestion';
|
|
11
|
+
const messages = {
|
|
12
|
+
[MESSAGE_ID_ERROR]: 'A literal string, `\'…\'`, has no escapes, so `{{escape}}` is a backslash and `{{rest}}`. Use a `"…"` string for escapes.',
|
|
13
|
+
[MESSAGE_ID_SUGGESTION]: 'Write it as `{{replacement}}`.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
@param {SomlContext} context
|
|
18
|
+
*/
|
|
19
|
+
const create = context => {
|
|
20
|
+
context.on('String', node => {
|
|
21
|
+
if (node.style !== 'literal' || node.block) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const escape = getMeantEscape(node.value);
|
|
26
|
+
|
|
27
|
+
if (escape === undefined) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Every backslash starts an escape and the value has no `"`, so the same text in `"…"` is a valid string, unless an escape names a character that SOML cannot hold, such as `\u{d}`.
|
|
32
|
+
let value;
|
|
33
|
+
try {
|
|
34
|
+
[value] = parse(`["${node.value}"]`);
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A literal string only when it would not be reported again, by this rule or by `no-doubled-backslashes-in-literal-strings`, as in `prefer-literal-strings`.
|
|
40
|
+
const replacement = canBeLiteral(value) ? `'${value}'` : toEscapedString(value);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
node,
|
|
44
|
+
messageId: MESSAGE_ID_ERROR,
|
|
45
|
+
data: {
|
|
46
|
+
escape,
|
|
47
|
+
rest: escape.slice(1),
|
|
48
|
+
},
|
|
49
|
+
suggest: [
|
|
50
|
+
{
|
|
51
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
52
|
+
data: {replacement},
|
|
53
|
+
fix: fixer => fixer.replaceText(node, replacement),
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
@type {SomlRule}
|
|
62
|
+
*/
|
|
63
|
+
const config = {
|
|
64
|
+
create,
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'problem',
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Disallow escapes in literal strings, such as `\'Line one\\nLine two\'`.',
|
|
69
|
+
recommended: true,
|
|
70
|
+
},
|
|
71
|
+
hasSuggestions: true,
|
|
72
|
+
messages,
|
|
73
|
+
languages: ['soml/soml'],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default config;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {parse} from 'soml-lang';
|
|
1
2
|
import {canBeLiteral, toEscapedString} from './utils/index.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -17,11 +18,8 @@ A Windows drive, such as `C:\`, at the start of the string, where a path value s
|
|
|
17
18
|
*/
|
|
18
19
|
const DRIVE = /^[A-Z]:\\/v;
|
|
19
20
|
const ESCAPE = /\\(?:u\{(?<codePoint>[\da-f]+)\}|(?<character>.))/gv;
|
|
21
|
+
// Only these short escapes can be mistaken for path separators; other escapes keep their meaning.
|
|
20
22
|
const MEANINGS = {n: 'a line break', t: 'a tab'};
|
|
21
|
-
// What the author meant by each short escape: `\n` and `\t` are a backslash and a letter, as in the path, and the others mean what they say.
|
|
22
|
-
const MEANT = {
|
|
23
|
-
'\\': '\\', '"': '"', n: String.raw`\n`, t: String.raw`\t`,
|
|
24
|
-
};
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
25
|
@param {SomlContext} context
|
|
@@ -47,8 +45,9 @@ const create = context => {
|
|
|
47
45
|
return;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
// What the author meant.
|
|
51
|
-
const
|
|
48
|
+
// What the author meant: make `\n` and `\t` literal, and let the parser decode every other escape. This cannot throw, because the string already parsed, so every escape in it is valid, and `\\n` and `\\t` are valid too.
|
|
49
|
+
const corrected = content.replaceAll(ESCAPE, (escape, codePoint, character) => Object.hasOwn(MEANINGS, character) ? `\\${escape}` : escape);
|
|
50
|
+
const [value] = parse(`["${corrected}"]`);
|
|
52
51
|
const replacement = canBeLiteral(value) ? `'${value}'` : toEscapedString(value);
|
|
53
52
|
const {character} = mistake.groups;
|
|
54
53
|
const suggestions = [];
|
|
@@ -20,6 +20,11 @@ const messages = {
|
|
|
20
20
|
[MESSAGE_ID_COMMENT]: 'The character U+{{codePoint}} is invisible or looks like a space. Remove it.',
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
/*
|
|
24
|
+
In a single-line string or a quoted key, a tab also looks like spaces. In a block string and a comment, a tab is allowed, because it indents.
|
|
25
|
+
*/
|
|
26
|
+
const singleLineCharacters = new RegExp(String.raw`${invisibleCharacters.source}|\t`, 'gv');
|
|
27
|
+
|
|
23
28
|
/**
|
|
24
29
|
@param {SomlContext} context
|
|
25
30
|
*/
|
|
@@ -43,13 +48,14 @@ const create = context => {
|
|
|
43
48
|
messageId = MESSAGE_ID_COMMENT;
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
const characters = isComment(node) || node.block ? invisibleCharacters : singleLineCharacters;
|
|
46
52
|
let isFirst = true;
|
|
47
53
|
|
|
48
|
-
for (const match of sourceCode.getText(node).matchAll(
|
|
54
|
+
for (const match of sourceCode.getText(node).matchAll(characters)) {
|
|
49
55
|
const [character] = match;
|
|
50
56
|
const start = nodeStart + match.index;
|
|
51
57
|
const end = start + character.length;
|
|
52
|
-
const escape = toUnicodeEscape(character);
|
|
58
|
+
const escape = character === '\t' ? String.raw`\t` : toUnicodeEscape(character);
|
|
53
59
|
let fix;
|
|
54
60
|
|
|
55
61
|
if (isEscaped) {
|
|
@@ -11,41 +11,56 @@ const messages = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const QUOTE_RUNS = {'\'': /'+/gv, '"': /"+/gv};
|
|
14
|
-
// An odd number of backslashes at the end, so the character after them is escaped.
|
|
15
|
-
const ESCAPING_BACKSLASHES = /(?:^|[^\\])(?:\\\\)*\\$/v;
|
|
16
14
|
|
|
17
15
|
/*
|
|
18
|
-
|
|
16
|
+
The runs of the delimiter's quote character on the line, each with its index, its length, and where it ends. In a `"""` block, `\"` is an escaped quote, so it is not part of the run.
|
|
19
17
|
*/
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
function getQuoteRuns(line, quote, isEscaped) {
|
|
19
|
+
return line.matchAll(QUOTE_RUNS[quote]).map(match => {
|
|
20
|
+
let backslashCount = 0;
|
|
21
|
+
if (isEscaped) {
|
|
22
|
+
for (let index = match.index - 1; line[index] === '\\'; index--) {
|
|
23
|
+
backslashCount++;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// An odd number of backslashes immediately before a quote means it is escaped.
|
|
28
|
+
const isFirstQuoteEscaped = backslashCount % 2 === 1;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
index: match.index + (isFirstQuoteEscaped ? 1 : 0),
|
|
32
|
+
length: match[0].length - (isFirstQuoteEscaped ? 1 : 0),
|
|
33
|
+
end: match.index + match[0].length,
|
|
34
|
+
};
|
|
35
|
+
}).toArray();
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
/*
|
|
26
|
-
|
|
39
|
+
Whether the run at `runIndex` closes a quote that the line opened, as in `Say '''hi'''`, because an odd number of runs of the same length come before it.
|
|
27
40
|
*/
|
|
28
|
-
|
|
41
|
+
function isClosingQuote(runs, runIndex, delimiterLength) {
|
|
42
|
+
return runs.slice(0, runIndex).filter(run => run.length === delimiterLength).length % 2 === 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
What may follow a closing delimiter on its line: whitespace, commas, closing brackets, and a comment. It is sticky, so it matches from `lastIndex`, which is set before every use. The `s` flag lets the comment hold U+2028 or U+2029, which do not end a SOML line.
|
|
47
|
+
*/
|
|
48
|
+
const AFTER_DELIMITER = /[\t ]*(?:[,\]\}][\t ]*)*(?:(?:#|\/\*).*)?$/svy;
|
|
29
49
|
|
|
30
50
|
/*
|
|
31
51
|
Where a misplaced delimiter starts on a content line, or `undefined`: a run of exactly the delimiter's length with only what may follow a closing delimiter after it, as TOML and Java allow, in `Welcome!'''`, `Hi'''}`, or `Welcome!''' # greeting`. Only the first run with such a tail is checked, because what follows it is a comment or brackets.
|
|
32
52
|
*/
|
|
33
53
|
function getMisplacedDelimiterIndex(line, delimiter, isEscaped) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
const runs = getQuoteRuns(line, delimiter[0], isEscaped);
|
|
55
|
+
|
|
56
|
+
for (const [runIndex, {index, length, end}] of runs.entries()) {
|
|
37
57
|
AFTER_DELIMITER.lastIndex = end;
|
|
38
58
|
|
|
39
59
|
if (!AFTER_DELIMITER.test(line)) {
|
|
40
60
|
continue;
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
if (isEscaped && ESCAPING_BACKSLASHES.test(line.slice(0, index))) {
|
|
45
|
-
index++;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return index > 0 && end - index === delimiter.length && !isClosingQuote(line, index, delimiter) ? index : undefined;
|
|
63
|
+
return index > 0 && length === delimiter.length && !isClosingQuote(runs, runIndex, delimiter.length) ? index : undefined;
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {parseTree} from 'soml-lang';
|
|
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_ERROR = 'no-quoted-values-in-arrays/error';
|
|
9
|
+
const MESSAGE_ID_SUGGESTION = 'no-quoted-values-in-arrays/suggestion';
|
|
10
|
+
const messages = {
|
|
11
|
+
[MESSAGE_ID_ERROR]: '`{{string}}` is a string, but this array holds {{type}}.',
|
|
12
|
+
[MESSAGE_ID_SUGGESTION]: 'Write it as `{{value}}`.',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
The types a string can spell. A boolean or `null` in quotes is for `no-quoted-keywords`. A float in quotes is not checked, because it is most often a version, as in `[1.21, 1.22, '1.23']`, where the floats are the mistake.
|
|
17
|
+
*/
|
|
18
|
+
const TYPES = new Map([
|
|
19
|
+
['Integer', 'ints'],
|
|
20
|
+
['Instant', 'instants'],
|
|
21
|
+
['Duration', 'durations'],
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
The type of the one value that `text` is the exact spelling of, such as `'Integer'` for `443` and `'Duration'` for `2s`, or `undefined` when it is not one value.
|
|
26
|
+
|
|
27
|
+
@param {string} text
|
|
28
|
+
@returns {string | undefined}
|
|
29
|
+
*/
|
|
30
|
+
function getSpelledType(text) {
|
|
31
|
+
let element;
|
|
32
|
+
try {
|
|
33
|
+
[element] = parseTree(`[${text}]`).body.elements;
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (element === undefined) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// eslint-disable-next-line internal/no-restricted-property-access -- A node of another document, not of this file.
|
|
43
|
+
const [start, end] = element.range;
|
|
44
|
+
|
|
45
|
+
// The value must be all of the text, so whitespace, a comment, or a second item around it does not count.
|
|
46
|
+
if (start === 1 && end === text.length + 1) {
|
|
47
|
+
return element.type;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
@param {SomlContext} context
|
|
53
|
+
*/
|
|
54
|
+
const create = context => {
|
|
55
|
+
context.on('Array', function * (node) {
|
|
56
|
+
const strings = [];
|
|
57
|
+
const typed = [];
|
|
58
|
+
|
|
59
|
+
// `null` is the missing value in a list of any type, so it does not count.
|
|
60
|
+
for (const element of node.elements) {
|
|
61
|
+
if (element.type === 'String') {
|
|
62
|
+
strings.push(element);
|
|
63
|
+
} else if (element.type !== 'Null') {
|
|
64
|
+
typed.push(element);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const type = typed[0]?.type;
|
|
69
|
+
|
|
70
|
+
// Each string is reported only when the array would hold one type without quotes, so an array with other text, such as `[80, 443, 'auto', '8080']`, is not. The quoted items must also be no more than the others, or the others are more likely the mistake.
|
|
71
|
+
if (
|
|
72
|
+
!TYPES.has(type)
|
|
73
|
+
|| strings.length === 0
|
|
74
|
+
|| strings.length > typed.length
|
|
75
|
+
|| typed.some(element => element.type !== type)
|
|
76
|
+
|| strings.some(string => string.block || getSpelledType(string.value) !== type)
|
|
77
|
+
) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const string of strings) {
|
|
82
|
+
const {value} = string;
|
|
83
|
+
|
|
84
|
+
yield {
|
|
85
|
+
node: string,
|
|
86
|
+
messageId: MESSAGE_ID_ERROR,
|
|
87
|
+
data: {
|
|
88
|
+
string: context.sourceCode.getText(string),
|
|
89
|
+
type: TYPES.get(type),
|
|
90
|
+
},
|
|
91
|
+
suggest: [
|
|
92
|
+
{
|
|
93
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
94
|
+
data: {value},
|
|
95
|
+
fix: fixer => fixer.replaceText(string, value),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
@type {SomlRule}
|
|
105
|
+
*/
|
|
106
|
+
const config = {
|
|
107
|
+
create,
|
|
108
|
+
meta: {
|
|
109
|
+
type: 'problem',
|
|
110
|
+
docs: {
|
|
111
|
+
description: 'Disallow quoted values in an array of that type, such as `\'443\'` in `[80, \'443\']`.',
|
|
112
|
+
recommended: true,
|
|
113
|
+
},
|
|
114
|
+
hasSuggestions: true,
|
|
115
|
+
messages,
|
|
116
|
+
languages: ['soml/soml'],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default config;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {getKeyWords} from './utils/index.js';
|
|
1
|
+
import {escapeInvisibleCharacters, getKeyWords} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -86,7 +86,7 @@ const create = context => {
|
|
|
86
86
|
messageId: MESSAGE_ID_ERROR,
|
|
87
87
|
data: {
|
|
88
88
|
duration,
|
|
89
|
-
key: context.sourceCode.getText(key),
|
|
89
|
+
key: escapeInvisibleCharacters(context.sourceCode.getText(key)),
|
|
90
90
|
},
|
|
91
91
|
suggest: [
|
|
92
92
|
{
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {escapeInvisibleCharacters, trimWhitespace} 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-surrounding-whitespace-in-keys';
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: 'The whitespace at the start or end of `{{key}}` is part of the key, so a program that reads `{{trimmed}}` does not find it.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
@param {SomlContext} context
|
|
15
|
+
*/
|
|
16
|
+
const create = context => {
|
|
17
|
+
context.on('Key', node => {
|
|
18
|
+
const trimmed = trimWhitespace(node.value);
|
|
19
|
+
|
|
20
|
+
// A key of only whitespace is for `no-empty-keys`.
|
|
21
|
+
if (trimmed === '' || trimmed === node.value) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
node,
|
|
27
|
+
messageId: MESSAGE_ID,
|
|
28
|
+
data: {
|
|
29
|
+
// The source text shows the quotes, so the whitespace is visible, and an invisible character is escaped.
|
|
30
|
+
key: escapeInvisibleCharacters(context.sourceCode.getText(node)),
|
|
31
|
+
trimmed: escapeInvisibleCharacters(trimmed),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
@type {SomlRule}
|
|
39
|
+
*/
|
|
40
|
+
const config = {
|
|
41
|
+
create,
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'problem',
|
|
44
|
+
docs: {
|
|
45
|
+
description: 'Disallow whitespace at the start and end of keys.',
|
|
46
|
+
recommended: true,
|
|
47
|
+
},
|
|
48
|
+
messages,
|
|
49
|
+
languages: ['soml/soml'],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default config;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {
|
|
2
|
+
escapeInvisibleCharacters,
|
|
3
|
+
getMeantEscape,
|
|
4
|
+
hasDoubledBackslashes,
|
|
5
|
+
hasInvisibleCharacter,
|
|
6
|
+
trimEnd,
|
|
7
|
+
} from './utils/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
@import {SomlContext} from './rule/soml-context.js';
|
|
11
|
+
@import {SomlRule} from './rule/to-eslint-rule.js';
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MESSAGE_ID = 'prefer-block-strings';
|
|
15
|
+
const messages = {
|
|
16
|
+
[MESSAGE_ID]: 'A string of three lines or more is easier to read as a block string.',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const MINIMUM_LINES = 3;
|
|
20
|
+
|
|
21
|
+
const hasTrailingWhitespace = line => trimEnd(line, '\t ') !== line;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
Write the lines of a `"""` block string. Every escape that `"…"` has works in it, so the line breaks at the edges, which a block string would drop, and the whitespace at the end of a line, which an editor can remove, are written as escapes.
|
|
25
|
+
|
|
26
|
+
@param {string[]} lines
|
|
27
|
+
@param {number} leadingCount The number of line breaks at the start of the value.
|
|
28
|
+
@param {number} trailingCount The number of line breaks at the end of the value.
|
|
29
|
+
@returns {string[]}
|
|
30
|
+
*/
|
|
31
|
+
function getEscapedLines(lines, leadingCount, trailingCount) {
|
|
32
|
+
const escapedLines = lines.map(line => {
|
|
33
|
+
const text = trimEnd(line, '\t ');
|
|
34
|
+
const whitespace = line.slice(text.length).replaceAll(' ', String.raw`\u{20}`).replaceAll('\t', String.raw`\t`);
|
|
35
|
+
return escapeInvisibleCharacters(text.replaceAll('\\', '\\\\')) + whitespace;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
escapedLines[0] = String.raw`\n`.repeat(leadingCount) + escapedLines[0];
|
|
39
|
+
escapedLines[escapedLines.length - 1] += String.raw`\n`.repeat(trailingCount);
|
|
40
|
+
|
|
41
|
+
return escapedLines;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
The block string for `value`, with its lines at `indent`, or `undefined` when there is none. A `'''` block string is used when it holds the value as it is, as `prefer-literal-strings` asks, and a `"""` block string otherwise.
|
|
46
|
+
|
|
47
|
+
@param {string} value
|
|
48
|
+
@param {string} indent
|
|
49
|
+
@returns {string | undefined}
|
|
50
|
+
*/
|
|
51
|
+
function getBlockString(value, indent) {
|
|
52
|
+
const withoutTrailing = trimEnd(value, '\n');
|
|
53
|
+
const text = withoutTrailing.replace(/^\n+/v, '');
|
|
54
|
+
const leadingCount = withoutTrailing.length - text.length;
|
|
55
|
+
const trailingCount = value.length - withoutTrailing.length;
|
|
56
|
+
const lines = text.split('\n');
|
|
57
|
+
|
|
58
|
+
if (lines.length < MINIMUM_LINES) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A line break at the start or end, and whitespace at the end of a line, would be lost or invisible in `'''`, which has no escapes. A `'''` in the text could end it. A doubled backslash reads as a mistake, which `no-doubled-backslashes-in-literal-strings` reports, and so does a backslash that `getMeantEscape()` reads as an escape, such as `\u{41}`.
|
|
63
|
+
const isLiteral = leadingCount === 0
|
|
64
|
+
&& trailingCount === 0
|
|
65
|
+
&& !text.includes('\'\'\'')
|
|
66
|
+
&& !hasInvisibleCharacter(text)
|
|
67
|
+
&& !hasDoubledBackslashes(text)
|
|
68
|
+
&& getMeantEscape(text) === undefined
|
|
69
|
+
&& lines.every(line => !hasTrailingWhitespace(line));
|
|
70
|
+
|
|
71
|
+
if (!isLiteral && text.includes('"""')) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const delimiter = isLiteral ? '\'\'\'' : '"""';
|
|
76
|
+
const contentLines = isLiteral ? lines : getEscapedLines(lines, leadingCount, trailingCount);
|
|
77
|
+
|
|
78
|
+
// An empty line needs no indentation.
|
|
79
|
+
const content = contentLines.map(line => line === '' ? '' : indent + line).join('\n');
|
|
80
|
+
return `${delimiter}\n${content}\n${indent}${delimiter}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
@param {SomlContext} context
|
|
85
|
+
*/
|
|
86
|
+
const create = context => {
|
|
87
|
+
const {sourceCode} = context;
|
|
88
|
+
|
|
89
|
+
context.on('String', node => {
|
|
90
|
+
if (node.style !== 'escaped' || node.block || !node.value.includes('\n')) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A value that starts its own line is already where the formatter puts a block string. One after a key or another value goes one level deeper. The `format` rule then moves the opening delimiter to the next line, as the formatter does.
|
|
95
|
+
const {start} = sourceCode.getLoc(node);
|
|
96
|
+
const [lineIndent] = /^[\t ]*/v.exec(sourceCode.lines[start.line - 1]);
|
|
97
|
+
const indent = start.column === lineIndent.length ? lineIndent : `${lineIndent}\t`;
|
|
98
|
+
const blockString = getBlockString(node.value, indent);
|
|
99
|
+
|
|
100
|
+
if (blockString === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
node,
|
|
106
|
+
messageId: MESSAGE_ID,
|
|
107
|
+
fix: fixer => fixer.replaceText(node, blockString),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
@type {SomlRule}
|
|
114
|
+
*/
|
|
115
|
+
const config = {
|
|
116
|
+
create,
|
|
117
|
+
meta: {
|
|
118
|
+
type: 'suggestion',
|
|
119
|
+
docs: {
|
|
120
|
+
description: 'Prefer block strings for strings of three lines or more.',
|
|
121
|
+
recommended: true,
|
|
122
|
+
},
|
|
123
|
+
fixable: 'code',
|
|
124
|
+
messages,
|
|
125
|
+
languages: ['soml/soml'],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export default config;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {canBeLiteral,
|
|
1
|
+
import {canBeLiteral, hasInvisibleCharacter} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -29,8 +29,8 @@ function getLiteral(node, text) {
|
|
|
29
29
|
return delimiter + text.slice(delimiter.length, -delimiter.length) + delimiter;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
// A value such as `^\\d+$` reads as a mistake in a literal string,
|
|
33
|
-
if (canBeLiteral(node.value)
|
|
32
|
+
// A value such as `^\\d+$` or `Done.\n` reads as a mistake in a literal string, so `canBeLiteral()` keeps it escaped.
|
|
33
|
+
if (canBeLiteral(node.value)) {
|
|
34
34
|
return `'${node.value}'`;
|
|
35
35
|
}
|
|
36
36
|
}
|
package/rules/sort-keys.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {compareKeys} from 'soml-lang';
|
|
2
|
-
import {isDisabledAnywhere, reorderMembers} from './utils/index.js';
|
|
2
|
+
import {escapeInvisibleCharacters, isDisabledAnywhere, reorderMembers} from './utils/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -147,8 +147,8 @@ const create = context => {
|
|
|
147
147
|
node: member.key,
|
|
148
148
|
messageId: MESSAGE_ID,
|
|
149
149
|
data: {
|
|
150
|
-
thisName: sourceCode.getText(member.key),
|
|
151
|
-
previousName: sourceCode.getText(previous.key),
|
|
150
|
+
thisName: escapeInvisibleCharacters(sourceCode.getText(member.key)),
|
|
151
|
+
previousName: escapeInvisibleCharacters(sourceCode.getText(previous.key)),
|
|
152
152
|
direction: direction === 'asc' ? 'ascending' : 'descending',
|
|
153
153
|
sensitivity: caseSensitive ? 'sensitive' : 'insensitive',
|
|
154
154
|
sortName: natural ? 'natural' : 'code point',
|
package/rules/spaced-comment.js
CHANGED
|
@@ -18,9 +18,9 @@ const BLOCK_START = /^\**(?=[^\s*])/v;
|
|
|
18
18
|
const BLOCK_END = /(?<=[^\s*])\**$/v;
|
|
19
19
|
|
|
20
20
|
/*
|
|
21
|
-
A line comment that is only a hex color, as in `#ff0000`, is almost always a color meant as an array item, which `#` turned into a comment. Adding a space would hide that mistake, so the rule leaves it, also with whitespace or another comment after it, as in `#ff0000 # red`.
|
|
21
|
+
A line comment that is only a hex color, as in `#ff0000`, is almost always a color meant as an array item, which `#` turned into a comment. Adding a space would hide that mistake, so the rule leaves it, also with a comma, whitespace, or another comment after it, as in `#ff0000,` or `#ff0000 # red`. The `s` flag lets that comment hold U+2028 or U+2029, which do not end a SOML line.
|
|
22
22
|
*/
|
|
23
|
-
const HEX_COLOR = /^(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})
|
|
23
|
+
const HEX_COLOR = /^(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?:[\t ]*,)?(?:[\t ]*$|[\t ]+(?:#|\/\*).*$)/isv;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
@param {SomlContext} context
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {invisibleCharacters, hasInvisibleCharacter} from './invisible-characters.js';
|
|
2
|
+
import hasDoubledBackslashes from './has-doubled-backslashes.js';
|
|
3
|
+
import getMeantEscape from './get-meant-escape.js';
|
|
2
4
|
|
|
3
5
|
const NOT_LITERAL = /[\p{Control}']/v;
|
|
4
6
|
|
|
@@ -13,13 +15,16 @@ export function toUnicodeEscape(character) {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
Write
|
|
18
|
+
Write line feeds and tabs in `text` as short escapes and every other invisible character as a `\u{…}` escape, such as for a decoded value in a message, where a control character could change the terminal and a bidirectional control could reorder the text.
|
|
17
19
|
|
|
18
20
|
@param {string} text
|
|
19
21
|
@returns {string}
|
|
20
22
|
*/
|
|
21
23
|
export function escapeInvisibleCharacters(text) {
|
|
22
|
-
return text
|
|
24
|
+
return text
|
|
25
|
+
.replaceAll(invisibleCharacters, character => toUnicodeEscape(character))
|
|
26
|
+
.replaceAll('\n', String.raw`\n`)
|
|
27
|
+
.replaceAll('\t', String.raw`\t`);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
@@ -39,11 +44,14 @@ export function toEscapedString(value) {
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
Whether a value should be written as a `'...'` string: it has no `'`, which would end it, and no line feed, tab, or other control character, which `'...'` cannot hold or which are invisible in it. A value with an invisible character also stays escaped, as `no-invisible-characters` asks. This is stricter than canonical form, which writes C1 controls and other invisible characters in `'...'`.
|
|
47
|
+
Whether a value should be written as a `'...'` string: it has no `'`, which would end it, and no line feed, tab, or other control character, which `'...'` cannot hold or which are invisible in it. A value with an invisible character also stays escaped, as `no-invisible-characters` asks. So does a value that reads as a mistake in `'...'`: one such as `^\\d+$`, which `no-doubled-backslashes-in-literal-strings` reports, and one such as `Done.\n`, with a backslash and `n`, which `no-escapes-in-literal-strings` reports. This is stricter than canonical form, which writes C1 controls and other invisible characters in `'...'`.
|
|
43
48
|
|
|
44
49
|
@param {string} value
|
|
45
50
|
@returns {boolean}
|
|
46
51
|
*/
|
|
47
52
|
export function canBeLiteral(value) {
|
|
48
|
-
return !NOT_LITERAL.test(value)
|
|
53
|
+
return !NOT_LITERAL.test(value)
|
|
54
|
+
&& !hasInvisibleCharacter(value)
|
|
55
|
+
&& !hasDoubledBackslashes(value)
|
|
56
|
+
&& getMeantEscape(value) === undefined;
|
|
49
57
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A backslash and what follows it, as a `"…"` string reads them. A `\u{…}` in another spelling, such as `\u{E9}` or `\u0041`, is not a SOML escape, so it goes to `other`, and so does a backslash at the end.
|
|
3
|
+
*/
|
|
4
|
+
const BACKSLASH = /\\(?:(?<escape>[\\nt]|u\{(?:0|[1-9a-f][\da-f]{0,5})\})|(?<other>.?))/gsv;
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
A Windows path, which starts with a drive, as in `C:\`, a network share, as in `\\server`, or a relative folder, as in `.\` and `..\`.
|
|
8
|
+
*/
|
|
9
|
+
const PATH = /^(?:[A-Za-z]:\\|\\\\|\.\.?\\)/v;
|
|
10
|
+
const WHITESPACE = /[\t ]/v;
|
|
11
|
+
const NAME_START = /[\p{Letter}\p{Number}]/v;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
The first escape in a value that was likely meant as one, such as `\n` in `Line one\nLine two`, or `undefined` when there is none. In a literal string, `'…'`, an escape is a backslash and the characters after it, as it is in TOML and in a shell, but in JavaScript and Python, `'…'` reads escapes.
|
|
15
|
+
|
|
16
|
+
A value is only checked when every backslash in it starts a SOML escape, so a regular expression such as `^\d+\n$`, which has `\d`, is not. A Windows path is not checked either, and neither is a value with a `"`, because it is code with strings in it, such as JSON text or a shell command with `printf "%s\n"`, which keeps its escapes for the program that runs it.
|
|
17
|
+
|
|
18
|
+
A `\u{…}` escape is always likely meant. A `\n` or `\t` is when the value has a space or a tab, as text does, or when no letter or digit follows it, as at the end of `Done.\n`. A `\n` or `\t` before a name without whitespace in the value, as in `src\test`, is more likely a path.
|
|
19
|
+
|
|
20
|
+
@param {string} value
|
|
21
|
+
@returns {string | undefined}
|
|
22
|
+
*/
|
|
23
|
+
export default function getMeantEscape(value) {
|
|
24
|
+
if (value.includes('"') || PATH.test(value)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const backslashes = value.matchAll(BACKSLASH).toArray();
|
|
29
|
+
|
|
30
|
+
if (backslashes.some(({groups}) => groups.escape === undefined)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasWhitespace = WHITESPACE.test(value);
|
|
35
|
+
// Only the character after the escape is read, so a value with many escapes takes linear time.
|
|
36
|
+
const isBeforeName = index => {
|
|
37
|
+
const codePoint = value.codePointAt(index);
|
|
38
|
+
return codePoint !== undefined && NAME_START.test(String.fromCodePoint(codePoint));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isMeant = ({0: escape, index}) => escape.startsWith(String.raw`\u`)
|
|
42
|
+
|| hasWhitespace
|
|
43
|
+
|| !isBeforeName(index + escape.length);
|
|
44
|
+
|
|
45
|
+
return backslashes.find(match => match[0] !== '\\\\' && isMeant(match))?.[0];
|
|
46
|
+
}
|
package/rules/utils/index.js
CHANGED
|
@@ -11,5 +11,6 @@ export {isComment, isPunctuator} from './token.js';
|
|
|
11
11
|
export {default as trimEnd} from './trim-end.js';
|
|
12
12
|
export {default as getLines} from './get-lines.js';
|
|
13
13
|
export {default as hasDoubledBackslashes} from './has-doubled-backslashes.js';
|
|
14
|
+
export {default as getMeantEscape} from './get-meant-escape.js';
|
|
14
15
|
export {default as getKeyWords} from './get-key-words.js';
|
|
15
16
|
export {default as isDisabledAnywhere} from './is-disabled-anywhere.js';
|
|
@@ -26,7 +26,7 @@ export function hasAdjacentComment(context, node) {
|
|
|
26
26
|
/**
|
|
27
27
|
A fix that puts `members` in the order of `ordered`, by replacing the text of each member with the text of the member that belongs there. The separators stay where they are. Object members have no order in SOML, so this keeps the value.
|
|
28
28
|
|
|
29
|
-
It aborts when a comment is next to any
|
|
29
|
+
It aborts when a comment is next to any member or inside a moved member, so comments stay where they are.
|
|
30
30
|
|
|
31
31
|
@param {SomlContext} context
|
|
32
32
|
@param {readonly MemberNode[]} members The members in source order.
|
|
@@ -39,9 +39,15 @@ export function reorderMembers(context, members, ordered) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
for (const [index, member] of members.entries()) {
|
|
42
|
-
if (ordered[index]
|
|
43
|
-
|
|
42
|
+
if (ordered[index] === member) {
|
|
43
|
+
continue;
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
if (context.sourceCode.getTokens(member, {includeComments: true}).some(token => isComment(token))) {
|
|
47
|
+
abort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
yield fixer.replaceText(member, context.sourceCode.getText(ordered[index]));
|
|
45
51
|
}
|
|
46
52
|
};
|
|
47
53
|
}
|