@sinemacula/coding-standards 1.20.2 → 1.21.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 CHANGED
@@ -130,13 +130,17 @@ type-aware rules (the curated typescript-eslint set plus the type-driven custom
130
130
  typescript-eslint tooling, and this package to your dev dependencies:
131
131
 
132
132
  ```bash
133
- npm install --save-dev eslint typescript typescript-eslint eslint-plugin-jsdoc @sinemacula/coding-standards
133
+ npm install --save-dev eslint typescript typescript-eslint eslint-plugin-jsdoc yaml-eslint-parser \
134
+ @sinemacula/coding-standards
134
135
  ```
135
136
 
137
+ `yaml-eslint-parser` is imported by the base layer for the YAML comment-width block, so it has to resolve even in a
138
+ repository with no YAML worth linting; without it the flat config fails to load at all.
139
+
136
140
  The package exposes three flat-config entry points:
137
141
 
138
142
  - `@sinemacula/coding-standards/js/eslint` - the base layer of syntax-only custom rules; needs no `tsconfig`, so it
139
- stays cheap and runs anywhere Biome runs.
143
+ stays cheap and runs anywhere Biome runs. Covers `.ts`/`.js` and, for the comment-width rule alone, `.yml`/`.yaml`.
140
144
  - `@sinemacula/coding-standards/js/eslint/type-checked` - the opt-in type-aware layer. It includes the base layer and
141
145
  adds the cross-file / type-driven rules, so it needs a consumer `tsconfig`; use it in place of the base layer where
142
146
  one exists.
@@ -187,7 +191,10 @@ source-exported plugin definitions do not reliably propagate, so mirror it consu
187
191
 
188
192
  ```toml
189
193
  [plugins.definitions.eslint]
190
- package_filters = ["@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc"]
194
+ package_filters = [
195
+ "@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc",
196
+ "yaml-eslint-parser",
197
+ ]
191
198
  ```
192
199
 
193
200
  Repositories enabling the Vue layer widen the same filter further, since its plugins have to resolve inside that sandbox
@@ -197,7 +204,7 @@ too:
197
204
  [plugins.definitions.eslint]
198
205
  package_filters = [
199
206
  "@sinemacula/coding-standards", "typescript-eslint", "@typescript-eslint", "eslint-plugin-jsdoc",
200
- "eslint-plugin-vue", "vue-eslint-parser", "eslint-plugin-check-file",
207
+ "yaml-eslint-parser", "eslint-plugin-vue", "vue-eslint-parser", "eslint-plugin-check-file",
201
208
  ]
202
209
  ```
203
210
 
@@ -269,6 +276,7 @@ tag = "<version>"
269
276
  | `js/biome.json` | Biome | JavaScript / TypeScript formatter + linter rules |
270
277
  | `js/knip.json` | Knip | Unused-export detection rules |
271
278
  | `js/eslint/` | ESLint | Structural, type-aware + Vue rules; runs with Biome |
279
+ | `js/eslint/` (YAML block) | ESLint | Comment width in `.yml` / `.yaml`; yamllint owns rest |
272
280
  | `markdown/.markdownlint.json` | markdownlint | Markdown linting rules |
273
281
  | `yaml/.yamllint.yaml` | yamllint | YAML linting rules |
274
282
  | `shell/.shellcheckrc` | ShellCheck | Shell script linting rules |
@@ -321,7 +329,7 @@ native directive - `// phpcs:ignore <code>` for a sniff, `@phpstan-ignore <ident
321
329
  ### ESLint rules
322
330
 
323
331
  All rules run in the base layer except `boolean-method-name`, which resolves return types and so requires the opt-in
324
- type-checked layer.
332
+ type-checked layer. Every rule is scoped to `.ts`/`.js`; `comment-line-wrap` alone also runs over `.yml`/`.yaml`.
325
333
 
326
334
  | Rule | Enforces |
327
335
  |------------------------------------------------|-------------------------------------------------------------------------------------|
@@ -336,7 +344,7 @@ type-checked layer.
336
344
  | `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
337
345
  | `@sinemacula/single-line-property-doc` | A data member's documentation comment sits on one line; autofixable. |
338
346
  | `@sinemacula/multiline-function-doc` | A method's documentation comment spans multiple lines; autofixable. |
339
- | `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars; premature wraps also fixed. |
347
+ | `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars, YAML included; premature wraps too. |
340
348
 
341
349
  `boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
342
350
  to widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
@@ -348,13 +356,21 @@ signatures and class fields holding a function take several. A data comment is n
348
356
  where present; a free function keeps the freedom of either shape.
349
357
 
350
358
  `comment-line-wrap` takes `maxLength` (default 80) and is the syntax-only counterpart of the PHP
351
- `SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` runs and multi-line docblock prose greedily,
352
- reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both. Markdown
353
- headings, docblock tag lines, machine-parsed tool directives (`eslint`, `biome-ignore`, `@ts-`, `Stryker`, `c8`/`v8`/
354
- `istanbul ignore`, `@vite-ignore` and the like), fenced code, an indented code or command block, a doc-tag whose value
355
- opens a multi-line bracketed type (an `array{...}` shape, a `<...>` generic or a `\Closure(...)` signature), tables,
356
- separators, a line whose overflow is a single unbreakable token such as a long name or URL, trailing comments after code
357
- and compact single-line docblocks are left untouched.
359
+ `SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` and `#` runs and multi-line docblock prose
360
+ greedily, reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both.
361
+ Markdown headings, docblock tag lines, machine-parsed tool directives (`eslint`, `biome-ignore`, `@ts-`, `Stryker`,
362
+ `c8`/`v8`/`istanbul ignore`, `@vite-ignore`, `yamllint`, `yaml-language-server`, `renovate:` and the like), fenced code,
363
+ an indented code or command block, a doc-tag whose value opens a multi-line bracketed type (an `array{...}` shape, a
364
+ `<...>` generic or a `\Closure(...)` signature), tables, separators, a line whose overflow is a single unbreakable token
365
+ such as a long name or URL, trailing comments after code and compact single-line docblocks are left untouched. Each
366
+ comment token reclaims its own width from the line, so a `#` comment fills one column further than a `//` one.
367
+
368
+ This is the one rule the base layer also carries over `.yml` and `.yaml`, which nothing else in the standards bounds for
369
+ comment width: yamllint's `line-length` cannot tell a comment from a value, so it would fault `run:` commands and action
370
+ refs nobody can shorten, and it has no autofix. The YAML block registers `yaml-eslint-parser` for its `#` comments and
371
+ enables this rule alone - none of `eslint-plugin-yml`'s own rules are switched on, so YAML quoting, key order and
372
+ indentation stay yamllint's business. Only standalone comments are reached: a block scalar's body is content rather
373
+ than comment, so a shell comment inside a `run: |` step is never seen, and a comment trailing a value is not standalone.
358
374
 
359
375
  The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `curly` (a brace on every
360
376
  control statement, as PSR-12 already requires on the PHP side), `max-lines-per-function` (50 lines, test code exempt)
@@ -1,9 +1,11 @@
1
1
  import jsdoc from 'eslint-plugin-jsdoc';
2
2
  import tseslint from 'typescript-eslint';
3
+ import * as yamlParser from 'yaml-eslint-parser';
3
4
  import plugin from './plugin.js';
4
5
 
5
6
  const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
6
7
  const TS_AND_JS_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs'];
8
+ const YAML_FILES = ['**/*.yml', '**/*.yaml'];
7
9
 
8
10
  /**
9
11
  * Base flat config: the AST-only custom rules that need no type information.
@@ -12,7 +14,9 @@ const TS_AND_JS_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.c
12
14
  * resolves TypeScript syntax. The interface, readonly-property and enum rules
13
15
  * target TypeScript-only constructs; no-mutable-static also applies to plain
14
16
  * JavaScript (exported let/var, mutable static fields), so it runs across both.
15
- * The opt-in type-aware layer lives in ./type-checked.js.
17
+ * A final block carries the comment-wrap rule alone over YAML, which otherwise
18
+ * bounds nothing about a comment's width. The opt-in type-aware layer lives in
19
+ * ./type-checked.js.
16
20
  *
17
21
  * @author Ben Carey <bdmc@sinemacula.co.uk>
18
22
  * @copyright 2026 Sine Macula Limited
@@ -93,6 +97,23 @@ export default [
93
97
  'jsdoc/lines-before-block': ['error', { lines: 1, ignoreSingleLines: false }],
94
98
  },
95
99
  },
100
+ {
101
+ // YAML carries the comment-wrap rule and nothing else. The parser is
102
+ // registered for its `#` comments alone, so no eslint-plugin-yml rule
103
+ // is enabled: pulling that plugin's own set in would fault every
104
+ // consumer's YAML on quoting, key order and indentation at once, none
105
+ // of which this package governs. yamllint keeps the rest of YAML.
106
+ files: YAML_FILES,
107
+ plugins: {
108
+ '@sinemacula': plugin,
109
+ },
110
+ languageOptions: {
111
+ parser: yamlParser,
112
+ },
113
+ rules: {
114
+ '@sinemacula/comment-line-wrap': 'error',
115
+ },
116
+ },
96
117
  {
97
118
  files: ['**/*.{test,spec}.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', '**/__tests__/**', '**/tests/**', '**/test-support/**'],
98
119
  rules: {
@@ -5,7 +5,10 @@
5
5
  * verbatim and acts as a paragraph boundary. Directives, docblock tags, fenced
6
6
  * or indented code, tables and rule separators are left exactly as written, so
7
7
  * a reflow never disturbs a construct whose position or spacing carries
8
- * meaning.
8
+ * meaning. The directive set spans the tools that read instructions out of a
9
+ * comment in any of the governed languages, YAML's included: a schema
10
+ * association or a Renovate manager hint is machine-read, so wrapping it would
11
+ * silently sever it from the key it annotates.
9
12
  *
10
13
  * @author Ben Carey <bdmc@sinemacula.co.uk>
11
14
  * @copyright 2026 Sine Macula Limited
@@ -18,7 +21,7 @@ const FENCE = /^(```|~~~)/;
18
21
  const TAG = new RegExp(`^@[A-Za-z][A-Za-z0-9-]*(?=${WS}|$)`);
19
22
  const LIST = new RegExp(`^${WS}*([-*+]|\\d+[.)])${WS}+`);
20
23
  const HEADING = new RegExp(`^#{1,6}${WS}`);
21
- const DIRECTIVE = /^(phpcs:|phpstan-ignore|@phpstan-|@psalm-|@phan-|eslint\b|globals?\b|exported\b|biome-ignore\b|@ts-|prettier-ignore|stylelint-|Stryker (?:disable|restore)\b|(?:c8|v8|istanbul) ignore\b|@vite-ignore\b|webpackChunkName\b|@preserve\b|@license\b|@codingStandards|@SuppressWarnings|NOSONAR|qlty-ignore)/;
24
+ const DIRECTIVE = /^(phpcs:|phpstan-ignore|@phpstan-|@psalm-|@phan-|eslint\b|globals?\b|exported\b|biome-ignore\b|@ts-|prettier-ignore|stylelint-|Stryker (?:disable|restore)\b|(?:c8|v8|istanbul) ignore\b|@vite-ignore\b|webpackChunkName\b|@preserve\b|@license\b|@codingStandards|@SuppressWarnings|NOSONAR|qlty-ignore|yamllint\b|yaml-language-server\b|renovate:)/;
22
25
  const SEPARATOR = /^[=\-~*_#.+ ]{3,}$/;
23
26
  const CODE = new RegExp(`=>|->|::|;${WS}*$|\\{${WS}*$|^\\}|^(?:if|elseif|for|foreach|while|switch|catch)${WS}*\\(|^[\\w$>[\\]'.-]+${WS}*=[^=>]|^\\$`);
24
27
  const SPAN = /`[^`]*`|\{@[^}]*\}|\[[^\]]*\]\([^)]*\)/g;
@@ -6,6 +6,12 @@ const DEFAULT_MAX_LENGTH = 80;
6
6
  /** A docblock interior line: its indent, the star, and the prose after it. */
7
7
  const DOC_LINE = /^([ \t\n\r\f\v]*)\*( ?)(.*)$/;
8
8
 
9
+ /** The comment types a parser gives a line or block comment it recognises. */
10
+ const COMMENT_TYPES = ['Line', 'Block'];
11
+
12
+ /** The line-comment openers governed, matched longest first. */
13
+ const LINE_TOKENS = ['//', '#'];
14
+
9
15
  /** Whether only whitespace precedes the comment on its own line. */
10
16
  function isStandalone(comment, sourceCode) {
11
17
  const line = sourceCode.lines[comment.loc.start.line - 1];
@@ -14,25 +20,44 @@ function isStandalone(comment, sourceCode) {
14
20
  }
15
21
 
16
22
  /**
17
- * Group standalone `//` comments into runs of adjacent lines sharing an indent,
18
- * so a wrapped paragraph is reflowed as one unit.
23
+ * The line-comment token a comment opens with, or null when it opens none.
24
+ *
25
+ * Read from the source text rather than inferred from the comment's type,
26
+ * because parsers label the same shape differently: a `//` comment arrives as
27
+ * `Line`, while yaml-eslint-parser tags a `#` comment `Block`. Requiring one of
28
+ * the two recognised types first leaves out a hashbang, which is neither and
29
+ * whose leading `#` must never be read as prose.
19
30
  */
20
- function slashRuns(comments, sourceCode) {
31
+ function lineToken(comment, sourceCode) {
32
+ if (!COMMENT_TYPES.includes(comment.type)) {
33
+ return null;
34
+ }
35
+
36
+ const opener = sourceCode.text.slice(comment.range[0], comment.range[0] + 2);
37
+
38
+ return LINE_TOKENS.find(token => opener.startsWith(token)) ?? null;
39
+ }
40
+
41
+ /**
42
+ * Group standalone line comments into runs of adjacent lines sharing a token
43
+ * and an indent, so a wrapped paragraph is reflowed as one unit.
44
+ */
45
+ function lineRuns(comments, sourceCode) {
21
46
  const runs = [];
22
47
  let current = null;
23
48
 
24
49
  for (const comment of comments) {
25
- if (comment.type !== 'Line' || !isStandalone(comment, sourceCode)) {
50
+ const token = lineToken(comment, sourceCode);
51
+
52
+ if (token === null || !isStandalone(comment, sourceCode)) {
26
53
  current = null;
27
54
  continue;
28
55
  }
29
56
 
30
- const last = current?.[current.length - 1];
31
-
32
- if (current && comment.loc.start.line === last.loc.start.line + 1 && comment.loc.start.column === current[0].loc.start.column) {
33
- current.push(comment);
57
+ if (current !== null && continues(current, comment, token)) {
58
+ current.comments.push(comment);
34
59
  } else {
35
- current = [comment];
60
+ current = { token, comments: [comment] };
36
61
  runs.push(current);
37
62
  }
38
63
  }
@@ -40,22 +65,32 @@ function slashRuns(comments, sourceCode) {
40
65
  return runs;
41
66
  }
42
67
 
43
- /** Strip the single optional space that follows a `//` from a comment's value. */
44
- function slashContent(value) {
68
+ /** Whether a comment extends the open run: same token, next line, same column. */
69
+ function continues(run, comment, token) {
70
+ const last = run.comments[run.comments.length - 1];
71
+
72
+ return run.token === token
73
+ && comment.loc.start.line === last.loc.start.line + 1
74
+ && comment.loc.start.column === run.comments[0].loc.start.column;
75
+ }
76
+
77
+ /** Strip the single optional space that follows the token from a value. */
78
+ function lineContent(value) {
45
79
  return value.startsWith(' ') ? value.slice(1) : value;
46
80
  }
47
81
 
48
- /** Describe a `//` run: its content lines, margin, report locations and rebuild. */
49
- function slashDescriptor(run, sourceCode, eol) {
50
- const first = run[0].loc.start;
82
+ /** Describe a line-comment run: content, margin, report locations and rebuild. */
83
+ function lineDescriptor(run, sourceCode, eol) {
84
+ const { token, comments } = run;
85
+ const first = comments[0].loc.start;
51
86
  const indent = sourceCode.lines[first.line - 1].slice(0, first.column);
52
87
 
53
88
  return {
54
- content: run.map(comment => slashContent(comment.value)),
55
- marginWidth: indent.length + 3,
56
- locs: run.map(comment => comment.loc),
57
- range: [run[0].range[0], run[run.length - 1].range[1]],
58
- rebuild: lines => lines.map((line, offset) => `${offset === 0 ? '' : indent}//${line === '' ? '' : ` ${line}`}`).join(eol),
89
+ content: comments.map(comment => lineContent(comment.value)),
90
+ marginWidth: indent.length + token.length + 1,
91
+ locs: comments.map(comment => comment.loc),
92
+ range: [comments[0].range[0], comments[comments.length - 1].range[1]],
93
+ rebuild: lines => lines.map((line, offset) => `${offset === 0 ? '' : indent}${token}${line === '' ? '' : ` ${line}`}`).join(eol),
59
94
  };
60
95
  }
61
96
 
@@ -135,13 +170,19 @@ function enforce(context, descriptor, maxLength) {
135
170
  * The syntax-only counterpart of the PHP comment line length sniff. It fills
136
171
  * each line greedily with as many whole words as fit and reports two faults on
137
172
  * their own footings: a line that overflows the width, and a line that wraps
138
- * earlier than it needs to. Standalone `//` runs and multi-line docblocks are
139
- * governed; tag lines, suppression directives, fenced or indented code, tables,
140
- * separators and trailing comments after code are left untouched, as is a line
141
- * whose overflow is a single unbreakable token such as a long name or URL, and
142
- * a compact single-line docblock, which the single-line property rule governs.
143
- * The fix reflows each faulted paragraph to its greedy canonical form and is
144
- * idempotent.
173
+ * earlier than it needs to. Standalone runs of the line-comment tokens `//` and
174
+ * `#` and multi-line docblocks are governed; tag lines, suppression directives,
175
+ * fenced or indented code, tables, separators and trailing comments after code
176
+ * are left untouched, as is a line whose overflow is a single unbreakable token
177
+ * such as a long name or URL, and a compact single-line docblock, which the
178
+ * single-line property rule governs. The fix reflows each faulted paragraph to
179
+ * its greedy canonical form and is idempotent.
180
+ *
181
+ * The `#` token carries the rule into YAML through yaml-eslint-parser, where it
182
+ * reaches only standalone comments: block-scalar bodies are content rather than
183
+ * comments, so a shell comment inside a `run:` step is never seen, and a
184
+ * comment trailing a value is not standalone. Each token reclaims its own
185
+ * width, so a `#` comment fills one column further than a `//` one.
145
186
  *
146
187
  * @author Ben Carey <bdmc@sinemacula.co.uk>
147
188
  * @copyright 2026 Sine Macula Limited
@@ -181,8 +222,8 @@ export default createRule({
181
222
  Program() {
182
223
  const comments = sourceCode.getAllComments();
183
224
 
184
- for (const run of slashRuns(comments, sourceCode)) {
185
- enforce(context, slashDescriptor(run, sourceCode, eol), maxLength);
225
+ for (const run of lineRuns(comments, sourceCode)) {
226
+ enforce(context, lineDescriptor(run, sourceCode, eol), maxLength);
186
227
  }
187
228
 
188
229
  for (const comment of comments) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinemacula/coding-standards",
3
- "version": "1.20.2",
3
+ "version": "1.21.0",
4
4
  "description": "Centralized coding standards, static analysis configurations, and code quality tooling for all Sine Macula repositories.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Ben Carey <bdmc@sinemacula.co.uk>",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "test:js": "vitest run",
45
- "lint:js": "eslint js/eslint"
45
+ "lint:js": "eslint ."
46
46
  },
47
47
  "devDependencies": {
48
48
  "@typescript-eslint/rule-tester": "^8.0.0",
@@ -54,7 +54,8 @@
54
54
  "typescript": "^5.0.0",
55
55
  "typescript-eslint": "^8.0.0",
56
56
  "vitest": "^3.0.0",
57
- "vue-eslint-parser": "^10.0.0"
57
+ "vue-eslint-parser": "^10.0.0",
58
+ "yaml-eslint-parser": "^2.1.0"
58
59
  },
59
60
  "peerDependencies": {
60
61
  "eslint": ">=9",
@@ -63,7 +64,8 @@
63
64
  "eslint-plugin-vue": ">=10",
64
65
  "typescript": ">=4.8.4",
65
66
  "typescript-eslint": "^8",
66
- "vue-eslint-parser": ">=10"
67
+ "vue-eslint-parser": ">=10",
68
+ "yaml-eslint-parser": ">=2"
67
69
  },
68
70
  "peerDependenciesMeta": {
69
71
  "eslint": {
@@ -86,6 +88,9 @@
86
88
  },
87
89
  "vue-eslint-parser": {
88
90
  "optional": true
91
+ },
92
+ "yaml-eslint-parser": {
93
+ "optional": true
89
94
  }
90
95
  },
91
96
  "publishConfig": {