eslint-soml 0.0.3 → 0.0.4
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 +199 -0
- package/language/soml-source-code.js +64 -1
- package/package.json +4 -4
- package/readme.md +94 -4
- package/rules/no-block-string-edge-blank-lines.js +4 -8
- package/rules/no-block-string-trailing-whitespace.js +6 -11
- package/rules/no-duplicate-array-items.js +3 -3
- package/rules/no-loss-of-precision.js +1 -1
- package/rules/no-misplaced-block-string-delimiters.js +35 -34
- package/rules/prefer-literal-strings.js +1 -1
- package/rules/rule/to-eslint-problem.js +1 -0
- package/rules/sort-keys.js +7 -5
- package/rules/utils/get-key-words.js +2 -2
- package/rules/utils/get-lines.js +24 -0
- package/rules/utils/index.js +1 -0
- package/rules/utils/invisible-characters.js +3 -2
- package/rules/utils/is-disabled-anywhere.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import type {ESLint, Linter} from 'eslint';
|
|
2
|
+
import type {LanguageOptions, RuleVisitor} from '@eslint/core';
|
|
3
|
+
import type {
|
|
4
|
+
CustomRuleDefinitionType,
|
|
5
|
+
CustomRuleTypeDefinitions,
|
|
6
|
+
CustomRuleVisitorWithExit,
|
|
7
|
+
TextSourceCodeBase,
|
|
8
|
+
} from '@eslint/plugin-kit';
|
|
9
|
+
import type {
|
|
10
|
+
Node,
|
|
11
|
+
DocumentNode,
|
|
12
|
+
ObjectNode,
|
|
13
|
+
MemberNode,
|
|
14
|
+
ArrayNode,
|
|
15
|
+
KeyNode,
|
|
16
|
+
StringNode,
|
|
17
|
+
IntegerNode,
|
|
18
|
+
FloatNode,
|
|
19
|
+
BooleanNode,
|
|
20
|
+
NullNode,
|
|
21
|
+
InstantNode,
|
|
22
|
+
DurationNode,
|
|
23
|
+
Token,
|
|
24
|
+
Comment,
|
|
25
|
+
} from 'soml-lang';
|
|
2
26
|
|
|
3
27
|
export type {
|
|
4
28
|
Node,
|
|
@@ -22,6 +46,181 @@ export type {
|
|
|
22
46
|
SourceLocation,
|
|
23
47
|
} from 'soml-lang';
|
|
24
48
|
|
|
49
|
+
/**
|
|
50
|
+
A node, a token, or a comment of a SOML document.
|
|
51
|
+
*/
|
|
52
|
+
export type SomlSyntaxElement = Node | Token | Comment;
|
|
53
|
+
|
|
54
|
+
type TokenOptions = {
|
|
55
|
+
/**
|
|
56
|
+
Whether comments count as tokens.
|
|
57
|
+
|
|
58
|
+
@default false
|
|
59
|
+
*/
|
|
60
|
+
readonly includeComments?: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type TokenOptionsWithoutComments = TokenOptions & {readonly includeComments?: false};
|
|
64
|
+
type TokenOptionsWithComments = TokenOptions & {readonly includeComments: true};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
The source code of a SOML document, which a rule gets as `context.sourceCode`.
|
|
68
|
+
|
|
69
|
+
It has the methods of `TextSourceCodeBase` from `@eslint/plugin-kit`, such as `getText()`, `getLoc()`, `getRange()`, and `getLocFromIndex()`, and the methods below. A token method finds a token in source order and returns `null` when there is none, like ESLint's own `SourceCode`. It takes only the `includeComments` option.
|
|
70
|
+
|
|
71
|
+
@example
|
|
72
|
+
```
|
|
73
|
+
import type {SomlRuleDefinition} from 'eslint-soml';
|
|
74
|
+
|
|
75
|
+
const rule: SomlRuleDefinition = {
|
|
76
|
+
meta: {
|
|
77
|
+
type: 'problem',
|
|
78
|
+
messages: {
|
|
79
|
+
trailingComma: 'Remove the comma after the last item.',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
create(context) {
|
|
83
|
+
return {
|
|
84
|
+
Array(node) {
|
|
85
|
+
const lastToken = context.sourceCode.getLastToken(node);
|
|
86
|
+
const beforeLast = lastToken && context.sourceCode.getTokenBefore(lastToken);
|
|
87
|
+
|
|
88
|
+
if (beforeLast?.value === ',') {
|
|
89
|
+
context.report({loc: beforeLast.loc, messageId: 'trailingComma'});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
*/
|
|
97
|
+
export interface SomlSourceCode extends TextSourceCodeBase<{
|
|
98
|
+
LangOptions: LanguageOptions;
|
|
99
|
+
RootNode: DocumentNode;
|
|
100
|
+
SyntaxElementWithLoc: SomlSyntaxElement;
|
|
101
|
+
ConfigNode: Comment;
|
|
102
|
+
}> {
|
|
103
|
+
/**
|
|
104
|
+
Marks the source code as SOML, so a rule that works on several languages can tell, as with `context.sourceCode.parserServices?.isSOML`.
|
|
105
|
+
*/
|
|
106
|
+
readonly parserServices: {readonly isSOML: true};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
The parent of a node, or `undefined` for the `Document` node. Nodes have no `parent` property.
|
|
110
|
+
*/
|
|
111
|
+
getParent(node: Node): Node | undefined;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
Every comment, in source order.
|
|
115
|
+
*/
|
|
116
|
+
getAllComments(): readonly Comment[];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
The token before a node, token, or comment.
|
|
120
|
+
*/
|
|
121
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options?: TokenOptionsWithoutComments): Token | null;
|
|
122
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options: TokenOptionsWithComments): Token | Comment | null;
|
|
123
|
+
getTokenBefore(nodeOrToken: SomlSyntaxElement, options?: TokenOptions): Token | Comment | null;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
The token after a node, token, or comment.
|
|
127
|
+
*/
|
|
128
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options?: TokenOptionsWithoutComments): Token | null;
|
|
129
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options: TokenOptionsWithComments): Token | Comment | null;
|
|
130
|
+
getTokenAfter(nodeOrToken: SomlSyntaxElement, options?: TokenOptions): Token | Comment | null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
The first token inside a node.
|
|
134
|
+
*/
|
|
135
|
+
getFirstToken(node: Node, options?: TokenOptionsWithoutComments): Token | null;
|
|
136
|
+
getFirstToken(node: Node, options: TokenOptionsWithComments): Token | Comment | null;
|
|
137
|
+
getFirstToken(node: Node, options?: TokenOptions): Token | Comment | null;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
The last token inside a node.
|
|
141
|
+
*/
|
|
142
|
+
getLastToken(node: Node, options?: TokenOptionsWithoutComments): Token | null;
|
|
143
|
+
getLastToken(node: Node, options: TokenOptionsWithComments): Token | Comment | null;
|
|
144
|
+
getLastToken(node: Node, options?: TokenOptions): Token | Comment | null;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
Every token inside a node, in source order.
|
|
148
|
+
*/
|
|
149
|
+
getTokens(node: Node, options?: TokenOptionsWithoutComments): Token[];
|
|
150
|
+
getTokens(node: Node, options: TokenOptionsWithComments): Array<Token | Comment>;
|
|
151
|
+
getTokens(node: Node, options?: TokenOptions): Array<Token | Comment>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
type ValueParent = MemberNode | ArrayNode;
|
|
155
|
+
type CollectionParent = DocumentNode | ValueParent;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
The listeners that a rule for SOML returns, with the type of each node and of its parent.
|
|
159
|
+
*/
|
|
160
|
+
export interface SomlRuleVisitor extends RuleVisitor, CustomRuleVisitorWithExit<{
|
|
161
|
+
Document?(node: DocumentNode): void;
|
|
162
|
+
Object?(node: ObjectNode, parent: CollectionParent): void;
|
|
163
|
+
Member?(node: MemberNode, parent: ObjectNode): void;
|
|
164
|
+
Key?(node: KeyNode, parent: MemberNode): void;
|
|
165
|
+
Array?(node: ArrayNode, parent: CollectionParent): void;
|
|
166
|
+
String?(node: StringNode, parent: ValueParent): void;
|
|
167
|
+
Integer?(node: IntegerNode, parent: ValueParent): void;
|
|
168
|
+
Float?(node: FloatNode, parent: ValueParent): void;
|
|
169
|
+
Boolean?(node: BooleanNode, parent: ValueParent): void;
|
|
170
|
+
Null?(node: NullNode, parent: ValueParent): void;
|
|
171
|
+
Instant?(node: InstantNode, parent: ValueParent): void;
|
|
172
|
+
Duration?(node: DurationNode, parent: ValueParent): void;
|
|
173
|
+
}> {}
|
|
174
|
+
|
|
175
|
+
export type SomlRuleDefinitionTypeOptions = CustomRuleTypeDefinitions;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
The type of a rule for SOML files, which gets a `SomlSourceCode` and returns a `SomlRuleVisitor`.
|
|
179
|
+
|
|
180
|
+
@example
|
|
181
|
+
```
|
|
182
|
+
import type {SomlRuleDefinition} from 'eslint-soml';
|
|
183
|
+
|
|
184
|
+
const rule: SomlRuleDefinition = {
|
|
185
|
+
meta: {
|
|
186
|
+
type: 'suggestion',
|
|
187
|
+
messages: {
|
|
188
|
+
tooLong: 'The key `{{key}}` is longer than 40 characters.',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
create(context) {
|
|
192
|
+
return {
|
|
193
|
+
Key(node) {
|
|
194
|
+
if (node.value.length > 40) {
|
|
195
|
+
context.report({node, messageId: 'tooLong', data: {key: node.value}});
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
*/
|
|
203
|
+
export type SomlRuleDefinition<Options extends Partial<SomlRuleDefinitionTypeOptions> = {}> = CustomRuleDefinitionType<{
|
|
204
|
+
LangOptions: LanguageOptions;
|
|
205
|
+
Code: SomlSourceCode;
|
|
206
|
+
Visitor: SomlRuleVisitor;
|
|
207
|
+
Node: SomlSyntaxElement;
|
|
208
|
+
}, Options>;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
The `context` of a rule for SOML files, for a helper that a rule passes it to.
|
|
212
|
+
|
|
213
|
+
@example
|
|
214
|
+
```
|
|
215
|
+
import type {SomlRuleContext, StringNode} from 'eslint-soml';
|
|
216
|
+
|
|
217
|
+
function isWrittenEscaped(context: SomlRuleContext, node: StringNode): boolean {
|
|
218
|
+
return context.sourceCode.getText(node).startsWith('"');
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
*/
|
|
222
|
+
export type SomlRuleContext<Options extends Partial<SomlRuleDefinitionTypeOptions> = {}> = Parameters<SomlRuleDefinition<Options>['create']>[0];
|
|
223
|
+
|
|
25
224
|
declare const eslintSoml: ESLint.Plugin & {
|
|
26
225
|
configs: {
|
|
27
226
|
recommended: Linter.Config;
|
|
@@ -13,6 +13,15 @@ import {visitorKeys} from 'soml-lang';
|
|
|
13
13
|
|
|
14
14
|
const commentParser = new ConfigCommentParser();
|
|
15
15
|
const INLINE_CONFIG = /^\s*eslint(?:-enable|-disable(?:(?:-next)?-line)?)?(?:\s|$)/v;
|
|
16
|
+
const RULE_CONFIG = /^\s*eslint(?:\s|$)/v;
|
|
17
|
+
// What separates a directive from its justification, as ESLint splits it.
|
|
18
|
+
const JUSTIFICATION = /\s-{2,}\s/v;
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
Whether a comment is a directive. A `#` comment is the usual comment style, so one that starts with `eslint` in prose, as in `# eslint version to install`, is not a rule configuration. A rule configuration always has a `:`, so a `#` comment without one before its justification is not a directive.
|
|
22
|
+
*/
|
|
23
|
+
const isInlineConfig = comment => INLINE_CONFIG.test(comment.value)
|
|
24
|
+
&& !(comment.type === 'Line' && RULE_CONFIG.test(comment.value) && !comment.value.split(JUSTIFICATION, 1)[0].includes(':'));
|
|
16
25
|
|
|
17
26
|
const isComment = item => item.type === 'Line' || item.type === 'Block';
|
|
18
27
|
|
|
@@ -58,10 +67,20 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
58
67
|
*/
|
|
59
68
|
#inlineConfigComments;
|
|
60
69
|
/**
|
|
70
|
+
Where each line starts, found once, when it is first needed.
|
|
71
|
+
|
|
72
|
+
@type {number[] | undefined}
|
|
73
|
+
*/
|
|
74
|
+
#lineStarts;
|
|
75
|
+
/**
|
|
61
76
|
@type {DocumentNode}
|
|
62
77
|
*/
|
|
63
78
|
ast;
|
|
64
79
|
visitorKeys = visitorKeys;
|
|
80
|
+
/**
|
|
81
|
+
Marks the source code as SOML, so a rule that works on several languages can tell, as `eslint-plugin-yml` and `eslint-plugin-toml` do with `isYAML` and `isTOML`.
|
|
82
|
+
*/
|
|
83
|
+
parserServices = {isSOML: true};
|
|
65
84
|
|
|
66
85
|
/**
|
|
67
86
|
@param {{text: string, ast: DocumentNode}} options
|
|
@@ -113,6 +132,49 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
113
132
|
return null;
|
|
114
133
|
}
|
|
115
134
|
|
|
135
|
+
/**
|
|
136
|
+
The 1-based line and 0-based column of an offset.
|
|
137
|
+
|
|
138
|
+
This replaces the method of `TextSourceCodeBase`, which searches the rest of the text for a line feed again for every offset on the last line of a file with no line feed at the end, so a rule with many problems on that line takes quadratic time.
|
|
139
|
+
|
|
140
|
+
@param {number} index
|
|
141
|
+
@returns {{line: number, column: number}}
|
|
142
|
+
*/
|
|
143
|
+
getLocFromIndex(index) {
|
|
144
|
+
if (typeof index !== 'number') {
|
|
145
|
+
throw new TypeError('Expected `index` to be a number.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (index < 0 || index > this.text.length) {
|
|
149
|
+
throw new RangeError(`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`);
|
|
150
|
+
}
|
|
151
|
+
|
|
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
|
+
// The last line that starts at or before `index`.
|
|
161
|
+
const lineStarts = this.#lineStarts;
|
|
162
|
+
let low = 0;
|
|
163
|
+
let high = lineStarts.length - 1;
|
|
164
|
+
|
|
165
|
+
while (low < high) {
|
|
166
|
+
const middle = Math.ceil((low + high) / 2);
|
|
167
|
+
|
|
168
|
+
if (lineStarts[middle] <= index) {
|
|
169
|
+
low = middle;
|
|
170
|
+
} else {
|
|
171
|
+
high = middle - 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {line: low + 1, column: index - lineStarts[low]};
|
|
176
|
+
}
|
|
177
|
+
|
|
116
178
|
/**
|
|
117
179
|
@returns {Iterable<VisitNodeStep>}
|
|
118
180
|
*/
|
|
@@ -162,6 +224,7 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
// 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.
|
|
227
|
+
|
|
165
228
|
/**
|
|
166
229
|
@returns {readonly Comment[]}
|
|
167
230
|
*/
|
|
@@ -173,7 +236,7 @@ export default class SomlSourceCode extends TextSourceCodeBase {
|
|
|
173
236
|
@returns {Comment[]}
|
|
174
237
|
*/
|
|
175
238
|
getInlineConfigNodes() {
|
|
176
|
-
this.#inlineConfigComments ??= this.ast.comments.filter(comment =>
|
|
239
|
+
this.#inlineConfigComments ??= this.ast.comments.filter(comment => isInlineConfig(comment));
|
|
177
240
|
return this.#inlineConfigComments;
|
|
178
241
|
}
|
|
179
242
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-soml",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "ESLint plugin for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "github:soml-lang/eslint-soml",
|
|
@@ -51,20 +51,20 @@
|
|
|
51
51
|
"style"
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@eslint/core": "^1.2.1",
|
|
54
55
|
"@eslint/plugin-kit": "^0.7.3",
|
|
55
|
-
"soml-lang": "^0.0.
|
|
56
|
+
"soml-lang": "^0.0.5"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@babel/code-frame": "^8.0.6",
|
|
59
60
|
"@eslint-community/eslint-utils": "^4.10.1",
|
|
60
|
-
"@eslint/core": "^1.2.1",
|
|
61
61
|
"@typescript-eslint/types": "^8.71.1",
|
|
62
62
|
"enquirer": "^2.4.1",
|
|
63
63
|
"eslint": "^10.12.0",
|
|
64
64
|
"eslint-config-xo": "^5.0.0",
|
|
65
65
|
"eslint-doc-generator": "^3.7.1",
|
|
66
66
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
67
|
-
"eslint-plugin-jsdoc": "^65.2.
|
|
67
|
+
"eslint-plugin-jsdoc": "^65.2.4",
|
|
68
68
|
"indent-string": "^5.0.0",
|
|
69
69
|
"markdownlint-cli": "^0.49.1",
|
|
70
70
|
"nano-spawn": "^2.1.0",
|
package/readme.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install --save-dev eslint eslint-soml
|
|
|
16
16
|
|
|
17
17
|
**Requires Node.js 22.13 or later, ESLint `>=10.4`, [flat config](https://eslint.org/docs/latest/use/configure/configuration-files), and [ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm).**
|
|
18
18
|
|
|
19
|
-
The
|
|
19
|
+
The rules of this plugin do not need `Temporal`, so they work on every supported Node.js version, also for files with instants and durations. Only reading the `value` of an `Instant` or `Duration` node needs it, as in your own rule or in a `no-restricted-syntax` selector that matches on `value`, such as `[value=1]`. Node.js 26 and later have `Temporal` built in. On older versions, load a [`Temporal` polyfill](https://github.com/fullcalendar/temporal-polyfill) first: make `import 'temporal-polyfill/global';` the first import in `eslint.config.js`, before `eslint-soml`.
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
@@ -61,7 +61,7 @@ Disable a rule with a comment, in either comment style:
|
|
|
61
61
|
'404': 'Not found' /* eslint-disable-line soml/prefer-bare-keys */
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
The [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-disable-directives) and [`reportUnusedInlineConfigs`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-inline-configs) linter options work for `.soml` files, and so do the rules of [`@eslint-community/eslint-plugin-eslint-comments`](https://github.com/eslint-community/eslint-plugin-eslint-comments), such as `no-unlimited-disable`.
|
|
64
|
+
The [`reportUnusedDisableDirectives`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-disable-directives) and [`reportUnusedInlineConfigs`](https://eslint.org/docs/latest/use/configure/configuration-files#report-unused-inline-configs) linter options work for `.soml` files, and so do the rules of [`@eslint-community/eslint-plugin-eslint-comments`](https://github.com/eslint-community/eslint-plugin-eslint-comments), such as `no-unlimited-disable`. Those rules follow JavaScript, where a line comment holds only `eslint-disable-line` and `eslint-disable-next-line`, so some of them, such as `no-unlimited-disable` and `no-use`, do not check `# eslint-disable` and `# eslint-enable`. Write those as `/* … */` comments to have them checked.
|
|
65
65
|
|
|
66
66
|
ESLint's [`no-restricted-syntax`](https://eslint.org/docs/latest/rules/no-restricted-syntax) rule works too. For example, to disallow a key named `password`:
|
|
67
67
|
|
|
@@ -86,6 +86,54 @@ export default defineConfig([
|
|
|
86
86
|
]);
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
A selector can also check a value under a given key, which catches settings that are risky in production:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
'no-restricted-syntax': [
|
|
93
|
+
'error',
|
|
94
|
+
{
|
|
95
|
+
selector: 'Member:has(> Key[value=/^(allowed-origins|cors-origins?)$/]) String[value="*"]',
|
|
96
|
+
message: 'Allow only known origins.',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
selector: 'Member:has(> Key[value=/^(verify-ssl|ssl-verify|tls-verify)$/]) > Boolean[value=false]',
|
|
100
|
+
message: 'Do not turn off TLS verification.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
selector: 'Member:has(> Key[value=/^(host|bind|listen)$/]) > String[value="0.0.0.0"]',
|
|
104
|
+
message: 'Bind to a specific address.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
// Only at the top level.
|
|
108
|
+
selector: 'Document > Object > Member > Key[value="debug"]',
|
|
109
|
+
message: 'Set `debug` in the environment, not in the file.',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### SOML in Markdown
|
|
115
|
+
|
|
116
|
+
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:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import markdown from '@eslint/markdown';
|
|
120
|
+
import soml from 'eslint-soml';
|
|
121
|
+
import {defineConfig} from 'eslint/config';
|
|
122
|
+
|
|
123
|
+
export default defineConfig([
|
|
124
|
+
{
|
|
125
|
+
files: ['**/*.md'],
|
|
126
|
+
plugins: {
|
|
127
|
+
markdown,
|
|
128
|
+
},
|
|
129
|
+
processor: 'markdown/markdown',
|
|
130
|
+
},
|
|
131
|
+
soml.configs.recommended,
|
|
132
|
+
]);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A block that shows a mistake on purpose can be skipped with `<!-- eslint-skip -->` on the line before it.
|
|
136
|
+
|
|
89
137
|
## Rules
|
|
90
138
|
|
|
91
139
|
<!-- Do not manually modify this list. Run: `npm run fix:eslint-docs` -->
|
|
@@ -111,7 +159,7 @@ export default defineConfig([
|
|
|
111
159
|
| [no-escapes-in-paths](docs/rules/no-escapes-in-paths.md) | Disallow `\n` and `\t` escapes in Windows paths. | ✅ ☑️ | | 💡 |
|
|
112
160
|
| [no-float-trailing-zeros](docs/rules/no-float-trailing-zeros.md) | Disallow trailing zeros in the fraction of a float. | ✅ | | 💡 |
|
|
113
161
|
| [no-invisible-characters](docs/rules/no-invisible-characters.md) | Disallow invisible characters in strings, keys, and comments. | ✅ ☑️ | 🔧 | |
|
|
114
|
-
| [no-loss-of-precision](docs/rules/no-loss-of-precision.md) | Disallow floats
|
|
162
|
+
| [no-loss-of-precision](docs/rules/no-loss-of-precision.md) | Disallow floats whose written digits are lost when they are stored. | ✅ ☑️ | | |
|
|
115
163
|
| [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. | ✅ ☑️ | | |
|
|
116
164
|
| [no-mixed-number-types](docs/rules/no-mixed-number-types.md) | Disallow arrays that mix ints and floats. | ✅ | | 💡 |
|
|
117
165
|
| [no-negative-zero](docs/rules/no-negative-zero.md) | Disallow a sign on zero. | ✅ | 🔧 | |
|
|
@@ -122,7 +170,7 @@ export default defineConfig([
|
|
|
122
170
|
| [no-zero-duration-parts](docs/rules/no-zero-duration-parts.md) | Disallow duration parts that are zero. | ✅ | 🔧 | |
|
|
123
171
|
| [numeric-separators-style](docs/rules/numeric-separators-style.md) | Enforce the grouping of digits with `_` in decimal numbers. | ✅ | 🔧 | |
|
|
124
172
|
| [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
|
|
173
|
+
| [prefer-literal-strings](docs/rules/prefer-literal-strings.md) | Prefer literal strings, `'…'`, when a string does not need escapes. | ✅ | 🔧 | |
|
|
126
174
|
| [prefer-utc-instants](docs/rules/prefer-utc-instants.md) | Prefer instants in UTC. | | 🔧 | |
|
|
127
175
|
| [sort-keys](docs/rules/sort-keys.md) | Require object keys to be sorted. | | 🔧 | |
|
|
128
176
|
| [spaced-comment](docs/rules/spaced-comment.md) | Require a space after `#` and `/*`, and before `*/`. | ✅ | 🔧 | |
|
|
@@ -201,6 +249,48 @@ In Visual Studio Code, the [ESLint extension](https://marketplace.visualstudio.c
|
|
|
201
249
|
}
|
|
202
250
|
```
|
|
203
251
|
|
|
252
|
+
## Writing rules
|
|
253
|
+
|
|
254
|
+
A rule for SOML files gets the syntax tree of [`soml-lang`](https://github.com/soml-lang/soml-javascript#parsetreetext), which documents every node. This package exports the types of the nodes, and `SomlRuleDefinition`, `SomlRuleVisitor`, `SomlSourceCode`, and `SomlRuleContext` (for a helper that a rule passes its `context` to) for a rule:
|
|
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.
|
|
293
|
+
|
|
204
294
|
## Related
|
|
205
295
|
|
|
206
296
|
- [soml-javascript](https://github.com/soml-lang/soml-javascript) - The reference parser and serializer for SOML
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {trimEnd} from './utils/index.js';
|
|
1
|
+
import {getLines, trimEnd} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -23,15 +23,11 @@ const create = context => {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const lines =
|
|
26
|
+
const lines = getLines(context, node);
|
|
27
27
|
// The first line holds the opening delimiter, and the last line the closing one.
|
|
28
|
-
const content = lines.slice(1, -1);
|
|
28
|
+
const content = lines.slice(1, -1).map(line => line.text);
|
|
29
29
|
// Where each content line starts, and then where the closing line starts.
|
|
30
|
-
const lineStarts =
|
|
31
|
-
|
|
32
|
-
for (const line of content) {
|
|
33
|
-
lineStarts.push(lineStarts.at(-1) + line.length + 1);
|
|
34
|
-
}
|
|
30
|
+
const lineStarts = lines.slice(1).map(line => line.start);
|
|
35
31
|
|
|
36
32
|
const firstContent = content.findIndex(line => !isBlank(line));
|
|
37
33
|
// With no content, every line is a leading blank line.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {trimEnd} from './utils/index.js';
|
|
1
|
+
import {getLines, trimEnd} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -21,25 +21,20 @@ const create = context => {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const lines = sourceCode.getText(node).split('\n');
|
|
25
|
-
let offset = sourceCode.getRange(node)[0] + lines[0].length + 1;
|
|
26
|
-
|
|
27
24
|
// The first line holds the opening delimiter, and the last line the closing one.
|
|
28
|
-
for (const
|
|
29
|
-
const content = trimEnd(
|
|
25
|
+
for (const {text, start} of getLines(context, node).slice(1, -1)) {
|
|
26
|
+
const content = trimEnd(text, ' \t');
|
|
30
27
|
|
|
31
28
|
// A blank line becomes an empty line, so its whitespace is not part of the value.
|
|
32
|
-
if (content !== '' && content.length <
|
|
29
|
+
if (content !== '' && content.length < text.length) {
|
|
33
30
|
yield {
|
|
34
31
|
loc: {
|
|
35
|
-
start: sourceCode.getLocFromIndex(
|
|
36
|
-
end: sourceCode.getLocFromIndex(
|
|
32
|
+
start: sourceCode.getLocFromIndex(start + content.length),
|
|
33
|
+
end: sourceCode.getLocFromIndex(start + text.length),
|
|
37
34
|
},
|
|
38
35
|
messageId: MESSAGE_ID,
|
|
39
36
|
};
|
|
40
37
|
}
|
|
41
|
-
|
|
42
|
-
offset += line.length + 1;
|
|
43
38
|
}
|
|
44
39
|
});
|
|
45
40
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {stringifyValue} from 'soml-lang';
|
|
2
|
-
import {hasAdjacentComment, isPunctuator} from './utils/index.js';
|
|
2
|
+
import {escapeInvisibleCharacters, hasAdjacentComment, isPunctuator} from './utils/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -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 ? stringifyValue(item.value) : sourceCode.getText(item),
|
|
75
|
+
// A block string spans lines, so it is shown as a one-line string with the same value. Canonical form writes a bidirectional control as it is, so it is escaped, as an escape in the block string was.
|
|
76
|
+
item: item.block ? escapeInvisibleCharacters(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.
|
|
@@ -81,7 +81,7 @@ const config = {
|
|
|
81
81
|
meta: {
|
|
82
82
|
type: 'problem',
|
|
83
83
|
docs: {
|
|
84
|
-
description: 'Disallow floats
|
|
84
|
+
description: 'Disallow floats whose written digits are lost when they are stored.',
|
|
85
85
|
recommended: 'unopinionated',
|
|
86
86
|
},
|
|
87
87
|
messages,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {getLines} from './utils/index.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
@import {SomlContext} from './rule/soml-context.js';
|
|
@@ -22,6 +22,33 @@ function isClosingQuote(line, index, delimiter) {
|
|
|
22
22
|
return runs.filter(run => run.length === delimiter.length).length % 2 === 1;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
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.
|
|
27
|
+
*/
|
|
28
|
+
const AFTER_DELIMITER = /[\t ]*(?:[,\]\}][\t ]*)*(?:(?:#|\/\*).*)?$/vy;
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
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
|
+
*/
|
|
33
|
+
function getMisplacedDelimiterIndex(line, delimiter, isEscaped) {
|
|
34
|
+
for (const match of line.matchAll(QUOTE_RUNS[delimiter[0]])) {
|
|
35
|
+
let {index} = match;
|
|
36
|
+
const end = index + match[0].length;
|
|
37
|
+
AFTER_DELIMITER.lastIndex = end;
|
|
38
|
+
|
|
39
|
+
if (!AFTER_DELIMITER.test(line)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// In a `"""` block, `\"` is an escaped quote, so it is not part of the run.
|
|
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;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
@param {SomlContext} context
|
|
27
54
|
*/
|
|
@@ -33,50 +60,24 @@ const create = context => {
|
|
|
33
60
|
return;
|
|
34
61
|
}
|
|
35
62
|
|
|
36
|
-
const lines =
|
|
63
|
+
const lines = getLines(context, node);
|
|
37
64
|
// The first line is the opening delimiter, so it is a run of one quote character.
|
|
38
|
-
const delimiter = lines[0];
|
|
39
|
-
let offset = sourceCode.getRange(node)[0] + delimiter.length + 1;
|
|
65
|
+
const delimiter = lines[0].text;
|
|
40
66
|
|
|
41
67
|
// The last line holds the closing delimiter. Only the first misplaced delimiter is reported, because the lines after it, such as the opening of another block string, are content only because of it.
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
let content = trimEnd(line, ' \t');
|
|
45
|
-
|
|
46
|
-
if (content.endsWith(',')) {
|
|
47
|
-
content = trimEnd(content.slice(0, -1), ' \t');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Where the run of quotes at the end starts.
|
|
51
|
-
let index = content.length;
|
|
52
|
-
|
|
53
|
-
while (index > 0 && content[index - 1] === delimiter[0]) {
|
|
54
|
-
index--;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// In a `"""` block, `\"` is an escaped quote, so it is not part of the run.
|
|
58
|
-
if (node.style === 'escaped' && content.length - index >= delimiter.length && ESCAPING_BACKSLASHES.test(content.slice(0, index))) {
|
|
59
|
-
index++;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
index > 0
|
|
64
|
-
&& content.length - index === delimiter.length
|
|
65
|
-
&& !isClosingQuote(content, index, delimiter)
|
|
66
|
-
) {
|
|
67
|
-
const start = offset + index;
|
|
68
|
+
for (const {text, start} of lines.slice(1, -1)) {
|
|
69
|
+
const index = getMisplacedDelimiterIndex(text, delimiter, node.style === 'escaped');
|
|
68
70
|
|
|
71
|
+
if (index !== undefined) {
|
|
69
72
|
return {
|
|
70
73
|
loc: {
|
|
71
|
-
start: sourceCode.getLocFromIndex(start),
|
|
72
|
-
end: sourceCode.getLocFromIndex(start + delimiter.length),
|
|
74
|
+
start: sourceCode.getLocFromIndex(start + index),
|
|
75
|
+
end: sourceCode.getLocFromIndex(start + index + delimiter.length),
|
|
73
76
|
},
|
|
74
77
|
messageId: MESSAGE_ID,
|
|
75
78
|
data: {delimiter},
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
offset += line.length + 1;
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
83
|
};
|
|
@@ -66,7 +66,7 @@ const config = {
|
|
|
66
66
|
meta: {
|
|
67
67
|
type: 'suggestion',
|
|
68
68
|
docs: {
|
|
69
|
-
description: 'Prefer literal strings, `\'…\'`, when a string
|
|
69
|
+
description: 'Prefer literal strings, `\'…\'`, when a string does not need escapes.',
|
|
70
70
|
recommended: true,
|
|
71
71
|
},
|
|
72
72
|
fixable: 'code',
|
package/rules/sort-keys.js
CHANGED
|
@@ -59,10 +59,9 @@ function compareNatural(first, second) {
|
|
|
59
59
|
return firstCodePoint - secondCodePoint;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
// The two code points are equal, so
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
secondIndex += length;
|
|
62
|
+
// The two code points are equal, so a surrogate pair is followed by the same low surrogate in both.
|
|
63
|
+
firstIndex++;
|
|
64
|
+
secondIndex++;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
// A key that ends first comes first, as `a` before `a1`.
|
|
@@ -94,6 +93,9 @@ const create = context => {
|
|
|
94
93
|
return commentLines;
|
|
95
94
|
};
|
|
96
95
|
|
|
96
|
+
// Whether the fix is allowed, found once per file, because the directives would be parsed again for every object.
|
|
97
|
+
let isFixable;
|
|
98
|
+
|
|
97
99
|
/**
|
|
98
100
|
Whether a blank line separates two members. A line in a comment is not blank.
|
|
99
101
|
|
|
@@ -152,7 +154,7 @@ const create = context => {
|
|
|
152
154
|
sortName: natural ? 'natural' : 'code point',
|
|
153
155
|
},
|
|
154
156
|
// The fix sorts the whole object at once, so one run of `--fix` is enough, and only the first problem carries it. Giving every problem the same fix would build it once per problem.
|
|
155
|
-
fix: isFirst && !isDisabledAnywhere(context) ? reorderMembers(context, node.members, groups.flatMap(group => group.toSorted(compareMembers))) : undefined,
|
|
157
|
+
fix: isFirst && (isFixable ??= !isDisabledAnywhere(context)) ? reorderMembers(context, node.members, groups.flatMap(group => group.toSorted(compareMembers))) : undefined,
|
|
156
158
|
};
|
|
157
159
|
|
|
158
160
|
isFirst = false;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
The words of a key, in lowercase: `maxHeapSize`, `max-heap-size`, and `max_heap_size` all give `max`, `heap`, and `size`. An acronym is a word too, so `maxRAMSize` gives `max`, `ram`, and `size`. Every character that is not a letter or a digit separates words, so a quoted key works too, as in `'spark.executor.memory'` (Spark) or `'php_admin_value[memory_limit]'` (PHP-FPM). A separator at an end or next to another one makes no empty word, so `_size` is one word.
|
|
2
|
+
The words of a key, in lowercase: `maxHeapSize`, `max-heap-size`, and `max_heap_size` all give `max`, `heap`, and `size`. An acronym is a word too, so `maxRAMSize` gives `max`, `ram`, and `size`. A word after an acronym has at least two lowercase letters, so a plural acronym, as in `maxCPUs`, stays one word. Every character that is not a letter or a digit separates words, so a quoted key works too, as in `'spark.executor.memory'` (Spark) or `'php_admin_value[memory_limit]'` (PHP-FPM). A separator at an end or next to another one makes no empty word, so `_size` is one word.
|
|
3
3
|
|
|
4
4
|
@param {string} key
|
|
5
5
|
@returns {string[]}
|
|
6
6
|
*/
|
|
7
7
|
export default function getKeyWords(key) {
|
|
8
|
-
return key.split(/[^\p{Letter}\p{Number}]|(?<=[\da-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/v)
|
|
8
|
+
return key.split(/[^\p{Letter}\p{Number}]|(?<=[\da-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z]{2})/v)
|
|
9
9
|
.filter(word => word !== '')
|
|
10
10
|
.map(word => word.toLowerCase());
|
|
11
11
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
@import {SomlContext} from '../rule/soml-context.js';
|
|
3
|
+
@import {Node} from 'soml-lang';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
The lines of the source text of `node`, each with the offset where it starts. In a block string, the first line holds the opening delimiter, and the last line the closing one.
|
|
8
|
+
|
|
9
|
+
@param {SomlContext} context
|
|
10
|
+
@param {Node} node
|
|
11
|
+
@returns {Array<{text: string, start: number}>}
|
|
12
|
+
*/
|
|
13
|
+
export default function getLines(context, node) {
|
|
14
|
+
const {sourceCode} = context;
|
|
15
|
+
const lines = [];
|
|
16
|
+
let [start] = sourceCode.getRange(node);
|
|
17
|
+
|
|
18
|
+
for (const text of sourceCode.getText(node).split('\n')) {
|
|
19
|
+
lines.push({text, start});
|
|
20
|
+
start += text.length + 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return lines;
|
|
24
|
+
}
|
package/rules/utils/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export {reorderMembers, hasAdjacentComment} from './reorder-members.js';
|
|
|
9
9
|
export {default as trimWhitespace} from './trim-whitespace.js';
|
|
10
10
|
export {isComment, isPunctuator} from './token.js';
|
|
11
11
|
export {default as trimEnd} from './trim-end.js';
|
|
12
|
+
export {default as getLines} from './get-lines.js';
|
|
12
13
|
export {default as hasDoubledBackslashes} from './has-doubled-backslashes.js';
|
|
13
14
|
export {default as getKeyWords} from './get-key-words.js';
|
|
14
15
|
export {default as isDisabledAnywhere} from './is-disabled-anywhere.js';
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
Characters that a reader cannot see, or cannot tell apart from a space: bidirectional controls (as in Trojan Source), zero-width characters (including the invisible math operators U+2061 to U+2064), the Hangul fillers, which show as blank, other invisible format characters (U+180E and U+206A to U+206F), the byte order mark, the soft hyphen, control characters, and every Unicode whitespace character except space, tab, and line feed. Zero-width joiners and non-joiners are left out, because emoji sequences and several scripts need them.
|
|
3
3
|
|
|
4
|
-
Tag characters are invisible too, and can hide text, as in ASCII smuggling. They are left out only in an emoji tag sequence, which is U+1F3F4
|
|
4
|
+
Tag characters are invisible too, and can hide text, as in ASCII smuggling. They are left out only in an emoji tag sequence, which is U+1F3F4, at most six tags, and U+E007F, as in the flag of Scotland. So a tag is reported when more than five tags or no U+1F3F4 come before it, or more than five tags or no U+E007F come after it.
|
|
5
5
|
*/
|
|
6
6
|
const characters = /[[\p{Control}\p{White_Space}\u{AD}\u{61C}\u{115F}\u{1160}\u{180E}\u{200B}\u{200E}\u{200F}\u{202A}-\u{202E}\u{2060}-\u{2064}\u{2066}-\u{206F}\u{3164}\u{FEFF}\u{FFA0}]--[\t\n ]]/v;
|
|
7
|
-
const
|
|
7
|
+
const tagSpec = String.raw`[\u{E0020}-\u{E007E}]`;
|
|
8
|
+
const tags = new RegExp(String.raw`\u{E0001}|(?<!\u{1F3F4}${tagSpec}{0,5})${tagSpec}|${tagSpec}(?!${tagSpec}{0,5}\u{E007F})|(?<!\u{1F3F4}${tagSpec}{1,6})\u{E007F}`, 'v');
|
|
8
9
|
const invisibleCharacter = new RegExp(`${characters.source}|${tags.source}`, 'v');
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -9,7 +9,7 @@ const commentParser = new ConfigCommentParser();
|
|
|
9
9
|
/**
|
|
10
10
|
Whether a disable directive in the document turns off the rule of `context` anywhere, as `# eslint-disable-next-line soml/format` does, or as one without rule names does.
|
|
11
11
|
|
|
12
|
-
A rule whose one fix changes the whole document or a whole object leaves out that fix then. ESLint applies a fix when the problem that carries it is reported, even when the other problems are turned off, so the fix would change the lines that the directive keeps, and ESLint would then remove the directive as unused.
|
|
12
|
+
A rule whose one fix changes the whole document or a whole object leaves out that fix then. ESLint applies a fix when the problem that carries it is reported, even when the other problems are turned off, so the fix would change the lines that the directive keeps, and ESLint would then remove the directive as unused. A rule cannot see the `noInlineConfig` linter option, so a directive that the option turns off still counts.
|
|
13
13
|
|
|
14
14
|
@param {SomlContext} context
|
|
15
15
|
@returns {boolean}
|