eslint-soml 0.0.1 → 0.0.2
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 +1 -0
- package/language/soml-source-code.js +3 -4
- package/package.json +7 -8
- package/readme.md +64 -31
- package/rules/format.js +14 -60
- package/rules/index.js +14 -0
- package/rules/key-name-casing.js +1 -2
- package/rules/no-block-string-edge-blank-lines.js +83 -0
- package/rules/no-block-string-trailing-whitespace.js +5 -6
- package/rules/no-confusable-keys.js +75 -0
- package/rules/no-decimal-file-modes.js +91 -0
- package/rules/no-doubled-backslashes-in-literal-strings.js +55 -0
- package/rules/no-duplicate-array-items.js +108 -0
- package/rules/no-empty-keys.js +5 -3
- package/rules/no-escapes-in-paths.js +95 -0
- package/rules/no-float-trailing-zeros.js +78 -0
- package/rules/no-invisible-characters.js +52 -31
- package/rules/no-loss-of-precision.js +6 -10
- package/rules/no-misplaced-block-string-delimiters.js +93 -0
- package/rules/no-mixed-number-types.js +8 -7
- package/rules/no-negative-zero.js +52 -0
- package/rules/no-numeric-dotted-keys.js +4 -16
- package/rules/no-placeholder-values.js +58 -0
- package/rules/no-quoted-keywords.js +61 -0
- package/rules/no-scattered-dotted-keys.js +102 -0
- package/rules/no-sizes-as-durations.js +121 -0
- package/rules/no-zero-duration-parts.js +57 -0
- package/rules/numeric-separators-style.js +17 -21
- package/rules/prefer-bare-keys.js +10 -9
- package/rules/prefer-literal-strings.js +4 -11
- package/rules/prefer-utc-instants.js +5 -3
- package/rules/rule/index.js +0 -6
- package/rules/rule/soml-context.js +3 -15
- package/rules/rule/soml-listeners.js +10 -23
- package/rules/rule/to-eslint-create.js +5 -9
- package/rules/rule/to-eslint-listener.js +2 -0
- package/rules/rule/to-eslint-problem.js +3 -1
- package/rules/rule/to-eslint-rule-fixer.js +5 -3
- package/rules/rule/to-eslint-rule.js +1 -1
- package/rules/rule/to-eslint-rules.js +11 -0
- package/rules/sort-keys.js +46 -56
- package/rules/spaced-comment.js +21 -10
- package/rules/utils/escape-string.js +12 -2
- package/rules/utils/get-documentation-url.js +3 -5
- package/rules/utils/get-key-words.js +9 -0
- package/rules/utils/has-doubled-backslashes.js +16 -0
- package/rules/utils/index.js +9 -2
- package/rules/utils/invisible-characters.js +4 -11
- package/rules/utils/is-decimal-point.js +12 -0
- package/rules/utils/reorder-members.js +47 -0
- package/rules/utils/token.js +24 -0
- package/rules/utils/trim-end.js +18 -0
- package/rules/utils/trim-whitespace.js +23 -0
package/index.d.ts
CHANGED
|
@@ -134,13 +134,12 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
134
134
|
@returns {Node | undefined}
|
|
135
135
|
*/
|
|
136
136
|
getParent(node) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
137
|
+
// The parents are recorded by the first traversal.
|
|
138
|
+
this.traverse();
|
|
141
139
|
return this.#parents.get(node);
|
|
142
140
|
}
|
|
143
141
|
|
|
142
|
+
// TODO: Add a `comments` property that holds `this.ast.comments` when ESLint ships https://github.com/eslint/rfcs/pull/150. It adds `comments` to `SourceCode` and `TextSourceCodeBase`, deprecates `getAllComments()`, and makes `no-irregular-whitespace` work on any language that has `comments`. Then use `comments` in the rules, too.
|
|
144
143
|
/**
|
|
145
144
|
@returns {readonly Comment[]}
|
|
146
145
|
*/
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-soml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "ESLint plugin for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"repository": "github:
|
|
6
|
+
"repository": "github:soml-lang/eslint-soml",
|
|
7
7
|
"funding": "https://github.com/sponsors/sindresorhus",
|
|
8
8
|
"author": "Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)",
|
|
9
9
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"sideEffects": false,
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
16
|
+
"node": "^22.13.0 || >=24"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
19
|
"create-rule": "node ./scripts/create-rule.js && npm run create-rules-index-file && npm run fix:eslint-docs",
|
|
@@ -53,20 +53,19 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@eslint/plugin-kit": "^0.7.3",
|
|
55
55
|
"natural-compare": "^1.4.0",
|
|
56
|
-
"soml-lang": "^0.0.
|
|
56
|
+
"soml-lang": "^0.0.3"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@babel/code-frame": "^8.0.6",
|
|
60
60
|
"@eslint-community/eslint-utils": "^4.10.1",
|
|
61
61
|
"@eslint/core": "^1.2.1",
|
|
62
|
-
"@typescript-eslint/types": "^8.71.
|
|
62
|
+
"@typescript-eslint/types": "^8.71.1",
|
|
63
63
|
"enquirer": "^2.4.1",
|
|
64
64
|
"eslint": "^10.12.0",
|
|
65
|
-
"eslint-config-xo": "^
|
|
65
|
+
"eslint-config-xo": "^5.0.0",
|
|
66
66
|
"eslint-doc-generator": "^3.7.1",
|
|
67
67
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
68
|
-
"eslint-plugin-jsdoc": "^65.
|
|
69
|
-
"globals": "^17.13.0",
|
|
68
|
+
"eslint-plugin-jsdoc": "^65.2.1",
|
|
70
69
|
"indent-string": "^5.0.0",
|
|
71
70
|
"markdownlint-cli": "^0.49.1",
|
|
72
71
|
"nano-spawn": "^2.1.0",
|
package/readme.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
> ESLint plugin for [SOML](https://github.com/soml-lang/soml), a config format for humans
|
|
4
4
|
|
|
5
|
-
Lint `.soml` files with ESLint. The plugin parses them with [`soml-lang`](https://github.com/sindresorhus/soml-js), the reference parser, so ESLint reports every syntax error the specification defines, and `# eslint-disable` comments work as in JavaScript.
|
|
6
|
-
|
|
7
5
|
[**Propose a new rule ➡**](.github/contributing.md)
|
|
8
6
|
|
|
9
7
|
*We do not accept pull requests because of too much AI slop.*
|
|
@@ -14,7 +12,9 @@ Lint `.soml` files with ESLint. The plugin parses them with [`soml-lang`](https:
|
|
|
14
12
|
npm install --save-dev eslint eslint-soml
|
|
15
13
|
```
|
|
16
14
|
|
|
17
|
-
**Requires Node.js
|
|
15
|
+
**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).**
|
|
16
|
+
|
|
17
|
+
The parser needs `Temporal` for instants and durations, and Node.js 26 and later have it built in. On older versions, linting a file with an instant or a duration fails, unless a [`Temporal` polyfill](https://github.com/fullcalendar/temporal-polyfill) is loaded first. Make `import 'temporal-polyfill/global';` the first import in `eslint.config.js`, before `eslint-soml`.
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
@@ -59,34 +59,73 @@ Disable a rule with a comment, in either comment style:
|
|
|
59
59
|
'404': 'Not found' /* eslint-disable-line soml/prefer-bare-keys */
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
The [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-disable-directives) and [`reportUnusedInlineConfigs`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-inline-configs) linter options work for `.soml` files, and so do the rules of [`@eslint-community/eslint-plugin-eslint-comments`](https://github.com/eslint-community/eslint-plugin-eslint-comments), such as `no-unlimited-disable`.
|
|
63
|
+
|
|
64
|
+
ESLint's [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) rule works too, with the [node types](#writing-rules) of the tree. For example, to disallow a key named `password`:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
import soml from 'eslint-soml';
|
|
68
|
+
import {defineConfig} from 'eslint/config';
|
|
69
|
+
|
|
70
|
+
export default defineConfig([
|
|
71
|
+
soml.configs.recommended,
|
|
72
|
+
{
|
|
73
|
+
files: ['**/*.soml'],
|
|
74
|
+
rules: {
|
|
75
|
+
'no-restricted-syntax': [
|
|
76
|
+
'error',
|
|
77
|
+
{
|
|
78
|
+
selector: 'Member > Key > KeySegment[value="password"]',
|
|
79
|
+
message: 'Read the password from the environment.',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
```
|
|
86
|
+
|
|
62
87
|
## Rules
|
|
63
88
|
|
|
64
89
|
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` -->
|
|
65
90
|
<!-- begin auto-generated rules list -->
|
|
66
91
|
|
|
67
|
-
💼 [Configurations](https://github.com/
|
|
68
|
-
✅ Set in the `recommended` [configuration](https://github.com/
|
|
69
|
-
☑️ Set in the `unopinionated` [configuration](https://github.com/
|
|
92
|
+
💼 [Configurations](https://github.com/soml-lang/eslint-soml#preset-configs) enabled in.\
|
|
93
|
+
✅ Set in the `recommended` [configuration](https://github.com/soml-lang/eslint-soml#preset-configs).\
|
|
94
|
+
☑️ Set in the `unopinionated` [configuration](https://github.com/soml-lang/eslint-soml#preset-configs).\
|
|
70
95
|
🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
|
|
71
96
|
💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
|
|
72
97
|
|
|
73
|
-
| Name
|
|
74
|
-
|
|
|
75
|
-
| [format](docs/rules/format.md)
|
|
76
|
-
| [key-name-casing](docs/rules/key-name-casing.md)
|
|
77
|
-
| [no-block-string-
|
|
78
|
-
| [no-
|
|
79
|
-
| [no-
|
|
80
|
-
| [no-
|
|
81
|
-
| [no-
|
|
82
|
-
| [no-
|
|
83
|
-
| [no-
|
|
84
|
-
| [
|
|
85
|
-
| [
|
|
86
|
-
| [
|
|
87
|
-
| [
|
|
88
|
-
| [
|
|
89
|
-
| [
|
|
98
|
+
| Name | Description | 💼 | 🔧 | 💡 |
|
|
99
|
+
| :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------ | :--- | :- | :- |
|
|
100
|
+
| [format](docs/rules/format.md) | Enforce the layout of the SOML formatter. | ✅ | 🔧 | |
|
|
101
|
+
| [key-name-casing](docs/rules/key-name-casing.md) | Enforce a casing style for keys. | | | |
|
|
102
|
+
| [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. | ✅ | 🔧 | |
|
|
103
|
+
| [no-block-string-trailing-whitespace](docs/rules/no-block-string-trailing-whitespace.md) | Disallow trailing whitespace in block strings. | ✅ ☑️ | | |
|
|
104
|
+
| [no-confusable-keys](docs/rules/no-confusable-keys.md) | Disallow keys that differ only in case, `-` and `_`, or surrounding whitespace. | ✅ ☑️ | | |
|
|
105
|
+
| [no-decimal-file-modes](docs/rules/no-decimal-file-modes.md) | Disallow file modes written as decimal numbers. | ✅ ☑️ | | 💡 |
|
|
106
|
+
| [no-doubled-backslashes-in-literal-strings](docs/rules/no-doubled-backslashes-in-literal-strings.md) | Disallow doubled backslashes in literal strings, such as `'^\\d+$'`. | ✅ | | 💡 |
|
|
107
|
+
| [no-duplicate-array-items](docs/rules/no-duplicate-array-items.md) | Disallow the same string twice in an array. | | | 💡 |
|
|
108
|
+
| [no-empty-keys](docs/rules/no-empty-keys.md) | Disallow empty and whitespace-only keys. | ✅ ☑️ | | |
|
|
109
|
+
| [no-escapes-in-paths](docs/rules/no-escapes-in-paths.md) | Disallow `\n` and `\t` escapes in Windows paths. | ✅ ☑️ | | 💡 |
|
|
110
|
+
| [no-float-trailing-zeros](docs/rules/no-float-trailing-zeros.md) | Disallow trailing zeros in the fraction of a float. | ✅ | | 💡 |
|
|
111
|
+
| [no-invisible-characters](docs/rules/no-invisible-characters.md) | Disallow invisible characters in strings, keys, and comments. | ✅ ☑️ | 🔧 | |
|
|
112
|
+
| [no-loss-of-precision](docs/rules/no-loss-of-precision.md) | Disallow floats that cannot be stored exactly as written. | ✅ ☑️ | | |
|
|
113
|
+
| [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. | ✅ ☑️ | | |
|
|
114
|
+
| [no-mixed-number-types](docs/rules/no-mixed-number-types.md) | Disallow arrays that mix ints and floats. | ✅ | | 💡 |
|
|
115
|
+
| [no-negative-zero](docs/rules/no-negative-zero.md) | Disallow a sign on zero. | ✅ | 🔧 | |
|
|
116
|
+
| [no-numeric-dotted-keys](docs/rules/no-numeric-dotted-keys.md) | Disallow dotted keys that read as one number. | ✅ ☑️ | | 💡 |
|
|
117
|
+
| [no-placeholder-values](docs/rules/no-placeholder-values.md) | Disallow placeholder values, such as `'changeme'`. | ✅ | | |
|
|
118
|
+
| [no-quoted-keywords](docs/rules/no-quoted-keywords.md) | Disallow the strings `'true'`, `'false'`, and `'null'`. | ✅ | | 💡 |
|
|
119
|
+
| [no-scattered-dotted-keys](docs/rules/no-scattered-dotted-keys.md) | Require the dotted keys under one prefix to be next to each other. | ✅ | 🔧 | |
|
|
120
|
+
| [no-sizes-as-durations](docs/rules/no-sizes-as-durations.md) | Disallow sizes that are durations, such as `memory: 512m`. | ✅ ☑️ | | 💡 |
|
|
121
|
+
| [no-unnormalized-keys](docs/rules/no-unnormalized-keys.md) | Disallow keys that are not in a Unicode normalization form. | ✅ ☑️ | | |
|
|
122
|
+
| [no-zero-duration-parts](docs/rules/no-zero-duration-parts.md) | Disallow duration parts that are zero. | ✅ | 🔧 | |
|
|
123
|
+
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the grouping of digits with `_` in decimal numbers. | ✅ | 🔧 | |
|
|
124
|
+
| [prefer-bare-keys](docs/rules/prefer-bare-keys.md) | Prefer bare keys when a key does not need quotes. | ✅ | 🔧 | |
|
|
125
|
+
| [prefer-literal-strings](docs/rules/prefer-literal-strings.md) | Prefer literal strings, `'…'`, when a string has no escapes. | ✅ | 🔧 | |
|
|
126
|
+
| [prefer-utc-instants](docs/rules/prefer-utc-instants.md) | Prefer instants in UTC. | | 🔧 | |
|
|
127
|
+
| [sort-keys](docs/rules/sort-keys.md) | Require object keys to be sorted. | | 🔧 | |
|
|
128
|
+
| [spaced-comment](docs/rules/spaced-comment.md) | Require a space after `#` and `/*`, and before `*/`. | ✅ | 🔧 | |
|
|
90
129
|
|
|
91
130
|
<!-- end auto-generated rules list -->
|
|
92
131
|
|
|
@@ -162,14 +201,8 @@ In Visual Studio Code, the [ESLint extension](https://marketplace.visualstudio.c
|
|
|
162
201
|
}
|
|
163
202
|
```
|
|
164
203
|
|
|
165
|
-
## Writing rules
|
|
166
|
-
|
|
167
|
-
Rules get the tree from [`parseTree()`](https://github.com/sindresorhus/soml-js#parsetreetext) in `soml-lang`. Its readme lists every node type. The node types are exported as TypeScript types from this package too.
|
|
168
|
-
|
|
169
|
-
`context.sourceCode` has `getParent()`, `getTokenBefore()`, `getTokenAfter()`, `getFirstToken()`, `getLastToken()`, and `getTokens()`, each token method with an `includeComments` option, and `getAllComments()`.
|
|
170
|
-
|
|
171
204
|
## Related
|
|
172
205
|
|
|
173
|
-
- [soml-
|
|
174
|
-
- [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) -
|
|
206
|
+
- [soml-javascript](https://github.com/soml-lang/soml-javascript) - The reference parser and serializer for SOML
|
|
207
|
+
- [eslint-plugin-unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn) - Powerful ESLint rules
|
|
175
208
|
- [eslint-package-json](https://github.com/sindresorhus/eslint-package-json) - Powerful ESLint rules for `package.json`
|
package/rules/format.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {formatEdits} from 'soml-lang';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -22,57 +22,6 @@ const showInvisibles = text => text
|
|
|
22
22
|
.replaceAll('\n', '⏎')
|
|
23
23
|
.replaceAll('\t', '↹');
|
|
24
24
|
|
|
25
|
-
const GAP_CHARACTERS = new Set([' ', '\t', '\n', ',']);
|
|
26
|
-
const isGapCharacter = character => GAP_CHARACTERS.has(character);
|
|
27
|
-
|
|
28
|
-
/*
|
|
29
|
-
The end of the run of spaces, tabs, line feeds, and commas at `index`.
|
|
30
|
-
*/
|
|
31
|
-
function skipGap(text, index) {
|
|
32
|
-
while (index < text.length && isGapCharacter(text[index])) {
|
|
33
|
-
index++;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return index;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/*
|
|
40
|
-
The changes from `text` to `formatted`, as ranges of `text` and what replaces them.
|
|
41
|
-
|
|
42
|
-
The formatter only changes spaces, tabs, line feeds, and commas, so without those the two texts are the same. Walking both at once, every run of those characters that differs is one change. This takes linear time, where a general diff takes quadratic time on a file with many changes.
|
|
43
|
-
*/
|
|
44
|
-
function * getChanges(text, formatted) {
|
|
45
|
-
let textIndex = 0;
|
|
46
|
-
let formattedIndex = 0;
|
|
47
|
-
|
|
48
|
-
while (textIndex < text.length || formattedIndex < formatted.length) {
|
|
49
|
-
const textGapEnd = skipGap(text, textIndex);
|
|
50
|
-
const formattedGapEnd = skipGap(formatted, formattedIndex);
|
|
51
|
-
const inserted = formatted.slice(formattedIndex, formattedGapEnd);
|
|
52
|
-
|
|
53
|
-
if (text.slice(textIndex, textGapEnd) !== inserted) {
|
|
54
|
-
yield {start: textIndex, end: textGapEnd, inserted};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
textIndex = textGapEnd;
|
|
58
|
-
formattedIndex = formattedGapEnd;
|
|
59
|
-
|
|
60
|
-
while (textIndex < text.length && !isGapCharacter(text[textIndex]) && text[textIndex] === formatted[formattedIndex]) {
|
|
61
|
-
textIndex++;
|
|
62
|
-
formattedIndex++;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Not expected, but if the formatter changed anything else, both stop at a different character, or one at its end, and the rest is one change.
|
|
66
|
-
const isTextStuck = textIndex === text.length || !isGapCharacter(text[textIndex]);
|
|
67
|
-
const isFormattedStuck = formattedIndex === formatted.length || !isGapCharacter(formatted[formattedIndex]);
|
|
68
|
-
|
|
69
|
-
if (isTextStuck && isFormattedStuck && (textIndex < text.length || formattedIndex < formatted.length)) {
|
|
70
|
-
yield {start: textIndex, end: text.length, inserted: formatted.slice(formattedIndex)};
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
25
|
/**
|
|
77
26
|
@param {SomlContext} context
|
|
78
27
|
*/
|
|
@@ -81,13 +30,11 @@ const create = context => {
|
|
|
81
30
|
|
|
82
31
|
context.on('Document', function * () {
|
|
83
32
|
const {text} = sourceCode;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
if (formatted === text) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
33
|
+
const edits = formatEdits(text);
|
|
89
34
|
|
|
90
|
-
|
|
35
|
+
// eslint-disable-next-line internal/no-restricted-property-access -- An edit is not a node.
|
|
36
|
+
for (const [index, {range, text: inserted}] of edits.entries()) {
|
|
37
|
+
const [start, end] = range;
|
|
91
38
|
const deleted = text.slice(start, end);
|
|
92
39
|
let messageId = MESSAGE_ID_REPLACE;
|
|
93
40
|
|
|
@@ -107,8 +54,15 @@ const create = context => {
|
|
|
107
54
|
deleted: showInvisibles(deleted),
|
|
108
55
|
inserted: showInvisibles(inserted),
|
|
109
56
|
},
|
|
110
|
-
//
|
|
111
|
-
fix:
|
|
57
|
+
// Every edit is in one fix, so the formatting is applied at once. Applying only some edits, when a fix from another rule overlaps one of them, could leave a block string half moved. Only the first problem carries the fix, because a copy of every edit in every problem would make the output grow with the file size times the problem count.
|
|
58
|
+
fix: index === 0
|
|
59
|
+
? function * (fixer) {
|
|
60
|
+
// eslint-disable-next-line internal/no-restricted-property-access -- An edit is not a node.
|
|
61
|
+
for (const {range, text} of edits) {
|
|
62
|
+
yield fixer.replaceTextRange(range, text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
: undefined,
|
|
112
66
|
};
|
|
113
67
|
}
|
|
114
68
|
});
|
package/rules/index.js
CHANGED
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
export {default as format} from './format.js';
|
|
4
4
|
export {default as 'key-name-casing'} from './key-name-casing.js';
|
|
5
|
+
export {default as 'no-block-string-edge-blank-lines'} from './no-block-string-edge-blank-lines.js';
|
|
5
6
|
export {default as 'no-block-string-trailing-whitespace'} from './no-block-string-trailing-whitespace.js';
|
|
7
|
+
export {default as 'no-confusable-keys'} from './no-confusable-keys.js';
|
|
8
|
+
export {default as 'no-decimal-file-modes'} from './no-decimal-file-modes.js';
|
|
9
|
+
export {default as 'no-doubled-backslashes-in-literal-strings'} from './no-doubled-backslashes-in-literal-strings.js';
|
|
10
|
+
export {default as 'no-duplicate-array-items'} from './no-duplicate-array-items.js';
|
|
6
11
|
export {default as 'no-empty-keys'} from './no-empty-keys.js';
|
|
12
|
+
export {default as 'no-escapes-in-paths'} from './no-escapes-in-paths.js';
|
|
13
|
+
export {default as 'no-float-trailing-zeros'} from './no-float-trailing-zeros.js';
|
|
7
14
|
export {default as 'no-invisible-characters'} from './no-invisible-characters.js';
|
|
8
15
|
export {default as 'no-loss-of-precision'} from './no-loss-of-precision.js';
|
|
16
|
+
export {default as 'no-misplaced-block-string-delimiters'} from './no-misplaced-block-string-delimiters.js';
|
|
9
17
|
export {default as 'no-mixed-number-types'} from './no-mixed-number-types.js';
|
|
18
|
+
export {default as 'no-negative-zero'} from './no-negative-zero.js';
|
|
10
19
|
export {default as 'no-numeric-dotted-keys'} from './no-numeric-dotted-keys.js';
|
|
20
|
+
export {default as 'no-placeholder-values'} from './no-placeholder-values.js';
|
|
21
|
+
export {default as 'no-quoted-keywords'} from './no-quoted-keywords.js';
|
|
22
|
+
export {default as 'no-scattered-dotted-keys'} from './no-scattered-dotted-keys.js';
|
|
23
|
+
export {default as 'no-sizes-as-durations'} from './no-sizes-as-durations.js';
|
|
11
24
|
export {default as 'no-unnormalized-keys'} from './no-unnormalized-keys.js';
|
|
25
|
+
export {default as 'no-zero-duration-parts'} from './no-zero-duration-parts.js';
|
|
12
26
|
export {default as 'numeric-separators-style'} from './numeric-separators-style.js';
|
|
13
27
|
export {default as 'prefer-bare-keys'} from './prefer-bare-keys.js';
|
|
14
28
|
export {default as 'prefer-literal-strings'} from './prefer-literal-strings.js';
|
package/rules/key-name-casing.js
CHANGED
|
@@ -25,8 +25,7 @@ const CASES = {
|
|
|
25
25
|
const create = context => {
|
|
26
26
|
const [{case: keyCase, ignore}] = context.options;
|
|
27
27
|
const pattern = CASES[keyCase];
|
|
28
|
-
|
|
29
|
-
const ignorePatterns = ignore.map(source => new RegExp(source, 'u'));
|
|
28
|
+
const ignorePatterns = ignore.map(source => new RegExp(source, 'v'));
|
|
30
29
|
|
|
31
30
|
context.on('KeySegment', node => {
|
|
32
31
|
// A quoted key holds data rather than a name, such as a path or a URL, so it is not checked.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {trimEnd} 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-block-string-edge-blank-lines';
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: 'Blank lines at the {{edge}} of a block string are not part of the value. To {{edge}} the value with a line break, write `\\n` in a `"""` block string.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const isBlank = line => trimEnd(line, ' \t') === '';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
@param {SomlContext} context
|
|
17
|
+
*/
|
|
18
|
+
const create = context => {
|
|
19
|
+
const {sourceCode} = context;
|
|
20
|
+
|
|
21
|
+
context.on('String', function * (node) {
|
|
22
|
+
if (!node.block) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = sourceCode.getText(node).split('\n');
|
|
27
|
+
// The first line holds the opening delimiter, and the last line the closing one.
|
|
28
|
+
const content = lines.slice(1, -1);
|
|
29
|
+
// Where each content line starts, and then where the closing line starts.
|
|
30
|
+
const lineStarts = [sourceCode.getRange(node)[0] + lines[0].length + 1];
|
|
31
|
+
|
|
32
|
+
for (const line of content) {
|
|
33
|
+
lineStarts.push(lineStarts.at(-1) + line.length + 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const firstContent = content.findIndex(line => !isBlank(line));
|
|
37
|
+
// With no content, every line is a leading blank line.
|
|
38
|
+
const startOfContent = firstContent === -1 ? content.length : firstContent;
|
|
39
|
+
const endOfContent = Math.max(content.findLastIndex(line => !isBlank(line)) + 1, startOfContent);
|
|
40
|
+
|
|
41
|
+
// Each range covers whole lines, with their line breaks.
|
|
42
|
+
const edges = [
|
|
43
|
+
['start', [lineStarts[0], lineStarts[startOfContent]]],
|
|
44
|
+
['end', [lineStarts[endOfContent], lineStarts.at(-1)]],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const [edge, range] of edges) {
|
|
48
|
+
if (range[0] === range[1]) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
yield {
|
|
53
|
+
loc: {
|
|
54
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
55
|
+
// Not the line break after the last blank line.
|
|
56
|
+
end: sourceCode.getLocFromIndex(range[1] - 1),
|
|
57
|
+
},
|
|
58
|
+
messageId: MESSAGE_ID,
|
|
59
|
+
data: {edge},
|
|
60
|
+
fix: fixer => fixer.removeRange(range),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
@type {SomlRule}
|
|
68
|
+
*/
|
|
69
|
+
const config = {
|
|
70
|
+
create,
|
|
71
|
+
meta: {
|
|
72
|
+
type: 'suggestion',
|
|
73
|
+
docs: {
|
|
74
|
+
description: 'Disallow blank lines at the start and end of block strings.',
|
|
75
|
+
recommended: true,
|
|
76
|
+
},
|
|
77
|
+
fixable: 'code',
|
|
78
|
+
messages,
|
|
79
|
+
languages: ['soml/soml'],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default config;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {trimEnd} from './utils/index.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
3
5
|
@import {SomlRule} from './rule/to-eslint-rule.js';
|
|
@@ -8,9 +10,6 @@ const messages = {
|
|
|
8
10
|
[MESSAGE_ID]: 'Whitespace at the end of a line in a block string is part of the value, and editors often remove it.',
|
|
9
11
|
};
|
|
10
12
|
|
|
11
|
-
const TRAILING_WHITESPACE = /[\t ]+$/v;
|
|
12
|
-
const BLANK_LINE = /^[\t ]*$/v;
|
|
13
|
-
|
|
14
13
|
/**
|
|
15
14
|
@param {SomlContext} context
|
|
16
15
|
*/
|
|
@@ -27,13 +26,13 @@ const create = context => {
|
|
|
27
26
|
|
|
28
27
|
// The first line holds the opening delimiter, and the last line the closing one.
|
|
29
28
|
for (const line of lines.slice(1, -1)) {
|
|
30
|
-
const
|
|
29
|
+
const content = trimEnd(line, ' \t');
|
|
31
30
|
|
|
32
31
|
// A blank line becomes an empty line, so its whitespace is not part of the value.
|
|
33
|
-
if (
|
|
32
|
+
if (content !== '' && content.length < line.length) {
|
|
34
33
|
yield {
|
|
35
34
|
loc: {
|
|
36
|
-
start: sourceCode.getLocFromIndex(offset +
|
|
35
|
+
start: sourceCode.getLocFromIndex(offset + content.length),
|
|
37
36
|
end: sourceCode.getLocFromIndex(offset + line.length),
|
|
38
37
|
},
|
|
39
38
|
messageId: MESSAGE_ID,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {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-confusable-keys';
|
|
9
|
+
const messages = {
|
|
10
|
+
[MESSAGE_ID]: '`{{key}}` and `{{otherKey}}` are one key to a program that ignores case, `-` versus `_`, or surrounding whitespace.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
What many programs compare keys by: case-insensitive, with `-` and `_` the same, and without surrounding whitespace.
|
|
15
|
+
*/
|
|
16
|
+
const fold = key => trimWhitespace(key).toLowerCase().replaceAll('-', '_');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
@param {SomlContext} context
|
|
20
|
+
*/
|
|
21
|
+
const create = context => {
|
|
22
|
+
context.on('Object', function * (node) {
|
|
23
|
+
/*
|
|
24
|
+
The first key seen for each folded key, as a tree that follows the key paths. A dotted key adds a key to each object on its path, so the keys under two spellings of one prefix are compared as one object.
|
|
25
|
+
*/
|
|
26
|
+
const root = new Map();
|
|
27
|
+
|
|
28
|
+
for (const {key} of node.members) {
|
|
29
|
+
let keys = root;
|
|
30
|
+
|
|
31
|
+
for (const segment of key.segments) {
|
|
32
|
+
const folded = fold(segment.value);
|
|
33
|
+
let entry = keys.get(folded);
|
|
34
|
+
|
|
35
|
+
if (entry === undefined) {
|
|
36
|
+
entry = {first: segment, keys: new Map(), reported: new Set()};
|
|
37
|
+
keys.set(folded, entry);
|
|
38
|
+
} else if (entry.first.value !== segment.value && !entry.reported.has(segment.value)) {
|
|
39
|
+
// Each pair of spellings is reported once, even when a dotted key repeats one of them.
|
|
40
|
+
entry.reported.add(segment.value);
|
|
41
|
+
|
|
42
|
+
yield {
|
|
43
|
+
node: segment,
|
|
44
|
+
messageId: MESSAGE_ID,
|
|
45
|
+
// The source text shows the quotes, so a key with whitespace stays readable.
|
|
46
|
+
data: {
|
|
47
|
+
key: context.sourceCode.getText(segment),
|
|
48
|
+
otherKey: context.sourceCode.getText(entry.first),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
keys = entry.keys;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
@type {SomlRule}
|
|
61
|
+
*/
|
|
62
|
+
const config = {
|
|
63
|
+
create,
|
|
64
|
+
meta: {
|
|
65
|
+
type: 'problem',
|
|
66
|
+
docs: {
|
|
67
|
+
description: 'Disallow keys that differ only in case, `-` and `_`, or surrounding whitespace.',
|
|
68
|
+
recommended: 'unopinionated',
|
|
69
|
+
},
|
|
70
|
+
messages,
|
|
71
|
+
languages: ['soml/soml'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default config;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import {SomlContext} from './rule/soml-context.js';
|
|
3
|
+
@import {SomlRule} from './rule/to-eslint-rule.js';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MESSAGE_ID_ERROR = 'no-decimal-file-modes/error';
|
|
7
|
+
const MESSAGE_ID_SUGGESTION = 'no-decimal-file-modes/suggestion';
|
|
8
|
+
const messages = {
|
|
9
|
+
[MESSAGE_ID_ERROR]: '`{{number}}` is a decimal number, so this file mode is `{{octal}}`. A file mode is octal, as in `0o{{number}}`.',
|
|
10
|
+
[MESSAGE_ID_SUGGESTION]: 'Write it as `0o{{number}}`.',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
A key for a file mode: `mode`, `perm`, `perms`, `permissions`, or `chmod`, alone or after `-` or `_` (`file-mode`), or `Mode` at the end of a camelCase key (`defaultMode`). A umask is left out, because it lists the permissions to remove, so what makes a mode sensible does not apply to it.
|
|
15
|
+
*/
|
|
16
|
+
const MODE_KEY = /(?:^|[\-_])(?:chmod|mode|perms?|permissions)$|[a-z]Mode$/v;
|
|
17
|
+
const OCTAL_DIGITS = /^[0-7]{3,4}$/v;
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
Whether a number is a sensible file mode, as ansible-lint's `risky-octal` rule decides: no write or execute permission without read permission, except execute alone when the owner can execute, and the owner has at least the permissions of the group, and the group at least those of others.
|
|
21
|
+
*/
|
|
22
|
+
function isSensibleMode(mode) {
|
|
23
|
+
const user = Math.trunc(mode / 64) % 8;
|
|
24
|
+
const group = Math.trunc(mode / 8) % 8;
|
|
25
|
+
const other = mode % 8;
|
|
26
|
+
const hasWriteWithoutRead = (digit, isUser = false) => digit !== 0 && digit < 4 && !(digit === 1 && (isUser || user % 2 === 1));
|
|
27
|
+
|
|
28
|
+
return !hasWriteWithoutRead(user, true)
|
|
29
|
+
&& !hasWriteWithoutRead(group)
|
|
30
|
+
&& !hasWriteWithoutRead(other)
|
|
31
|
+
&& user >= group
|
|
32
|
+
&& group >= other;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
@param {SomlContext} context
|
|
37
|
+
*/
|
|
38
|
+
const create = context => {
|
|
39
|
+
const {sourceCode} = context;
|
|
40
|
+
|
|
41
|
+
context.on('Member', member => {
|
|
42
|
+
const {key, value} = member;
|
|
43
|
+
|
|
44
|
+
if (value.type !== 'Integer' || value.radix !== 10 || !MODE_KEY.test(key.segments.at(-1).value)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const number = sourceCode.getText(value);
|
|
49
|
+
const decimal = Number(value.value);
|
|
50
|
+
|
|
51
|
+
// The digits look octal, and the value makes no sense as a mode, but the same digits read as octal do.
|
|
52
|
+
if (!OCTAL_DIGITS.test(number) || isSensibleMode(decimal) || !isSensibleMode(Number.parseInt(number, 8))) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
node: value,
|
|
58
|
+
messageId: MESSAGE_ID_ERROR,
|
|
59
|
+
data: {
|
|
60
|
+
number,
|
|
61
|
+
octal: `0o${decimal.toString(8)}`,
|
|
62
|
+
},
|
|
63
|
+
suggest: [
|
|
64
|
+
{
|
|
65
|
+
messageId: MESSAGE_ID_SUGGESTION,
|
|
66
|
+
data: {number},
|
|
67
|
+
fix: fixer => fixer.replaceText(value, `0o${number}`),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
@type {SomlRule}
|
|
76
|
+
*/
|
|
77
|
+
const config = {
|
|
78
|
+
create,
|
|
79
|
+
meta: {
|
|
80
|
+
type: 'problem',
|
|
81
|
+
docs: {
|
|
82
|
+
description: 'Disallow file modes written as decimal numbers.',
|
|
83
|
+
recommended: 'unopinionated',
|
|
84
|
+
},
|
|
85
|
+
hasSuggestions: true,
|
|
86
|
+
messages,
|
|
87
|
+
languages: ['soml/soml'],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default config;
|