@sinemacula/coding-standards 1.15.0 → 1.17.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
|
@@ -334,10 +334,12 @@ one line where present; a free function keeps the freedom of either shape.
|
|
|
334
334
|
|
|
335
335
|
`comment-line-wrap` takes `maxLength` (default 80) and is the syntax-only counterpart of the PHP
|
|
336
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.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
337
|
+
reporting an overflowing line and a prematurely wrapped line on their own footings and autofixing both. Markdown
|
|
338
|
+
headings, docblock tag lines, machine-parsed tool directives (`eslint`, `biome-ignore`, `@ts-`, `Stryker`, `c8`/`v8`/
|
|
339
|
+
`istanbul ignore`, `@vite-ignore` and the like), fenced code, an indented code or command block, a doc-tag whose
|
|
340
|
+
value opens a multi-line bracketed type (an `array{...}` shape, a `<...>` generic or a `\Closure(...)` signature),
|
|
341
|
+
tables, separators, a line whose overflow is a single unbreakable token such as a long name or URL, trailing comments
|
|
342
|
+
after code and compact single-line docblocks are left untouched.
|
|
341
343
|
|
|
342
344
|
The base layer also switches on a set of built-in rules: `@typescript-eslint/no-explicit-any`, `curly` (a brace on every
|
|
343
345
|
control statement, as PSR-12 already requires on the PHP side), `max-lines-per-function` (50 lines, test code exempt)
|
|
@@ -17,10 +17,19 @@ const WS = '[ \\t\\n\\r\\f\\v]';
|
|
|
17
17
|
const FENCE = /^(```|~~~)/;
|
|
18
18
|
const TAG = new RegExp(`^@[A-Za-z][A-Za-z0-9-]*(?=${WS}|$)`);
|
|
19
19
|
const LIST = new RegExp(`^${WS}*([-*+]|\\d+[.)])${WS}+`);
|
|
20
|
-
const
|
|
20
|
+
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)/;
|
|
21
22
|
const SEPARATOR = /^[=\-~*_#.+ ]{3,}$/;
|
|
22
23
|
const CODE = new RegExp(`=>|->|::|;${WS}*$|\\{${WS}*$|^\\}|^(?:if|elseif|for|foreach|while|switch|catch)${WS}*\\(|^[\\w$>[\\]'.-]+${WS}*=[^=>]|^\\$`);
|
|
23
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;
|
|
24
33
|
|
|
25
34
|
/** A list marker: a bullet or an ordered number, and the space after it. */
|
|
26
35
|
export const LIST_MARKER = new RegExp(`^(${WS}*)([-*+]|\\d+[.)])${WS}+`);
|
|
@@ -36,6 +45,7 @@ export const KIND = {
|
|
|
36
45
|
TABLE: 'table',
|
|
37
46
|
FENCE: 'fence',
|
|
38
47
|
CODE: 'code',
|
|
48
|
+
HEADING: 'heading',
|
|
39
49
|
};
|
|
40
50
|
|
|
41
51
|
/** The ordered rules mapping the first matching leading mark to a kind. */
|
|
@@ -44,6 +54,7 @@ const KINDS = [
|
|
|
44
54
|
{ kind: KIND.DIRECTIVE, test: ({ trimmed }) => DIRECTIVE.test(trimmed) },
|
|
45
55
|
{ kind: KIND.TAG, test: ({ trimmed }) => TAG.test(trimmed) },
|
|
46
56
|
{ kind: KIND.TABLE, test: ({ trimmed }) => trimmed.startsWith('|') },
|
|
57
|
+
{ kind: KIND.HEADING, test: ({ trimmed }) => HEADING.test(trimmed) },
|
|
47
58
|
{ kind: KIND.SEPARATOR, test: ({ trimmed }) => SEPARATOR.test(trimmed) },
|
|
48
59
|
{ kind: KIND.LIST, test: ({ content }) => LIST.test(content) },
|
|
49
60
|
{ kind: KIND.CODE, test: ({ trimmed }) => CODE.test(trimmed.replace(SPAN, '')) },
|
|
@@ -84,3 +95,46 @@ export function listMarker(content) {
|
|
|
84
95
|
|
|
85
96
|
return { marker: match[2], indent: len(match[1]), width: len(match[2]) + 1 };
|
|
86
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
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { KIND, LIST_MARKER, classify } from './comment-classifier.js';
|
|
16
16
|
import { len, tokenize, trimAsciiStart } from './comment-tokenizer.js';
|
|
17
17
|
|
|
18
|
-
const POISON = /^(@[A-Za-z][A-Za-z0-9-]*|[-*+]|\d+[.)])$/;
|
|
18
|
+
const POISON = /^(@[A-Za-z][A-Za-z0-9-]*|[-*+]|\d+[.)]|#{1,6})$/;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Reflow a paragraph, returning its output lines and the faults found, their
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* content lines of one comment block (their margin already stripped) and the
|
|
6
6
|
* width that margin will reclaim, it walks the block and hands each run of
|
|
7
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.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
12
|
*
|
|
12
13
|
* Widths count Unicode code points and whitespace is matched as ASCII only, so
|
|
13
14
|
* the output is byte-for-byte identical to the PHP engine (mb_strlen and the
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
* @copyright 2026 Sine Macula Limited
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
|
-
import { KIND, classify, isFence, listMarker } from './comment-classifier.js';
|
|
21
|
+
import { KIND, bracketDelta, classify, indentWidth, indented, isFence, isTypeBoundary, listMarker, typeTagDepth } from './comment-classifier.js';
|
|
21
22
|
import { reflowParagraph } from './comment-paragraph.js';
|
|
22
23
|
|
|
23
24
|
export { KIND, classify, isFence, listMarker } from './comment-classifier.js';
|
|
@@ -29,42 +30,73 @@ export { tokenize } from './comment-tokenizer.js';
|
|
|
29
30
|
* prematurely.
|
|
30
31
|
*/
|
|
31
32
|
export function reflow(lines, marginWidth, maxLength) {
|
|
32
|
-
const
|
|
33
|
-
const long = [];
|
|
34
|
-
const premature = [];
|
|
35
|
-
let inFence = false;
|
|
33
|
+
const ctx = { out: [], long: [], premature: [], marginWidth, maxLength, inFence: false, typeDepth: 0 };
|
|
36
34
|
let i = 0;
|
|
37
35
|
|
|
38
36
|
while (i < lines.length) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (inFence) {
|
|
42
|
-
out.push(lines[i]);
|
|
43
|
-
inFence = !isFence(lines[i]);
|
|
44
|
-
i += 1;
|
|
45
|
-
} else {
|
|
46
|
-
i = consume(lines, i, type, { out, long, premature, marginWidth, maxLength, fence: value => { inFence = value; } });
|
|
47
|
-
}
|
|
37
|
+
i = step(lines, i, ctx);
|
|
48
38
|
}
|
|
49
39
|
|
|
50
|
-
return { lines: out, long, premature };
|
|
40
|
+
return { lines: ctx.out, long: ctx.long, premature: ctx.premature };
|
|
51
41
|
}
|
|
52
42
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
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
|
+
|
|
55
83
|
if (type === KIND.PROSE) {
|
|
56
84
|
return paragraph(lines, i, null, ctx);
|
|
57
85
|
}
|
|
58
86
|
if (type === KIND.LIST) {
|
|
59
87
|
return paragraph(lines, i, listMarker(lines[i]), ctx);
|
|
60
88
|
}
|
|
61
|
-
|
|
62
|
-
ctx.out.push(lines[i]);
|
|
63
|
-
|
|
64
89
|
if (type === KIND.FENCE) {
|
|
65
|
-
ctx.
|
|
90
|
+
ctx.inFence = true;
|
|
66
91
|
}
|
|
67
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
|
+
|
|
68
100
|
return i + 1;
|
|
69
101
|
}
|
|
70
102
|
|
|
@@ -73,7 +105,7 @@ function consume(lines, i, type, ctx) {
|
|
|
73
105
|
* always parses to a marker, so it never reaches the plain-prose branch.
|
|
74
106
|
*/
|
|
75
107
|
function paragraph(lines, start, marker, ctx) {
|
|
76
|
-
const end =
|
|
108
|
+
const end = marker === null ? flushEnd(lines, start) : continuationEnd(lines, start + 1, marker.indent + marker.width);
|
|
77
109
|
const slice = lines.slice(start, end);
|
|
78
110
|
const segment = reflowParagraph(slice, marker, ctx.marginWidth, ctx.maxLength, start);
|
|
79
111
|
|
|
@@ -84,11 +116,28 @@ function paragraph(lines, start, marker, ctx) {
|
|
|
84
116
|
return end;
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
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) {
|
|
89
138
|
let end = from;
|
|
90
139
|
|
|
91
|
-
while (end < lines.length && classify(lines[end], false) === KIND.PROSE) {
|
|
140
|
+
while (end < lines.length && classify(lines[end], false) === KIND.PROSE && indentWidth(lines[end]) <= hanging) {
|
|
92
141
|
end += 1;
|
|
93
142
|
}
|
|
94
143
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sinemacula/coding-standards",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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>",
|