eslint-config-uphold 6.11.0 → 6.13.0
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/README.md +63 -0
- package/package.json +9 -11
- package/src/configs/common.js +2 -1
- package/src/configs/typescript.js +1 -0
- package/src/index.js +10 -30
- package/src/rules/index.js +3 -1
- package/src/rules/no-trailing-period-in-log-messages.js +1 -1
- package/src/rules/require-comment-punctuation.js +341 -0
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ This config includes custom Uphold-specific rules, under `uphold-plugin`.
|
|
|
154
154
|
| [`database-migration-filename-format`](#database-migration-filename-format) | False | False |
|
|
155
155
|
| [`explicit-sinon-use-fake-timers`](#explicit-sinon-use-fake-timers) | True | False |
|
|
156
156
|
| [`no-trailing-period-in-log-messages`](#no-trailing-period-in-log-messages) | True | True |
|
|
157
|
+
| [`require-comment-punctuation`](#require-comment-punctuation) | True | True |
|
|
157
158
|
|
|
158
159
|
#### `database-migration-filename-format`
|
|
159
160
|
|
|
@@ -221,6 +222,50 @@ logger.error('An error occurred.'); // ❌ Remove the trailing period.
|
|
|
221
222
|
}]
|
|
222
223
|
```
|
|
223
224
|
|
|
225
|
+
#### `require-comment-punctuation`
|
|
226
|
+
|
|
227
|
+
Requires `//` comment blocks to end with punctuation on the last line. Valid endings are `.`, `:`, `;`, `?`, `!`, or paired triple backticks (`` ``` ``). This helps maintain consistency in code comments.
|
|
228
|
+
|
|
229
|
+
**Valid:**
|
|
230
|
+
|
|
231
|
+
````js
|
|
232
|
+
// This is a comment with a period.
|
|
233
|
+
// Is this a question?
|
|
234
|
+
// This is important!
|
|
235
|
+
// ``` let foo = 1 + 2 ```
|
|
236
|
+
````
|
|
237
|
+
|
|
238
|
+
**Invalid:**
|
|
239
|
+
|
|
240
|
+
- ❌ Missing punctuation.
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
// This is a comment without punctuation
|
|
244
|
+
// Comment with unmatched backticks ```
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Options:**
|
|
248
|
+
|
|
249
|
+
- `exclusionPrefixes` - An array of strings. Comments starting with these prefixes (after `//`) are excluded from the rule:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
'uphold-plugin/require-comment-punctuation': ['error', {
|
|
253
|
+
exclusionPrefixes: ['TODO:', 'FIXME:', 'NOTE:']
|
|
254
|
+
}]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
With this config, comments like `// TODO: implement this` will not require punctuation.
|
|
258
|
+
|
|
259
|
+
- `additionalAllowedEndings` - An array of strings to extend the default allowed endings (`.`, `:`, `;`, `?`, `!`, `` ``` ``):
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
'uphold-plugin/require-comment-punctuation': ['error', {
|
|
263
|
+
additionalAllowedEndings: [')']
|
|
264
|
+
}]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
With this config, comments ending with `)` will also be considered valid.
|
|
268
|
+
|
|
224
269
|
### Test configs
|
|
225
270
|
|
|
226
271
|
This package includes individual exported configs for multiple test frameworks:
|
|
@@ -258,6 +303,20 @@ import { jest, mocha, vitest } from 'eslint-config-uphold/configs';
|
|
|
258
303
|
> [!NOTE]
|
|
259
304
|
> Test configs use top-level `await` for dynamic module detection and are **ESM-only**. If your project uses CommonJS, your `eslint.config.mjs` still supports `import()` and top-level `await`.
|
|
260
305
|
|
|
306
|
+
### Custom parsers
|
|
307
|
+
|
|
308
|
+
From [ESLint's docs > Configure a Parser](https://eslint.org/docs/latest/use/configure/parser):
|
|
309
|
+
|
|
310
|
+
> You can use custom parsers to convert JavaScript code into an abstract syntax tree for ESLint to evaluate. You might want to add a custom parser if your code isn’t compatible with ESLint’s default parser, Espree.
|
|
311
|
+
>
|
|
312
|
+
> (...)
|
|
313
|
+
>
|
|
314
|
+
> The following third-party parsers are known to be compatible with ESLint:
|
|
315
|
+
>
|
|
316
|
+
> - [Esprima](https://www.npmjs.com/package/esprima)
|
|
317
|
+
> - [@babel/eslint-parser](https://www.npmjs.com/package/@babel/eslint-parser) - A wrapper around the Babel parser that makes it compatible with ESLint.
|
|
318
|
+
> - [@typescript-eslint/parser](https://www.npmjs.com/package/@typescript-eslint/parser) - A parser that converts TypeScript into an ESTree-compatible form so it can be used in ESLint.
|
|
319
|
+
|
|
261
320
|
## Upgrading ESLint
|
|
262
321
|
|
|
263
322
|
See the [ESLint repo](https://github.com/eslint/eslint#semantic-versioning-policy) for ESLint's guidelines on semantic versioning.
|
|
@@ -269,6 +328,10 @@ This is down to the fact that no guarantee is made that minor upgrades do not ca
|
|
|
269
328
|
The downside here is a package update is required for any security or other bug fixes.
|
|
270
329
|
The benefit however is the included rules are always guaranteed to be stable.
|
|
271
330
|
|
|
331
|
+
## Migration guide
|
|
332
|
+
|
|
333
|
+
See the [MIGRATIONS.md](MIGRATIONS.md) file for migration guides between versions v5 and v6, v6 and v7.
|
|
334
|
+
|
|
272
335
|
## Release process
|
|
273
336
|
|
|
274
337
|
The release of a version is automated via the [release](https://github.com/uphold/eslint-config-uphold/.github/workflows/release.yml) GitHub workflow.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-config-uphold",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.13.0",
|
|
4
4
|
"description": "Uphold-flavored ESLint config",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -35,13 +35,10 @@
|
|
|
35
35
|
"test": "node --test $(find test -name '*.test.js')"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@babel/eslint-parser": "^7.28.6",
|
|
40
|
-
"@stylistic/eslint-plugin": "^5.7.0",
|
|
38
|
+
"@stylistic/eslint-plugin": "^5.7.1",
|
|
41
39
|
"dayjs": "^1.11.19",
|
|
42
40
|
"eslint-config-prettier": "^10.1.8",
|
|
43
|
-
"eslint-plugin-jsdoc": "^62.
|
|
44
|
-
"eslint-plugin-mocha": "^11.2.0",
|
|
41
|
+
"eslint-plugin-jsdoc": "^62.5.1",
|
|
45
42
|
"eslint-plugin-n": "^17.23.2",
|
|
46
43
|
"eslint-plugin-prettier": "^5.5.5",
|
|
47
44
|
"eslint-plugin-promise": "^7.2.1",
|
|
@@ -49,17 +46,18 @@
|
|
|
49
46
|
"eslint-plugin-sort-imports-requires": "^2.0.0",
|
|
50
47
|
"eslint-plugin-sort-keys-fix": "^1.1.2",
|
|
51
48
|
"eslint-plugin-sql-template": "^3.1.0",
|
|
52
|
-
"globals": "^17.
|
|
49
|
+
"globals": "^17.3.0"
|
|
53
50
|
},
|
|
54
51
|
"devDependencies": {
|
|
55
52
|
"@fastify/pre-commit": "^2.2.1",
|
|
56
|
-
"@types/node": "^22.19.
|
|
53
|
+
"@types/node": "^22.19.8",
|
|
57
54
|
"@uphold/github-changelog-generator": "^4.0.2",
|
|
58
55
|
"@vitest/eslint-plugin": "^1.6.6",
|
|
59
56
|
"eslint": "~9.39.2",
|
|
60
|
-
"eslint-plugin-jest": "^29.12.
|
|
61
|
-
"
|
|
62
|
-
"
|
|
57
|
+
"eslint-plugin-jest": "^29.12.2",
|
|
58
|
+
"eslint-plugin-mocha": "^11.2.0",
|
|
59
|
+
"prettier": "^3.8.1",
|
|
60
|
+
"release-it": "^19.2.4",
|
|
63
61
|
"typescript": "^5.9.3",
|
|
64
62
|
"typescript-eslint": "^8.54.0"
|
|
65
63
|
},
|
package/src/configs/common.js
CHANGED
|
@@ -374,7 +374,8 @@ export const upholdPluginConfig = {
|
|
|
374
374
|
},
|
|
375
375
|
rules: {
|
|
376
376
|
...(isSinonAvailable && { 'uphold-plugin/explicit-sinon-use-fake-timers': 'error' }),
|
|
377
|
-
'uphold-plugin/no-trailing-period-in-log-messages': 'error'
|
|
377
|
+
'uphold-plugin/no-trailing-period-in-log-messages': 'error',
|
|
378
|
+
'uphold-plugin/require-comment-punctuation': 'error'
|
|
378
379
|
}
|
|
379
380
|
};
|
|
380
381
|
|
|
@@ -138,6 +138,7 @@ export async function createTypeScriptConfig(moduleType = 'module', { ecmaVersio
|
|
|
138
138
|
},
|
|
139
139
|
rules: {
|
|
140
140
|
...eslintRules,
|
|
141
|
+
'@typescript-eslint/array-type': ['error', { default: 'array-simple', readonly: 'array-simple' }],
|
|
141
142
|
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_.+' }],
|
|
142
143
|
'no-unused-expressions': 'off',
|
|
143
144
|
'no-unused-vars': 'off'
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { defineConfig } from 'eslint/config';
|
|
|
6
6
|
import {
|
|
7
7
|
eslintRules,
|
|
8
8
|
jsdocPluginConfigJavaScript,
|
|
9
|
+
nodePluginConfig,
|
|
9
10
|
promisePluginConfig,
|
|
10
11
|
sortDestructureKeysConfig,
|
|
11
12
|
sortImportsRequiresConfig,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
sqlTemplateConfig,
|
|
14
15
|
stylisticConfig
|
|
15
16
|
} from './configs/common.js';
|
|
16
|
-
import babelParser from '@babel/eslint-parser';
|
|
17
17
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
18
18
|
import globals from 'globals';
|
|
19
19
|
import js from '@eslint/js';
|
|
@@ -35,17 +35,8 @@ import stylistic from '@stylistic/eslint-plugin';
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
const languageOptions = {
|
|
38
|
-
ecmaVersion:
|
|
39
|
-
globals:
|
|
40
|
-
...globals.jasmine,
|
|
41
|
-
...globals.jest,
|
|
42
|
-
...globals.mocha,
|
|
43
|
-
...globals.node
|
|
44
|
-
},
|
|
45
|
-
parser: babelParser,
|
|
46
|
-
parserOptions: {
|
|
47
|
-
requireConfigFile: false
|
|
48
|
-
},
|
|
38
|
+
ecmaVersion: 2022,
|
|
39
|
+
globals: globals.node,
|
|
49
40
|
sourceType: 'module'
|
|
50
41
|
};
|
|
51
42
|
|
|
@@ -68,8 +59,8 @@ const upholdBaseConfig = defineConfig([
|
|
|
68
59
|
'@stylistic': stylistic,
|
|
69
60
|
jsdoc,
|
|
70
61
|
mocha,
|
|
71
|
-
//
|
|
72
|
-
|
|
62
|
+
// eslint-disable-next-line id-length
|
|
63
|
+
n: nodePlugin,
|
|
73
64
|
// @ts-expect-error Outdated types for `eslint-plugin-promise`.
|
|
74
65
|
promise,
|
|
75
66
|
// @ts-expect-error Outdated types for `eslint-plugin-sort-destructure-keys`.
|
|
@@ -82,27 +73,16 @@ const upholdBaseConfig = defineConfig([
|
|
|
82
73
|
},
|
|
83
74
|
rules: {
|
|
84
75
|
...eslintRules,
|
|
76
|
+
...nodePluginConfig.rules,
|
|
85
77
|
...promisePluginConfig.rules,
|
|
86
78
|
...sortDestructureKeysConfig.rules,
|
|
87
79
|
...sortImportsRequiresConfig.rules,
|
|
88
80
|
...sortKeysFixConfig.rules,
|
|
89
81
|
...sqlTemplateConfig.rules,
|
|
90
82
|
...stylisticConfig.rules,
|
|
91
|
-
// Mocha rules to be removed on next major release.
|
|
92
|
-
'mocha/no-exclusive-tests': 'error',
|
|
93
|
-
'mocha/no-identical-title': 'error',
|
|
94
|
-
'mocha/no-nested-tests': 'error',
|
|
95
|
-
'mocha/no-sibling-hooks': 'error',
|
|
96
|
-
'node-plugin/no-mixed-requires': 'error',
|
|
97
|
-
'node-plugin/no-new-require': 'error',
|
|
98
|
-
'node-plugin/no-path-concat': 'error',
|
|
99
|
-
'node-plugin/no-process-env': 'error',
|
|
100
|
-
'node-plugin/no-process-exit': 'error',
|
|
101
|
-
'node-plugin/no-restricted-import': 'error',
|
|
102
|
-
'node-plugin/no-restricted-require': 'error',
|
|
103
|
-
'node-plugin/no-sync': 'error',
|
|
104
83
|
'uphold-plugin/explicit-sinon-use-fake-timers': 'error',
|
|
105
|
-
'uphold-plugin/no-trailing-period-in-log-messages': 'error'
|
|
84
|
+
'uphold-plugin/no-trailing-period-in-log-messages': 'error',
|
|
85
|
+
'uphold-plugin/require-comment-punctuation': 'error'
|
|
106
86
|
}
|
|
107
87
|
}
|
|
108
88
|
]);
|
|
@@ -118,8 +98,8 @@ const upholdBinScriptsConfig = {
|
|
|
118
98
|
languageOptions,
|
|
119
99
|
name: 'uphold/scripts',
|
|
120
100
|
rules: {
|
|
121
|
-
'no-
|
|
122
|
-
'
|
|
101
|
+
'n/no-process-exit': 'off',
|
|
102
|
+
'no-console': 'off'
|
|
123
103
|
}
|
|
124
104
|
};
|
|
125
105
|
|
package/src/rules/index.js
CHANGED
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
import databaseMigrationFilenameFormat from './database-migration-filename-format.js';
|
|
8
8
|
import explicitSinonUseFakeTimers from './explicit-sinon-use-fake-timers.js';
|
|
9
9
|
import noTrailingPeriodInLogMessages from './no-trailing-period-in-log-messages.js';
|
|
10
|
+
import requireCommentPunctuation from './require-comment-punctuation.js';
|
|
10
11
|
|
|
11
12
|
export default {
|
|
12
13
|
'database-migration-filename-format': databaseMigrationFilenameFormat,
|
|
13
14
|
'explicit-sinon-use-fake-timers': explicitSinonUseFakeTimers,
|
|
14
|
-
'no-trailing-period-in-log-messages': noTrailingPeriodInLogMessages
|
|
15
|
+
'no-trailing-period-in-log-messages': noTrailingPeriodInLogMessages,
|
|
16
|
+
'require-comment-punctuation': requireCommentPunctuation
|
|
15
17
|
};
|
|
@@ -100,7 +100,7 @@ const noTrailingPeriodInLogMessages = {
|
|
|
100
100
|
/**
|
|
101
101
|
* Extracts the log method name from a callee node.
|
|
102
102
|
* @param {import('estree').Node} callee - Callee node to analyze.
|
|
103
|
-
* @returns {string|null} Method name if it's a log method, null otherwise.
|
|
103
|
+
* @returns {string | null} Method name if it's a log method, null otherwise.
|
|
104
104
|
*/
|
|
105
105
|
function getLogMethodName(callee) {
|
|
106
106
|
if (callee.type === 'MemberExpression') {
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('estree').Comment} Comment
|
|
3
|
+
* @typedef {Comment & { range: [number, number], loc: import('estree').SourceLocation }} CommentWithRange
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* comment: CommentWithRange,
|
|
6
|
+
* isDirective: boolean,
|
|
7
|
+
* isExcluded: boolean,
|
|
8
|
+
* isFullLine: boolean,
|
|
9
|
+
* valueTrimmed: string
|
|
10
|
+
* }} CommentInfo
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Constants.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Allowed endings for the last line in a `//` comment block.
|
|
18
|
+
const ALLOWED_ENDINGS = Object.freeze(['.', ':', ';', '?', '!', '```']);
|
|
19
|
+
|
|
20
|
+
// Directive prefixes that have semantic meaning and should not be style-linted.
|
|
21
|
+
const DIRECTIVE_PREFIXES = Object.freeze([
|
|
22
|
+
'@ts-',
|
|
23
|
+
'c8 ',
|
|
24
|
+
'eslint ',
|
|
25
|
+
'eslint-',
|
|
26
|
+
'global ',
|
|
27
|
+
'istanbul ',
|
|
28
|
+
'prettier-ignore',
|
|
29
|
+
'v8 '
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Has Paired Triple Backticks.
|
|
34
|
+
* @param {string} trimmedEnd The comment text trimmed only on the right side.
|
|
35
|
+
* @returns {boolean} True if the comment contains at least two instances of ```, and they're a multiple of two.
|
|
36
|
+
*/
|
|
37
|
+
function hasPairedTripleBackticks(trimmedEnd) {
|
|
38
|
+
const matches = trimmedEnd.match(/```/g);
|
|
39
|
+
|
|
40
|
+
return Boolean(matches && matches.length >= 2 && matches.length % 2 === 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ends with Allowed Punctuation.
|
|
45
|
+
* @param {string} trimmedEnd The comment text trimmed only on the right side.
|
|
46
|
+
* @param {string[]} allowedEndings The list of allowed endings.
|
|
47
|
+
* @returns {boolean} True if the trimmed comment text ends with allowed punctuation.
|
|
48
|
+
*/
|
|
49
|
+
function endsWithAllowedPunctuation(trimmedEnd, allowedEndings) {
|
|
50
|
+
return allowedEndings.some(ending => {
|
|
51
|
+
if (ending === '```') {
|
|
52
|
+
return trimmedEnd.endsWith(ending) && hasPairedTripleBackticks(trimmedEnd);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return trimmedEnd.endsWith(ending);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pre-compile frequently used regexes to avoid repeated allocation.
|
|
60
|
+
const PUNCT_RE = new RegExp(/[^\w\s]/);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Is Punctuation Character.
|
|
64
|
+
* @param {string|undefined} ch The character to check.
|
|
65
|
+
* @returns {boolean} True if the character is a punctuation character.
|
|
66
|
+
*/
|
|
67
|
+
function isPunctuationChar(ch) {
|
|
68
|
+
return !!ch && PUNCT_RE.test(ch);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Is Directive Comment.
|
|
73
|
+
* - These comments have semantic meaning and should not be style-linted.
|
|
74
|
+
* @param {string} trimmed Comment text with whitespace trimmed on both ends.
|
|
75
|
+
* @returns {boolean} Returns true if this comment is an ESLint/TS/prettier/coverage directive.
|
|
76
|
+
*/
|
|
77
|
+
function isDirectiveComment(trimmed) {
|
|
78
|
+
if (!trimmed) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (trimmed === 'eslint') {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return DIRECTIVE_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Is Full-Line Comment.
|
|
91
|
+
* (i.e. it is a full-line comment, not a trailing inline comment).
|
|
92
|
+
* @param {import('eslint').SourceCode} sourceCode The ESLint source code object.
|
|
93
|
+
* @param {CommentWithRange} comment The comment token.
|
|
94
|
+
* @returns {boolean} True if the comment starts at the first non-whitespace character of the line.
|
|
95
|
+
*/
|
|
96
|
+
function isFullLineComment(sourceCode, comment) {
|
|
97
|
+
const line = sourceCode.lines[comment.loc.start.line - 1] || '';
|
|
98
|
+
const before = line.slice(0, comment.loc.start.column);
|
|
99
|
+
|
|
100
|
+
return /^[\t ]*$/.test(before);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Is Line Comment With Range.
|
|
105
|
+
* @param {Comment} comment The comment token.
|
|
106
|
+
* @returns {comment is CommentWithRange} True if the comment is a line comment with range/loc metadata.
|
|
107
|
+
*/
|
|
108
|
+
function isLineCommentWithRange(comment) {
|
|
109
|
+
return comment.type === 'Line' && Boolean(comment.range && comment.loc);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Normalize rule options.
|
|
114
|
+
* @param {{ exclusionPrefixes?: string[], additionalAllowedEndings?: string[] }} rawOptions The raw options.
|
|
115
|
+
* @returns {{ exclusionPrefixes: string[], allowedEndings: string[] }} Normalized options.
|
|
116
|
+
*/
|
|
117
|
+
function normalizeOptions(rawOptions) {
|
|
118
|
+
const exclusionPrefixes = rawOptions.exclusionPrefixes || [];
|
|
119
|
+
const additionalAllowedEndings = rawOptions.additionalAllowedEndings || [];
|
|
120
|
+
const allowedEndings = [...ALLOWED_ENDINGS, ...additionalAllowedEndings];
|
|
121
|
+
|
|
122
|
+
return { allowedEndings, exclusionPrefixes };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get Index to Insert Period Before Trailing Whitespace.
|
|
127
|
+
* - Compute where to insert a period so it lands before trailing whitespace.
|
|
128
|
+
* @param {import('eslint').SourceCode} sourceCode The ESLint source code object.
|
|
129
|
+
* @param {CommentWithRange} comment The comment token.
|
|
130
|
+
* @returns {number} The index in sourceCode.text where the period should be inserted.
|
|
131
|
+
*/
|
|
132
|
+
function getInsertIndexBeforeTrailingWhitespace(sourceCode, comment) {
|
|
133
|
+
const raw = sourceCode.text.slice(comment.range[0], comment.range[1]);
|
|
134
|
+
const trailingWhitespace = raw.match(/\s*$/)?.[0].length || 0;
|
|
135
|
+
|
|
136
|
+
return comment.range[1] - trailingWhitespace;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* `require-comment-punctuation` ESLint rule definition.
|
|
141
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
142
|
+
*/
|
|
143
|
+
const requireCommentPunctuation = {
|
|
144
|
+
create(context) {
|
|
145
|
+
const { sourceCode } = context;
|
|
146
|
+
/** @type {{ exclusionPrefixes?: string[], additionalAllowedEndings?: string[] }} */
|
|
147
|
+
const rawOptions = context.options[0] || {};
|
|
148
|
+
const { allowedEndings, exclusionPrefixes } = normalizeOptions(rawOptions);
|
|
149
|
+
const allowedEndingsText = [...new Set(allowedEndings)].join(', ');
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Check if comment should be excluded based on exclusion prefixes.
|
|
153
|
+
* @param {string} trimmed The trimmed comment text.
|
|
154
|
+
* @returns {boolean} True if comment should be excluded.
|
|
155
|
+
*/
|
|
156
|
+
function isExcludedComment(trimmed) {
|
|
157
|
+
return exclusionPrefixes.some(prefix => trimmed.startsWith(prefix));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Decide if two comments should be grouped into the same block.
|
|
162
|
+
* @param {CommentInfo} previousInfo The previous comment info.
|
|
163
|
+
* @param {CommentInfo} nextInfo The next comment info.
|
|
164
|
+
* @returns {boolean} True if the comments should be grouped.
|
|
165
|
+
*/
|
|
166
|
+
function shouldJoinBlock(previousInfo, nextInfo) {
|
|
167
|
+
const adjacentLine = nextInfo.comment.loc.start.line === previousInfo.comment.loc.end.line + 1;
|
|
168
|
+
|
|
169
|
+
return adjacentLine && previousInfo.isFullLine && nextInfo.isFullLine;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Find the last non-empty comment in a block.
|
|
174
|
+
* @param {CommentInfo[]} block The block of line comments to check.
|
|
175
|
+
* @returns {CommentInfo | null} The last meaningful comment, or null.
|
|
176
|
+
*/
|
|
177
|
+
function findLastMeaningfulInfo(block) {
|
|
178
|
+
/** @type {CommentInfo | null} */
|
|
179
|
+
let lastMeaningful = null;
|
|
180
|
+
|
|
181
|
+
for (const info of block) {
|
|
182
|
+
// should not happen; directives break blocks.
|
|
183
|
+
if (info.isDirective || info.isExcluded) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (info.valueTrimmed.length > 0) {
|
|
188
|
+
lastMeaningful = info;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lastMeaningful;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check Block for punctuation.
|
|
197
|
+
* - Enforce punctuation on the last *meaningful* line of the block.
|
|
198
|
+
* - If the block ends with blank `//` lines, we skip those and check the
|
|
199
|
+
* last non-empty comment.
|
|
200
|
+
* @param {CommentInfo[]} block The block of line comments to check.
|
|
201
|
+
*/
|
|
202
|
+
function checkBlock(block) {
|
|
203
|
+
if (!block.length) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const lastMeaningful = findLastMeaningfulInfo(block);
|
|
208
|
+
|
|
209
|
+
if (!lastMeaningful) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Compute right-trimmed value lazily to avoid unnecessary work.
|
|
214
|
+
const trimmedEnd = lastMeaningful.comment.value.trimEnd();
|
|
215
|
+
|
|
216
|
+
if (endsWithAllowedPunctuation(trimmedEnd, allowedEndings)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (trimmedEnd.endsWith('```') && !hasPairedTripleBackticks(trimmedEnd)) {
|
|
221
|
+
context.report({
|
|
222
|
+
loc: lastMeaningful.comment.loc,
|
|
223
|
+
messageId: 'unpairedBackticks'
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
context.report({
|
|
230
|
+
data: { allowedEndings: allowedEndingsText },
|
|
231
|
+
fix(fixer) {
|
|
232
|
+
const insertAt = getInsertIndexBeforeTrailingWhitespace(sourceCode, lastMeaningful.comment);
|
|
233
|
+
|
|
234
|
+
// Attempt to replace a single trailing disallowed punctuation mark
|
|
235
|
+
// (e.g. `,`) with a period instead of appending, to avoid outputs
|
|
236
|
+
// like `comma,.`. We only replace when the last non-whitespace
|
|
237
|
+
// character is a single punctuation character (not part of a
|
|
238
|
+
// multi-character punctuation like `..` or triple backticks).
|
|
239
|
+
const lastNonWsIndex = insertAt - 1;
|
|
240
|
+
const lastChar = sourceCode.text.charAt(lastNonWsIndex);
|
|
241
|
+
const prevChar = sourceCode.text.charAt(lastNonWsIndex - 1);
|
|
242
|
+
const lastCharIsSinglePunctuation = isPunctuationChar(lastChar) && !isPunctuationChar(prevChar);
|
|
243
|
+
|
|
244
|
+
// If it's a single backtick we avoid replacing because triple
|
|
245
|
+
// backtick handling is done separately above.
|
|
246
|
+
if (lastCharIsSinglePunctuation && lastChar !== '`') {
|
|
247
|
+
return fixer.replaceTextRange([lastNonWsIndex, insertAt], '.');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return fixer.insertTextBeforeRange([insertAt, insertAt], '.');
|
|
251
|
+
},
|
|
252
|
+
loc: lastMeaningful.comment.loc,
|
|
253
|
+
messageId: 'missingPunctuation'
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
Program() {
|
|
259
|
+
/** @type {CommentWithRange[]} */
|
|
260
|
+
const lineComments = sourceCode.getAllComments().filter(isLineCommentWithRange);
|
|
261
|
+
|
|
262
|
+
/** @type {CommentInfo[]} */
|
|
263
|
+
const commentInfos = lineComments.map(comment => {
|
|
264
|
+
const valueTrimmed = comment.value.trim();
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
comment,
|
|
268
|
+
isDirective: isDirectiveComment(valueTrimmed),
|
|
269
|
+
isExcluded: isExcludedComment(valueTrimmed),
|
|
270
|
+
isFullLine: isFullLineComment(sourceCode, comment),
|
|
271
|
+
valueTrimmed
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/** @type {CommentInfo[]} */
|
|
276
|
+
let currentBlock = [];
|
|
277
|
+
|
|
278
|
+
for (const info of commentInfos) {
|
|
279
|
+
// Directives and excluded comments are always their own "block" and are ignored.
|
|
280
|
+
if (info.isDirective || info.isExcluded) {
|
|
281
|
+
checkBlock(currentBlock);
|
|
282
|
+
currentBlock = [];
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (currentBlock.length === 0) {
|
|
287
|
+
currentBlock.push(info);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const previousInfo = currentBlock[currentBlock.length - 1];
|
|
292
|
+
|
|
293
|
+
// Only group full-line comments. Trailing inline comments are checked individually.
|
|
294
|
+
if (shouldJoinBlock(previousInfo, info)) {
|
|
295
|
+
currentBlock.push(info);
|
|
296
|
+
} else {
|
|
297
|
+
checkBlock(currentBlock);
|
|
298
|
+
currentBlock = [info];
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
checkBlock(currentBlock);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
},
|
|
306
|
+
meta: {
|
|
307
|
+
docs: {
|
|
308
|
+
description: 'Require `//` comment blocks to end with punctuation on the last line.'
|
|
309
|
+
},
|
|
310
|
+
fixable: 'code',
|
|
311
|
+
messages: {
|
|
312
|
+
missingPunctuation: 'Line comment must end with one of: {{allowedEndings}}.',
|
|
313
|
+
unpairedBackticks: 'Line comment ends with unpaired triple backticks (```).'
|
|
314
|
+
},
|
|
315
|
+
schema: [
|
|
316
|
+
{
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
properties: {
|
|
319
|
+
additionalAllowedEndings: {
|
|
320
|
+
description: 'Additional endings to allow beyond the defaults (., :, ;, ?, !, ```).',
|
|
321
|
+
items: { type: 'string' },
|
|
322
|
+
type: 'array'
|
|
323
|
+
},
|
|
324
|
+
exclusionPrefixes: {
|
|
325
|
+
description: 'Comments starting with these strings (after `//`) are excluded from the rule.',
|
|
326
|
+
items: { type: 'string' },
|
|
327
|
+
type: 'array'
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
type: 'object'
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
type: 'suggestion'
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Export `require-comment-punctuation` rule.
|
|
339
|
+
*/
|
|
340
|
+
|
|
341
|
+
export default requireCommentPunctuation;
|