@sinemacula/coding-standards 1.14.0 → 1.16.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 +13 -3
- package/js/eslint/index.js +2 -0
- package/js/eslint/plugin.js +2 -0
- package/js/eslint/rules/boolean-method-name.js +6 -7
- package/js/eslint/rules/comment-classifier.js +140 -0
- package/js/eslint/rules/comment-line-wrap.js +198 -0
- package/js/eslint/rules/comment-paragraph.js +159 -0
- package/js/eslint/rules/comment-reflow.js +145 -0
- package/js/eslint/rules/comment-tokenizer.js +89 -0
- package/js/eslint/vue.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ native directive - `// phpcs:ignore <code>` for a sniff, `@phpstan-ignore <ident
|
|
|
274
274
|
| `SineMacula.Attributes.DisallowToolingAttribute` | No IDE/tooling attributes (e.g. `JetBrains\PhpStorm`). |
|
|
275
275
|
| `SineMacula.Classes.RequireFinalClass` | Concrete classes must be `final` or `abstract` (`@inheritable` opts out). |
|
|
276
276
|
| `SineMacula.Classes.RequireReadonlyPublicProperty` | Public properties (declared or promoted) must be `readonly`. |
|
|
277
|
-
| `SineMacula.Commenting.CommentLineLength` | Standalone comment
|
|
277
|
+
| `SineMacula.Commenting.CommentLineLength` | Standalone comment prose wrapped to 80 chars; premature wraps also fixed. |
|
|
278
278
|
| `SineMacula.Commenting.ConsistentEnumCaseComments` | Enum case docs are all-or-nothing within an enum. |
|
|
279
279
|
| `SineMacula.Commenting.MultilineMethodComment` | A method's doc comment must span multiple lines. |
|
|
280
280
|
| `SineMacula.Commenting.RequireConstantComment` | Every class/interface/enum/trait constant needs a doc comment. |
|
|
@@ -321,6 +321,7 @@ type-checked layer.
|
|
|
321
321
|
| `@sinemacula/align-doc-tags` | `@author` and `@copyright` values line up at a single column; autofixable. |
|
|
322
322
|
| `@sinemacula/single-line-property-doc` | A data member's documentation comment sits on one line; autofixable. |
|
|
323
323
|
| `@sinemacula/multiline-function-doc` | A method's documentation comment spans multiple lines; autofixable. |
|
|
324
|
+
| `@sinemacula/comment-line-wrap` | Standalone comment prose wrapped to 80 chars; premature wraps also fixed. |
|
|
324
325
|
|
|
325
326
|
`boolean-method-name` takes `additionalPrefixes`, `additionalPredicates` and `additionalCommandVerbs` (string arrays)
|
|
326
327
|
to widen the accepted vocabulary from a consumer config. `max-methods-per-class` takes `max`, `no-base-error` takes
|
|
@@ -331,8 +332,17 @@ members (interface property signatures, enum members and data class fields) take
|
|
|
331
332
|
method signatures and class fields holding a function take several. A data comment is never required, only held to
|
|
332
333
|
one line where present; a free function keeps the freedom of either shape.
|
|
333
334
|
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
`comment-line-wrap` takes `maxLength` (default 80) and is the syntax-only counterpart of the PHP
|
|
336
|
+
`SineMacula.Commenting.CommentLineLength` sniff. It fills standalone `//` runs and multi-line docblock prose greedily,
|
|
337
|
+
reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both. Markdown
|
|
338
|
+
headings, docblock tag lines, suppression directives, fenced code, an indented code or command block, a doc-tag whose
|
|
339
|
+
value opens a multi-line bracketed type (an `array{...}` shape, a `<...>` generic or a `\Closure(...)` signature),
|
|
340
|
+
tables, separators, a line whose overflow is a single unbreakable token such as a long name or URL, trailing comments
|
|
341
|
+
after code and compact single-line docblocks are left untouched.
|
|
342
|
+
|
|
343
|
+
The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `curly` (a brace on every
|
|
344
|
+
control statement, as PSR-12 already requires on the PHP side), `max-lines-per-function` (50 lines, test code exempt)
|
|
345
|
+
and `max-depth` (4), plus `eslint-plugin-jsdoc` rules that require a documentation comment
|
|
336
346
|
on every declared function, method, class, interface member and class field, forbid types in `@param`/`@returns` (the
|
|
337
347
|
tags themselves are welcome, types belong in the signature) and keep a blank line above every documentation block,
|
|
338
348
|
single-line blocks included. The type-checked layer adds `@typescript-eslint/explicit-module-boundary-types` and
|
package/js/eslint/index.js
CHANGED
|
@@ -52,7 +52,9 @@ export default [
|
|
|
52
52
|
'@sinemacula/align-doc-tags': 'error',
|
|
53
53
|
'@sinemacula/single-line-property-doc': 'error',
|
|
54
54
|
'@sinemacula/multiline-function-doc': 'error',
|
|
55
|
+
'@sinemacula/comment-line-wrap': 'error',
|
|
55
56
|
|
|
57
|
+
curly: ['error', 'all'],
|
|
56
58
|
'max-lines-per-function': ['error', { max: 50, skipComments: true, skipBlankLines: true, IIFEs: true }],
|
|
57
59
|
'max-depth': ['error', 4],
|
|
58
60
|
|
package/js/eslint/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import alignDocTags from './rules/align-doc-tags.js';
|
|
2
2
|
import booleanMethodName from './rules/boolean-method-name.js';
|
|
3
|
+
import commentLineWrap from './rules/comment-line-wrap.js';
|
|
3
4
|
import maxMethodsPerClass from './rules/max-methods-per-class.js';
|
|
4
5
|
import multilineFunctionDoc from './rules/multiline-function-doc.js';
|
|
5
6
|
import noBaseError from './rules/no-base-error.js';
|
|
@@ -36,5 +37,6 @@ export default {
|
|
|
36
37
|
'align-doc-tags': alignDocTags,
|
|
37
38
|
'single-line-property-doc': singleLinePropertyDoc,
|
|
38
39
|
'multiline-function-doc': multilineFunctionDoc,
|
|
40
|
+
'comment-line-wrap': commentLineWrap,
|
|
39
41
|
},
|
|
40
42
|
};
|
|
@@ -278,13 +278,12 @@ function buildListeners(state) {
|
|
|
278
278
|
* guard, ...) that returns a result bool is exempt via COMMAND_VERBS. A member
|
|
279
279
|
* may also opt out with an
|
|
280
280
|
* @imperative docblock tag. An on* event-handler callback (onError) and a
|
|
281
|
-
* method named after a primitive type (a typed accessor such as boolean())
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* through the rule options.
|
|
281
|
+
* method named after a primitive type (a typed accessor such as boolean()) are
|
|
282
|
+
* exempt, as are accessors, the constructor, computed names, magic names and
|
|
283
|
+
* type-predicate guards (x is T). The return type is resolved from type
|
|
284
|
+
* information - inferred booleans and awaited Promise<boolean> included - so
|
|
285
|
+
* the rule degrades to a no-op when no type information is available. The
|
|
286
|
+
* accepted vocabulary can be widened per consumer through the rule options.
|
|
288
287
|
*
|
|
289
288
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
290
289
|
* @copyright 2026 Sine Macula Limited
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies a single content line of a comment.
|
|
3
|
+
*
|
|
4
|
+
* Only prose and list items are reflowed; every other kind is preserved
|
|
5
|
+
* verbatim and acts as a paragraph boundary. Directives, docblock tags, fenced
|
|
6
|
+
* or indented code, tables and rule separators are left exactly as written, so
|
|
7
|
+
* a reflow never disturbs a construct whose position or spacing carries
|
|
8
|
+
* meaning.
|
|
9
|
+
*
|
|
10
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
11
|
+
* @copyright 2026 Sine Macula Limited
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { len, trimAscii } from './comment-tokenizer.js';
|
|
15
|
+
|
|
16
|
+
const WS = '[ \\t\\n\\r\\f\\v]';
|
|
17
|
+
const FENCE = /^(```|~~~)/;
|
|
18
|
+
const TAG = new RegExp(`^@[A-Za-z][A-Za-z0-9-]*(?=${WS}|$)`);
|
|
19
|
+
const LIST = new RegExp(`^${WS}*([-*+]|\\d+[.)])${WS}+`);
|
|
20
|
+
const HEADING = new RegExp(`^#{1,6}${WS}`);
|
|
21
|
+
const DIRECTIVE = /^(phpcs:|phpstan-ignore|@phpstan-|@psalm-|@phan-|eslint-disable|eslint-enable|@ts-|prettier-ignore|stylelint-|@codingStandards|@SuppressWarnings|NOSONAR|qlty-ignore)/;
|
|
22
|
+
const SEPARATOR = /^[=\-~*_#.+ ]{3,}$/;
|
|
23
|
+
const CODE = new RegExp(`=>|->|::|;${WS}*$|\\{${WS}*$|^\\}|^(?:if|elseif|for|foreach|while|switch|catch)${WS}*\\(|^[\\w$>[\\]'.-]+${WS}*=[^=>]|^\\$`);
|
|
24
|
+
const SPAN = /`[^`]*`|\{@[^}]*\}|\[[^\]]*\]\([^)]*\)/g;
|
|
25
|
+
const TYPE_TAG = /^@(?:(?:phpstan|psalm|phan)-(?:type|import-type|param|return|var)|var|param|return|typedef|type|property|returns)\b/;
|
|
26
|
+
const TYPE_CONTINUES = /[{<(,:|&]\s*$/;
|
|
27
|
+
const QUOTE = /'[^']*'|"[^"]*"/g;
|
|
28
|
+
const INDENT = /^(?: {2,}|\t)/;
|
|
29
|
+
const LEADING_SPACES = /^ +/;
|
|
30
|
+
const ARROWS = /->|=>/g;
|
|
31
|
+
const OPENERS = /[{<(]/g;
|
|
32
|
+
const CLOSERS = /[}>)]/g;
|
|
33
|
+
|
|
34
|
+
/** A list marker: a bullet or an ordered number, and the space after it. */
|
|
35
|
+
export const LIST_MARKER = new RegExp(`^(${WS}*)([-*+]|\\d+[.)])${WS}+`);
|
|
36
|
+
|
|
37
|
+
/** The kinds a comment content line can take. Only prose and lists reflow. */
|
|
38
|
+
export const KIND = {
|
|
39
|
+
BLANK: 'blank',
|
|
40
|
+
PROSE: 'prose',
|
|
41
|
+
LIST: 'list',
|
|
42
|
+
TAG: 'tag',
|
|
43
|
+
DIRECTIVE: 'directive',
|
|
44
|
+
SEPARATOR: 'separator',
|
|
45
|
+
TABLE: 'table',
|
|
46
|
+
FENCE: 'fence',
|
|
47
|
+
CODE: 'code',
|
|
48
|
+
HEADING: 'heading',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The ordered rules mapping the first matching leading mark to a kind. */
|
|
52
|
+
const KINDS = [
|
|
53
|
+
{ kind: KIND.FENCE, test: ({ trimmed }) => FENCE.test(trimmed) },
|
|
54
|
+
{ kind: KIND.DIRECTIVE, test: ({ trimmed }) => DIRECTIVE.test(trimmed) },
|
|
55
|
+
{ kind: KIND.TAG, test: ({ trimmed }) => TAG.test(trimmed) },
|
|
56
|
+
{ kind: KIND.TABLE, test: ({ trimmed }) => trimmed.startsWith('|') },
|
|
57
|
+
{ kind: KIND.HEADING, test: ({ trimmed }) => HEADING.test(trimmed) },
|
|
58
|
+
{ kind: KIND.SEPARATOR, test: ({ trimmed }) => SEPARATOR.test(trimmed) },
|
|
59
|
+
{ kind: KIND.LIST, test: ({ content }) => LIST.test(content) },
|
|
60
|
+
{ kind: KIND.CODE, test: ({ trimmed }) => CODE.test(trimmed.replace(SPAN, '')) },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Classify a content line, given whether it sits inside a fenced block.
|
|
65
|
+
*/
|
|
66
|
+
export function classify(content, inFence) {
|
|
67
|
+
const trimmed = trimAscii(content);
|
|
68
|
+
|
|
69
|
+
if (inFence) {
|
|
70
|
+
return KIND.FENCE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return trimmed === '' ? KIND.BLANK : kindOf(content, trimmed);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Classify a non-blank line by its leading marks, defaulting to prose. */
|
|
77
|
+
function kindOf(content, trimmed) {
|
|
78
|
+
const line = { content, trimmed };
|
|
79
|
+
|
|
80
|
+
return KINDS.find(({ test }) => test(line))?.kind ?? KIND.PROSE;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Whether a content line opens or closes a fenced block. */
|
|
84
|
+
export function isFence(content) {
|
|
85
|
+
return FENCE.test(trimAscii(content));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Parse a list marker into its bullet, indent and occupied width, or null. */
|
|
89
|
+
export function listMarker(content) {
|
|
90
|
+
const match = LIST_MARKER.exec(content);
|
|
91
|
+
|
|
92
|
+
if (!match) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { marker: match[2], indent: len(match[1]), width: len(match[2]) + 1 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a content line is indented past the baseline: a tab or two spaces. */
|
|
100
|
+
export function indented(content) {
|
|
101
|
+
return INDENT.test(content);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The number of leading spaces on a content line, for the hanging-indent bound. */
|
|
105
|
+
export function indentWidth(content) {
|
|
106
|
+
return content.length - content.replace(LEADING_SPACES, '').length;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether a line ends the verbatim type block before it: a blank line or a fresh tag. */
|
|
110
|
+
export function isTypeBoundary(content) {
|
|
111
|
+
const trimmed = trimAscii(content);
|
|
112
|
+
|
|
113
|
+
return trimmed === '' || trimmed.startsWith('@');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The net depth a line's type slot opens, ignoring inline spans and arrows. */
|
|
117
|
+
export function bracketDelta(text) {
|
|
118
|
+
const stripped = typeSlot(text).replace(ARROWS, '');
|
|
119
|
+
|
|
120
|
+
return (stripped.match(OPENERS) ?? []).length - (stripped.match(CLOSERS) ?? []).length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The depth a type-annotation tag opens, or zero when it is not one or closes here. */
|
|
124
|
+
export function typeTagDepth(content) {
|
|
125
|
+
if (!TYPE_TAG.test(trimAscii(content))) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const delta = bracketDelta(content);
|
|
130
|
+
|
|
131
|
+
return delta > 0 && TYPE_CONTINUES.test(typeSlot(content)) ? delta : 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The type-carrying portion of a line: spans and quotes removed, and everything from the first variable dropped. */
|
|
135
|
+
function typeSlot(content) {
|
|
136
|
+
const stripped = content.replace(SPAN, '').replace(QUOTE, '');
|
|
137
|
+
const variable = stripped.indexOf('$');
|
|
138
|
+
|
|
139
|
+
return variable === -1 ? stripped : stripped.slice(0, variable);
|
|
140
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { createRule } from './lib.js';
|
|
2
|
+
import { reflow } from './comment-reflow.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_LENGTH = 80;
|
|
5
|
+
|
|
6
|
+
/** A docblock interior line: its indent, the star, and the prose after it. */
|
|
7
|
+
const DOC_LINE = /^([ \t\n\r\f\v]*)\*( ?)(.*)$/;
|
|
8
|
+
|
|
9
|
+
/** Whether only whitespace precedes the comment on its own line. */
|
|
10
|
+
function isStandalone(comment, sourceCode) {
|
|
11
|
+
const line = sourceCode.lines[comment.loc.start.line - 1];
|
|
12
|
+
|
|
13
|
+
return line.slice(0, comment.loc.start.column).trim() === '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Group standalone `//` comments into runs of adjacent lines sharing an indent,
|
|
18
|
+
* so a wrapped paragraph is reflowed as one unit.
|
|
19
|
+
*/
|
|
20
|
+
function slashRuns(comments, sourceCode) {
|
|
21
|
+
const runs = [];
|
|
22
|
+
let current = null;
|
|
23
|
+
|
|
24
|
+
for (const comment of comments) {
|
|
25
|
+
if (comment.type !== 'Line' || !isStandalone(comment, sourceCode)) {
|
|
26
|
+
current = null;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
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);
|
|
34
|
+
} else {
|
|
35
|
+
current = [comment];
|
|
36
|
+
runs.push(current);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return runs;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Strip the single optional space that follows a `//` from a comment's value. */
|
|
44
|
+
function slashContent(value) {
|
|
45
|
+
return value.startsWith(' ') ? value.slice(1) : value;
|
|
46
|
+
}
|
|
47
|
+
|
|
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;
|
|
51
|
+
const indent = sourceCode.lines[first.line - 1].slice(0, first.column);
|
|
52
|
+
|
|
53
|
+
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),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The margin-stripped interior lines of a docblock, or null for an odd shape. */
|
|
63
|
+
function docContent(comment, sourceCode) {
|
|
64
|
+
const { lines } = sourceCode;
|
|
65
|
+
const open = comment.loc.start.line;
|
|
66
|
+
const close = comment.loc.end.line;
|
|
67
|
+
|
|
68
|
+
if (open === close || lines[open - 1].trim() !== '/**' || lines[close - 1].trim() !== '*/') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const content = [];
|
|
73
|
+
const locs = [];
|
|
74
|
+
let indent = null;
|
|
75
|
+
|
|
76
|
+
for (let line = open + 1; line < close; line += 1) {
|
|
77
|
+
const match = DOC_LINE.exec(lines[line - 1]);
|
|
78
|
+
|
|
79
|
+
if (!match || (indent !== null && match[1] !== indent)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
indent ??= match[1];
|
|
84
|
+
content.push(match[3]);
|
|
85
|
+
locs.push({ start: { line, column: 0 }, end: { line, column: lines[line - 1].length } });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return content.length === 0 ? null : { content, locs, indent };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Describe a docblock: its content lines, margin, report locations and rebuild. */
|
|
92
|
+
function docDescriptor(comment, sourceCode, eol) {
|
|
93
|
+
if (comment.type !== 'Block' || !comment.value.startsWith('*')) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parsed = docContent(comment, sourceCode);
|
|
98
|
+
|
|
99
|
+
if (parsed === null) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { content, locs, indent } = parsed;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
content,
|
|
107
|
+
marginWidth: indent.length + 2,
|
|
108
|
+
locs,
|
|
109
|
+
range: comment.range,
|
|
110
|
+
rebuild: lines => `/**${eol}${lines.map(line => (line === '' ? `${indent}*` : `${indent}* ${line}`)).join(eol)}${eol}${indent}*/`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Reflow a block and report each overflowing and prematurely wrapped line. */
|
|
115
|
+
function enforce(context, descriptor, maxLength) {
|
|
116
|
+
const result = reflow(descriptor.content, descriptor.marginWidth, maxLength);
|
|
117
|
+
|
|
118
|
+
if (result.long.length === 0 && result.premature.length === 0) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Replace the whole comment block with its reflowed, canonical form. */
|
|
123
|
+
const fix = fixer => fixer.replaceTextRange(descriptor.range, descriptor.rebuild(result.lines));
|
|
124
|
+
|
|
125
|
+
/** Report one faulted line under the given message. */
|
|
126
|
+
const report = (index, messageId) => context.report({ loc: descriptor.locs[index], messageId, data: { max: maxLength }, fix });
|
|
127
|
+
|
|
128
|
+
result.long.forEach(index => report(index, 'tooLong'));
|
|
129
|
+
result.premature.forEach(index => report(index, 'prematureWrap'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Keep standalone comment prose wrapped to a readable width.
|
|
134
|
+
*
|
|
135
|
+
* The syntax-only counterpart of the PHP comment line length sniff. It fills
|
|
136
|
+
* each line greedily with as many whole words as fit and reports two faults on
|
|
137
|
+
* 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.
|
|
145
|
+
*
|
|
146
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
147
|
+
* @copyright 2026 Sine Macula Limited
|
|
148
|
+
*/
|
|
149
|
+
export default createRule({
|
|
150
|
+
name: 'comment-line-wrap',
|
|
151
|
+
meta: {
|
|
152
|
+
type: 'layout',
|
|
153
|
+
fixable: 'whitespace',
|
|
154
|
+
docs: {
|
|
155
|
+
description: 'Keep standalone comment prose wrapped greedily to a readable width.',
|
|
156
|
+
},
|
|
157
|
+
schema: [
|
|
158
|
+
{
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
maxLength: {
|
|
162
|
+
type: 'integer',
|
|
163
|
+
minimum: 1,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
messages: {
|
|
170
|
+
tooLong: 'Comment line must not exceed {{ max }} characters.',
|
|
171
|
+
prematureWrap: 'Comment line wraps before it needs to; the next word fits within {{ max }} characters.',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
defaultOptions: [{ maxLength: DEFAULT_MAX_LENGTH }],
|
|
175
|
+
create(context, [options]) {
|
|
176
|
+
const { sourceCode } = context;
|
|
177
|
+
const maxLength = options.maxLength ?? DEFAULT_MAX_LENGTH;
|
|
178
|
+
const eol = sourceCode.text.includes('\r\n') ? '\r\n' : '\n';
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
Program() {
|
|
182
|
+
const comments = sourceCode.getAllComments();
|
|
183
|
+
|
|
184
|
+
for (const run of slashRuns(comments, sourceCode)) {
|
|
185
|
+
enforce(context, slashDescriptor(run, sourceCode, eol), maxLength);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const comment of comments) {
|
|
189
|
+
const descriptor = docDescriptor(comment, sourceCode, eol);
|
|
190
|
+
|
|
191
|
+
if (descriptor !== null) {
|
|
192
|
+
enforce(context, descriptor, maxLength);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reflows a single paragraph of comment prose to its greedy canonical form.
|
|
3
|
+
*
|
|
4
|
+
* A paragraph is a run of prose lines, or one list item with its continuation
|
|
5
|
+
* lines. It is filled line by line with as many whole words as the width
|
|
6
|
+
* allows, never splitting a word and keeping any list marker's hanging indent.
|
|
7
|
+
* A paragraph is reflowed only when it is safe to: it holds no token wider than
|
|
8
|
+
* the line, is not too narrow, and would still read as prose once wrapped.
|
|
9
|
+
* Otherwise it is returned untouched.
|
|
10
|
+
*
|
|
11
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
12
|
+
* @copyright 2026 Sine Macula Limited
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { KIND, LIST_MARKER, classify } from './comment-classifier.js';
|
|
16
|
+
import { len, tokenize, trimAsciiStart } from './comment-tokenizer.js';
|
|
17
|
+
|
|
18
|
+
const POISON = /^(@[A-Za-z][A-Za-z0-9-]*|[-*+]|\d+[.)]|#{1,6})$/;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reflow a paragraph, returning its output lines and the faults found, their
|
|
22
|
+
* indices offset from the given base, or the verbatim input when it cannot be
|
|
23
|
+
* reflowed or carries no fault.
|
|
24
|
+
*/
|
|
25
|
+
export function reflowParagraph(slice, marker, marginWidth, maxLength, base) {
|
|
26
|
+
const wrapped = wrapLines(slice, marker, marginWidth, maxLength);
|
|
27
|
+
const faults = wrapped === null ? { long: [], premature: [] } : findFaults(slice, marginWidth, maxLength);
|
|
28
|
+
|
|
29
|
+
if (wrapped === null || (faults.long.length === 0 && faults.premature.length === 0)) {
|
|
30
|
+
return { lines: slice, long: [], premature: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
lines: wrapped,
|
|
35
|
+
long: faults.long.map(offset => base + offset),
|
|
36
|
+
premature: faults.premature.map(offset => base + offset),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The greedy canonical lines of a paragraph, or null when it holds an
|
|
42
|
+
* unwrappable token, is too narrow, or would re-classify once wrapped.
|
|
43
|
+
*/
|
|
44
|
+
function wrapLines(slice, marker, marginWidth, maxLength) {
|
|
45
|
+
const indent = marker === null ? leadingSpaces(slice[0]) : marker.indent;
|
|
46
|
+
const hanging = indent + (marker === null ? 0 : marker.width);
|
|
47
|
+
const width = maxLength - marginWidth - hanging;
|
|
48
|
+
const tokens = glue(tokenize(joinText(slice, marker)));
|
|
49
|
+
|
|
50
|
+
if (width < 1 || tokens.length === 0 || longestToken(tokens) > width) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lines = layout(greedy(tokens, width), marker, indent, hanging);
|
|
55
|
+
|
|
56
|
+
return stable(lines, marker) ? lines : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Assemble output lines, prefixing the marker then the hanging indent. */
|
|
60
|
+
function layout(wrapped, marker, indent, hanging) {
|
|
61
|
+
return wrapped.map((text, offset) =>
|
|
62
|
+
offset === 0 && marker !== null
|
|
63
|
+
? `${' '.repeat(indent)}${marker.marker} ${text}`
|
|
64
|
+
: `${' '.repeat(hanging)}${text}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Whether every reflowed line still classifies as it must. */
|
|
69
|
+
function stable(lines, marker) {
|
|
70
|
+
return lines.every((line, offset) => {
|
|
71
|
+
const expected = offset === 0 && marker !== null ? KIND.LIST : KIND.PROSE;
|
|
72
|
+
|
|
73
|
+
return classify(line, false) === expected;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The input offsets that overflow the width and those that wrap early. */
|
|
78
|
+
function findFaults(slice, marginWidth, maxLength) {
|
|
79
|
+
const long = [];
|
|
80
|
+
const premature = [];
|
|
81
|
+
|
|
82
|
+
slice.forEach((content, offset) => {
|
|
83
|
+
if (marginWidth + len(content) > maxLength) {
|
|
84
|
+
long.push(offset);
|
|
85
|
+
}
|
|
86
|
+
if (wrapsEarly(slice, offset, marginWidth, maxLength)) {
|
|
87
|
+
premature.push(offset);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { long, premature };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether a line could hold the first word of the next line within the width. */
|
|
95
|
+
function wrapsEarly(slice, offset, marginWidth, maxLength) {
|
|
96
|
+
if (offset + 1 >= slice.length) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const next = glue(tokenize(slice[offset + 1]))[0] ?? '';
|
|
101
|
+
|
|
102
|
+
return next !== '' && marginWidth + len(slice[offset]) + 1 + len(next) <= maxLength;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Join a paragraph's lines, stripping any list marker from the first line. */
|
|
106
|
+
function joinText(slice, marker) {
|
|
107
|
+
const first = marker === null ? trimAsciiStart(slice[0]) : slice[0].replace(LIST_MARKER, '');
|
|
108
|
+
const rest = slice.slice(1).map(line => trimAsciiStart(line));
|
|
109
|
+
|
|
110
|
+
return [first, ...rest].join(' ');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Merge backward any token that must never open a wrapped line. */
|
|
114
|
+
function glue(tokens) {
|
|
115
|
+
const out = [];
|
|
116
|
+
|
|
117
|
+
for (const token of tokens) {
|
|
118
|
+
if (out.length > 0 && POISON.test(token)) {
|
|
119
|
+
out[out.length - 1] += ` ${token}`;
|
|
120
|
+
} else {
|
|
121
|
+
out.push(token);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Fill lines greedily with whole tokens up to the width. */
|
|
129
|
+
function greedy(tokens, width) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
let current = '';
|
|
132
|
+
|
|
133
|
+
for (const token of tokens) {
|
|
134
|
+
if (current === '') {
|
|
135
|
+
current = token;
|
|
136
|
+
} else if (len(current) + 1 + len(token) <= width) {
|
|
137
|
+
current += ` ${token}`;
|
|
138
|
+
} else {
|
|
139
|
+
lines.push(current);
|
|
140
|
+
current = token;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (current !== '') {
|
|
145
|
+
lines.push(current);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return lines;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The length of the longest token. */
|
|
152
|
+
function longestToken(tokens) {
|
|
153
|
+
return Math.max(...tokens.map(len));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The number of leading spaces on a line. */
|
|
157
|
+
function leadingSpaces(text) {
|
|
158
|
+
return len(text) - len(trimAsciiStart(text));
|
|
159
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic greedy re-wrapper for comment prose.
|
|
3
|
+
*
|
|
4
|
+
* The syntax-only counterpart of the PHP comment line length sniff: given the
|
|
5
|
+
* content lines of one comment block (their margin already stripped) and the
|
|
6
|
+
* width that margin will reclaim, it walks the block and hands each run of
|
|
7
|
+
* prose and each list item to a paragraph reflow, gathering the canonical lines
|
|
8
|
+
* and the indices of the input lines that overflow or wrap prematurely. A
|
|
9
|
+
* fenced block, a type-annotation tag whose value opens a bracketed type across
|
|
10
|
+
* lines, headings, indented code, tags, directives, tables and separators are
|
|
11
|
+
* left verbatim and bound the paragraph around them.
|
|
12
|
+
*
|
|
13
|
+
* Widths count Unicode code points and whitespace is matched as ASCII only, so
|
|
14
|
+
* the output is byte-for-byte identical to the PHP engine (mb_strlen and the
|
|
15
|
+
* non-unicode PCRE `\s`).
|
|
16
|
+
*
|
|
17
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
18
|
+
* @copyright 2026 Sine Macula Limited
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { KIND, bracketDelta, classify, indentWidth, indented, isFence, isTypeBoundary, listMarker, typeTagDepth } from './comment-classifier.js';
|
|
22
|
+
import { reflowParagraph } from './comment-paragraph.js';
|
|
23
|
+
|
|
24
|
+
export { KIND, classify, isFence, listMarker } from './comment-classifier.js';
|
|
25
|
+
export { tokenize } from './comment-tokenizer.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reflow a block's content lines to the greedy canonical form, returning the
|
|
29
|
+
* canonical lines and the indices of the input lines that overflow or wrap
|
|
30
|
+
* prematurely.
|
|
31
|
+
*/
|
|
32
|
+
export function reflow(lines, marginWidth, maxLength) {
|
|
33
|
+
const ctx = { out: [], long: [], premature: [], marginWidth, maxLength, inFence: false, typeDepth: 0 };
|
|
34
|
+
let i = 0;
|
|
35
|
+
|
|
36
|
+
while (i < lines.length) {
|
|
37
|
+
i = step(lines, i, ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { lines: ctx.out, long: ctx.long, premature: ctx.premature };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Advance one line, keeping verbatim any line inside a fenced block or inside a
|
|
45
|
+
* type-annotation tag whose bracketed value is still open.
|
|
46
|
+
*/
|
|
47
|
+
function step(lines, i, ctx) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
|
|
50
|
+
if (ctx.inFence) {
|
|
51
|
+
ctx.inFence = !isFence(line);
|
|
52
|
+
|
|
53
|
+
return verbatim(line, i, ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (ctx.typeDepth > 0 && !isTypeBoundary(line)) {
|
|
57
|
+
ctx.typeDepth = Math.max(0, ctx.typeDepth + bracketDelta(line));
|
|
58
|
+
|
|
59
|
+
return verbatim(line, i, ctx);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ctx.typeDepth = 0;
|
|
63
|
+
|
|
64
|
+
const opening = typeTagDepth(line);
|
|
65
|
+
|
|
66
|
+
if (opening > 0) {
|
|
67
|
+
ctx.typeDepth = opening;
|
|
68
|
+
|
|
69
|
+
return verbatim(line, i, ctx);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return consume(lines, i, ctx);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Dispatch a line by kind: an indented line is verbatim; else fence, prose or list. */
|
|
76
|
+
function consume(lines, i, ctx) {
|
|
77
|
+
if (indented(lines[i])) {
|
|
78
|
+
return verbatim(lines[i], i, ctx);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const type = classify(lines[i], false);
|
|
82
|
+
|
|
83
|
+
if (type === KIND.PROSE) {
|
|
84
|
+
return paragraph(lines, i, null, ctx);
|
|
85
|
+
}
|
|
86
|
+
if (type === KIND.LIST) {
|
|
87
|
+
return paragraph(lines, i, listMarker(lines[i]), ctx);
|
|
88
|
+
}
|
|
89
|
+
if (type === KIND.FENCE) {
|
|
90
|
+
ctx.inFence = true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return verbatim(lines[i], i, ctx);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Emit one line unchanged and advance past it. */
|
|
97
|
+
function verbatim(line, i, ctx) {
|
|
98
|
+
ctx.out.push(line);
|
|
99
|
+
|
|
100
|
+
return i + 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Append a run of prose or a list item, reflowed where it faults. A list line
|
|
105
|
+
* always parses to a marker, so it never reaches the plain-prose branch.
|
|
106
|
+
*/
|
|
107
|
+
function paragraph(lines, start, marker, ctx) {
|
|
108
|
+
const end = marker === null ? flushEnd(lines, start) : continuationEnd(lines, start + 1, marker.indent + marker.width);
|
|
109
|
+
const slice = lines.slice(start, end);
|
|
110
|
+
const segment = reflowParagraph(slice, marker, ctx.marginWidth, ctx.maxLength, start);
|
|
111
|
+
|
|
112
|
+
ctx.out.push(...segment.lines);
|
|
113
|
+
segment.long.forEach(offset => ctx.long.push(offset));
|
|
114
|
+
segment.premature.forEach(offset => ctx.premature.push(offset));
|
|
115
|
+
|
|
116
|
+
return end;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The index one past the last flush prose line from the given start. An
|
|
121
|
+
* indented line ends the paragraph, bounding a verbatim indented code block.
|
|
122
|
+
*/
|
|
123
|
+
function flushEnd(lines, from) {
|
|
124
|
+
let end = from;
|
|
125
|
+
|
|
126
|
+
while (end < lines.length && classify(lines[end], false) === KIND.PROSE && !indented(lines[end])) {
|
|
127
|
+
end += 1;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return end;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The index one past the last continuation line of a list item. A line indented
|
|
135
|
+
* past the hanging indent is verbatim code, not a continuation.
|
|
136
|
+
*/
|
|
137
|
+
function continuationEnd(lines, from, hanging) {
|
|
138
|
+
let end = from;
|
|
139
|
+
|
|
140
|
+
while (end < lines.length && classify(lines[end], false) === KIND.PROSE && indentWidth(lines[end]) <= hanging) {
|
|
141
|
+
end += 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return end;
|
|
145
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text primitives for the comment reflow engine.
|
|
3
|
+
*
|
|
4
|
+
* Splits prose into wrapping tokens, keeping inline code, an inline `{@link}`
|
|
5
|
+
* tag and a Markdown link whole even when they hold spaces, and measures widths
|
|
6
|
+
* in Unicode code points. Widths and whitespace match the PHP engine exactly:
|
|
7
|
+
* code points as mb_strlen counts them, and ASCII-only trimming as trim does.
|
|
8
|
+
*
|
|
9
|
+
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
10
|
+
* @copyright 2026 Sine Macula Limited
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const ASCII_TRIM = /^[ \t\n\r\0\v]+|[ \t\n\r\0\v]+$/g;
|
|
14
|
+
const ASCII_TRIM_START = /^[ \t\n\r\0\v]+/;
|
|
15
|
+
|
|
16
|
+
/** The number of Unicode code points in a string, matching PHP mb_strlen. */
|
|
17
|
+
export function len(text) {
|
|
18
|
+
return [...text].length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Strip leading and trailing ASCII whitespace, matching PHP trim. */
|
|
22
|
+
export function trimAscii(text) {
|
|
23
|
+
return text.replace(ASCII_TRIM, '');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Strip leading ASCII whitespace, matching PHP ltrim. */
|
|
27
|
+
export function trimAsciiStart(text) {
|
|
28
|
+
return text.replace(ASCII_TRIM_START, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Split a line of prose into tokens, keeping inline code, link tags and
|
|
33
|
+
* Markdown links whole even when they hold spaces.
|
|
34
|
+
*/
|
|
35
|
+
export function tokenize(text) {
|
|
36
|
+
const chars = [...text];
|
|
37
|
+
const tokens = [];
|
|
38
|
+
let i = 0;
|
|
39
|
+
|
|
40
|
+
while (i < chars.length) {
|
|
41
|
+
if (chars[i] === ' ') {
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const start = i;
|
|
47
|
+
i = consumeToken(chars, i);
|
|
48
|
+
tokens.push(chars.slice(start, i).join(''));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return tokens;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Advance past one token, stepping over any atomic span it contains. */
|
|
55
|
+
function consumeToken(chars, i) {
|
|
56
|
+
while (i < chars.length && chars[i] !== ' ') {
|
|
57
|
+
if (chars[i] === '`') {
|
|
58
|
+
i = consumeSpan(chars, i, '`');
|
|
59
|
+
} else if (chars[i] === '{' && chars[i + 1] === '@') {
|
|
60
|
+
i = consumeSpan(chars, i, '}');
|
|
61
|
+
} else if (chars[i] === '[') {
|
|
62
|
+
i = consumeLink(chars, i);
|
|
63
|
+
} else {
|
|
64
|
+
i += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return i;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Advance past a delimited span to its closer, or by one when it never closes. */
|
|
72
|
+
function consumeSpan(chars, i, close) {
|
|
73
|
+
const found = chars.indexOf(close, i + 1);
|
|
74
|
+
|
|
75
|
+
return found === -1 ? i + 1 : found + 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Advance past a Markdown link, or by one when the shape does not hold. */
|
|
79
|
+
function consumeLink(chars, i) {
|
|
80
|
+
const label = consumeSpan(chars, i, ']');
|
|
81
|
+
|
|
82
|
+
if (label === i + 1 || chars[label] !== '(') {
|
|
83
|
+
return i + 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const target = consumeSpan(chars, label, ')');
|
|
87
|
+
|
|
88
|
+
return target === label + 1 ? i + 1 : target;
|
|
89
|
+
}
|
package/js/eslint/vue.js
CHANGED
|
@@ -8,14 +8,14 @@ import tseslint from 'typescript-eslint';
|
|
|
8
8
|
* through the TypeScript parser, and holds component filenames to the same
|
|
9
9
|
* kebab-case convention the shared formatter enforces on plain sources.
|
|
10
10
|
*
|
|
11
|
-
* This layer also carries the template layout rules, which the shared
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* This layer also carries the template layout rules, which the shared formatter
|
|
12
|
+
* cannot: it does not understand single-file components, so `.vue` markup would
|
|
13
|
+
* otherwise go unformatted entirely.
|
|
14
14
|
*
|
|
15
15
|
* Additive, and deliberately unlike the type-aware layer: it carries no base
|
|
16
16
|
* rules of its own, so spread it after whichever layer a repo already uses
|
|
17
|
-
* rather than in place of one. Vue is orthogonal to type-awareness, and a
|
|
18
|
-
*
|
|
17
|
+
* rather than in place of one. Vue is orthogonal to type-awareness, and a repo
|
|
18
|
+
* enabling both would otherwise apply the base rules twice.
|
|
19
19
|
*
|
|
20
20
|
* @author Ben Carey <bdmc@sinemacula.co.uk>
|
|
21
21
|
* @copyright 2026 Sine Macula Limited
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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>",
|