sparkle-design-cli 2.5.0-beta.3 → 2.5.0-beta.5
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/CONTRIBUTING.md +28 -2
- package/README.md +22 -1
- package/bin/sparkle-design.js +114 -0
- package/docs/anti-patterns.md +98 -2
- package/docs/config.md +2 -0
- package/lib/anti-pattern-rules.js +352 -8
- package/lib/check-config.js +358 -0
- package/lib/check.js +230 -18
- package/lib/migrate.js +446 -0
- package/lib/path-utils.js +40 -0
- package/lib/rules-report.js +5 -1
- package/lib/stop-hook.js +16 -0
- package/lib/token-migration.js +3 -1
- package/package.json +1 -1
- package/templates/sparkle-variables/README.md +6 -2
- package/templates/sparkle-variables/scripts/validate-tokens.mjs +161 -0
- package/templates/sparkle-variables/sparkle-design.template.css +3 -25
package/lib/check.js
CHANGED
|
@@ -12,6 +12,13 @@ import {
|
|
|
12
12
|
getCheckRules,
|
|
13
13
|
getManualReviewReminders,
|
|
14
14
|
} from './anti-pattern-rules.js';
|
|
15
|
+
import {
|
|
16
|
+
ALLOW_TOKENS_RULE_ID,
|
|
17
|
+
isPathIgnored,
|
|
18
|
+
isTokenAllowed,
|
|
19
|
+
loadCheckConfig,
|
|
20
|
+
unknownIgnoreRuleIds,
|
|
21
|
+
} from './check-config.js';
|
|
15
22
|
import { loadAntiPatternPlugins } from './load-plugins.js';
|
|
16
23
|
import { MATCH_HELPERS } from './plugin-helpers.js';
|
|
17
24
|
import { REGEX, FONT_DOMAINS } from './constants.js';
|
|
@@ -28,6 +35,11 @@ const BUILTIN_RULES = getCheckRules(BUILTIN_ANTI_PATTERN_GROUPS);
|
|
|
28
35
|
|
|
29
36
|
const SEVERITY_ORDER = [SEVERITY.ERROR, SEVERITY.WARNING, SEVERITY.INFO];
|
|
30
37
|
|
|
38
|
+
// ルール定義を持たず check.js が直接出す finding の ID。除外設定で指定できる
|
|
39
|
+
// ルール ID の判定に使う。
|
|
40
|
+
// en: Finding IDs emitted directly by check.js rather than by a rule definition.
|
|
41
|
+
const BUILTIN_REPORT_RULE_IDS = ['font-import-in-css', 'csp-font-block'];
|
|
42
|
+
|
|
31
43
|
/**
|
|
32
44
|
* finding の severity を既知の値に丸める。
|
|
33
45
|
*
|
|
@@ -171,27 +183,43 @@ function formatSnippet(text) {
|
|
|
171
183
|
}
|
|
172
184
|
|
|
173
185
|
/**
|
|
174
|
-
* ESLint 風の suppression
|
|
175
|
-
*
|
|
186
|
+
* ESLint 風の suppression コメントをサポートする。コメントの種類で効く行が
|
|
187
|
+
* 決まっており、取り違えると抑制されない(issue #101):
|
|
176
188
|
*
|
|
177
|
-
* // sparkle-disable-line <rule-id>
|
|
178
|
-
* // sparkle-disable-next-line <rule-id>
|
|
179
|
-
* {/* sparkle-disable-line <rule-id> *\/} (JSX
|
|
189
|
+
* // sparkle-disable-line <rule-id> … finding と**同じ行**に置く
|
|
190
|
+
* // sparkle-disable-next-line <rule-id> … finding の**直前の行**に置く
|
|
191
|
+
* {/* sparkle-disable-line <rule-id> *\/} (JSX コメント。効く行は同上)
|
|
180
192
|
*
|
|
181
193
|
* ユーザーがどうしても character-* に移行できない typography(token に
|
|
182
|
-
*
|
|
183
|
-
*
|
|
194
|
+
* 存在しないサイズなど)を例外扱いしたい場合の escape hatch として用意する。
|
|
195
|
+
* ファイル単位の除外は `sparkle-disable-file`(`getFileSuppressedRules`)か、
|
|
196
|
+
* 設定ファイルの `check.ignore` を使う。
|
|
197
|
+
* ルール ID は複数カンマ区切り可(例:
|
|
184
198
|
* `sparkle-disable-line tailwind-typography, card-padding-override`)。
|
|
185
199
|
*
|
|
186
|
-
* en: ESLint-style suppression comments
|
|
187
|
-
*
|
|
188
|
-
* font sizes). Same-line or previous-line comment is honored.
|
|
200
|
+
* en: ESLint-style suppression comments. `disable-line` applies to its own
|
|
201
|
+
* line only; `disable-next-line` applies to the following line only.
|
|
189
202
|
*/
|
|
190
203
|
const SUPPRESS_LINE =
|
|
191
204
|
/(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
|
|
192
205
|
const SUPPRESS_NEXT_LINE =
|
|
193
206
|
/(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-next-line\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
|
|
194
207
|
|
|
208
|
+
/**
|
|
209
|
+
* ファイル単位の suppression(issue #102)。ファイル内のどこに書いても、
|
|
210
|
+
* そのファイル全体で指定ルールの finding を出さない。行コメントと同じく
|
|
211
|
+
* ルール ID は必須(ID 無しの全面除外はコメントでは許さず、設定ファイルの
|
|
212
|
+
* `check.ignore` に寄せる。ファイルを開いても気付きにくい全面除外を散らさないため)。
|
|
213
|
+
*
|
|
214
|
+
* /* sparkle-disable-file shadcn-token *\/
|
|
215
|
+
* // sparkle-disable-file shadcn-token, tailwind-typography
|
|
216
|
+
*
|
|
217
|
+
* en: File-level suppression. Rule IDs are required, as with line comments;
|
|
218
|
+
* blanket "all rules" ignores belong in `check.ignore` in sparkle.config.json.
|
|
219
|
+
*/
|
|
220
|
+
const SUPPRESS_FILE =
|
|
221
|
+
/(?:\/\/|\/\*|\{\/\*)\s*sparkle-disable-file\s+([A-Za-z0-9,\s_-]+?)\s*(?:\*\/\}?|$)/;
|
|
222
|
+
|
|
195
223
|
function extractSuppressedRules(source, regex) {
|
|
196
224
|
if (!source) return null;
|
|
197
225
|
const m = source.match(regex);
|
|
@@ -204,6 +232,19 @@ function extractSuppressedRules(source, regex) {
|
|
|
204
232
|
);
|
|
205
233
|
}
|
|
206
234
|
|
|
235
|
+
/**
|
|
236
|
+
* `sparkle-disable-file` で除外されたルール ID の集合を返す(複数書けば和集合)。
|
|
237
|
+
* en: Union of rule IDs named by every `sparkle-disable-file` comment in the file.
|
|
238
|
+
*/
|
|
239
|
+
function getFileSuppressedRules(content) {
|
|
240
|
+
const ids = new Set();
|
|
241
|
+
if (!content || !content.includes('sparkle-disable-file')) return ids;
|
|
242
|
+
for (const line of content.split(/\r?\n/)) {
|
|
243
|
+
for (const id of extractSuppressedRules(line, SUPPRESS_FILE) ?? []) ids.add(id);
|
|
244
|
+
}
|
|
245
|
+
return ids;
|
|
246
|
+
}
|
|
247
|
+
|
|
207
248
|
function isSuppressed(ruleId, contentLines, lineNumber) {
|
|
208
249
|
// lineNumber は 1-origin
|
|
209
250
|
// en: 1-based line numbers from getLineNumber.
|
|
@@ -219,11 +260,15 @@ function isSuppressed(ruleId, contentLines, lineNumber) {
|
|
|
219
260
|
function collectFindings(filePath, content, rules = BUILTIN_RULES, options = {}) {
|
|
220
261
|
const fileKind = options.fileKind ?? RULE_TARGET.SOURCE;
|
|
221
262
|
const onRuleError = options.onRuleError;
|
|
263
|
+
const onSuppress = options.onSuppress;
|
|
222
264
|
const findings = [];
|
|
223
265
|
const contentLines = content.split(/\r?\n/);
|
|
224
266
|
const pushFinding = (rule, index, snippet, recommendationOverride) => {
|
|
225
267
|
const line = getLineNumber(content, index ?? 0);
|
|
226
|
-
if (isSuppressed(rule.id, contentLines, line))
|
|
268
|
+
if (isSuppressed(rule.id, contentLines, line)) {
|
|
269
|
+
onSuppress?.(rule.id);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
227
272
|
findings.push({
|
|
228
273
|
filePath,
|
|
229
274
|
id: rule.id,
|
|
@@ -405,9 +450,27 @@ function collectNextjsCspFindings(cwd) {
|
|
|
405
450
|
return findings;
|
|
406
451
|
}
|
|
407
452
|
|
|
453
|
+
/**
|
|
454
|
+
* `allowTokens` に書いたクラスを、`shadcn-token` が検出するか。typo の検出に使う。
|
|
455
|
+
* `shadcn-token` はクラス単体の正規表現なので、単体の文字列で判定して誤りが無い。
|
|
456
|
+
* en: Whether shadcn-token detects this class — used to flag allowTokens typos.
|
|
457
|
+
* The rule is a plain class regex, so probing the bare string is exact.
|
|
458
|
+
*/
|
|
459
|
+
function isDetectedByAllowTokensRule(token, rules) {
|
|
460
|
+
// プラグインが同じ ID のルールを足すこともあるので、該当するものをすべて使う
|
|
461
|
+
// en: Plugins may add rules under the same ID, so probe with all of them.
|
|
462
|
+
const targetRules = rules.filter((candidate) => candidate.id === ALLOW_TOKENS_RULE_ID);
|
|
463
|
+
if (targetRules.length === 0) return true;
|
|
464
|
+
return collectFindings('allow-tokens-probe', token, targetRules, {
|
|
465
|
+
onRuleError: () => {},
|
|
466
|
+
}).some((finding) => finding.snippet === token);
|
|
467
|
+
}
|
|
468
|
+
|
|
408
469
|
function createCheckReport(targets = [], options = {}) {
|
|
409
470
|
const rules = options.rules ?? BUILTIN_RULES;
|
|
410
471
|
const baseReminders = options.baseReminders ?? BUILTIN_MANUAL_REVIEW_REMINDERS;
|
|
472
|
+
const ignoreEntries = options.ignore ?? [];
|
|
473
|
+
const allowTokenEntries = options.allowTokens ?? [];
|
|
411
474
|
const resolvedTargets = targets.length > 0 ? targets : [DEFAULT_TARGET];
|
|
412
475
|
const textFiles = new Set();
|
|
413
476
|
const cssFiles = new Set();
|
|
@@ -428,6 +491,23 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
428
491
|
fileContents.set(filePath, fs.readFileSync(filePath, 'utf8'));
|
|
429
492
|
}
|
|
430
493
|
|
|
494
|
+
// CSS も先に読み、ファイル単位コメントを走査前に集めておく。ルールが落ちたときに
|
|
495
|
+
// 「そのファイル × ルールは除外済みか」を判定するため。
|
|
496
|
+
// en: Read CSS up front and gather file-level suppressions before scanning, so
|
|
497
|
+
// a rule crash on an already-ignored file/rule pair can be disregarded.
|
|
498
|
+
const cssContents = new Map();
|
|
499
|
+
for (const filePath of cssFiles) {
|
|
500
|
+
cssContents.set(filePath, fs.readFileSync(filePath, 'utf8'));
|
|
501
|
+
}
|
|
502
|
+
const fileSuppressions = new Map();
|
|
503
|
+
for (const [filePath, content] of [...fileContents, ...cssContents]) {
|
|
504
|
+
const ids = getFileSuppressedRules(content);
|
|
505
|
+
if (ids.size > 0) fileSuppressions.set(toRelativeReportPath(filePath), ids);
|
|
506
|
+
}
|
|
507
|
+
const isIgnoredFor = (relativePath, ruleId) =>
|
|
508
|
+
Boolean(fileSuppressions.get(relativePath)?.has(ruleId)) ||
|
|
509
|
+
isPathIgnored(ignoreEntries, relativePath, ruleId);
|
|
510
|
+
|
|
431
511
|
// 落ちたルールは rule ID 単位で 1 回だけ warn する。catch が (rule × file) 単位
|
|
432
512
|
// なので、素直に warn すると 500 ファイルのリポジトリで同じ行が 500 回出て
|
|
433
513
|
// 本当の findings がスクロールバックから押し出される。
|
|
@@ -435,6 +515,11 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
435
515
|
// every file would bury the real findings.
|
|
436
516
|
const skippedRuleMap = new Map();
|
|
437
517
|
const onRuleError = ({ ruleId, filePath, message }) => {
|
|
518
|
+
// 除外済みのファイル × ルールで落ちても「未検査」にはしない。結果を出さない
|
|
519
|
+
// 箇所のせいで --strict / stop-hook が失敗するのは利用者の意図に反する。
|
|
520
|
+
// en: A crash on an ignored file/rule pair is not "unchecked" — its result
|
|
521
|
+
// would be hidden anyway, so it must not fail --strict / stop-hook.
|
|
522
|
+
if (isIgnoredFor(toRelativeReportPath(filePath), ruleId)) return;
|
|
438
523
|
const existing = skippedRuleMap.get(ruleId);
|
|
439
524
|
if (existing) {
|
|
440
525
|
existing.fileCount += 1;
|
|
@@ -451,9 +536,20 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
451
536
|
);
|
|
452
537
|
};
|
|
453
538
|
|
|
539
|
+
// 除外した件数を仕組みごとに数える。除外しすぎに気付けるよう summary に載せる(#102)。
|
|
540
|
+
// en: Count suppressed findings per mechanism so over-ignoring stays visible.
|
|
541
|
+
const ignored = { line: 0, file: 0, config: 0, token: 0 };
|
|
542
|
+
const onSuppress = () => {
|
|
543
|
+
ignored.line += 1;
|
|
544
|
+
};
|
|
545
|
+
|
|
454
546
|
const findings = [...fileContents.entries()]
|
|
455
547
|
.flatMap(([filePath, content]) =>
|
|
456
|
-
collectFindings(filePath, content, rules, {
|
|
548
|
+
collectFindings(filePath, content, rules, {
|
|
549
|
+
fileKind: RULE_TARGET.SOURCE,
|
|
550
|
+
onRuleError,
|
|
551
|
+
onSuppress,
|
|
552
|
+
})
|
|
457
553
|
)
|
|
458
554
|
.map((finding) => ({
|
|
459
555
|
...finding,
|
|
@@ -465,12 +561,12 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
465
561
|
// 対象は `targets` に css を含むルールだけなので、既存ルールの挙動は変わらない。
|
|
466
562
|
// en: Scan CSS too — variable references live there and fail silently. Only
|
|
467
563
|
// rules that opted into `css` run, so existing rules are unaffected.
|
|
468
|
-
for (const filePath of
|
|
469
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
564
|
+
for (const [filePath, content] of cssContents) {
|
|
470
565
|
findings.push(
|
|
471
566
|
...collectFindings(filePath, content, rules, {
|
|
472
567
|
fileKind: RULE_TARGET.CSS,
|
|
473
568
|
onRuleError,
|
|
569
|
+
onSuppress,
|
|
474
570
|
}).map((finding) => ({ ...finding, filePath: toRelativeReportPath(finding.filePath) }))
|
|
475
571
|
);
|
|
476
572
|
}
|
|
@@ -479,14 +575,99 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
479
575
|
findings.push(...fontFindings.map((f) => ({ ...f, filePath: toRelativeReportPath(f.filePath) })));
|
|
480
576
|
|
|
481
577
|
const cspFindings = collectNextjsCspFindings(process.cwd());
|
|
578
|
+
// CSP の finding は走査対象外の `next.config.*` に対して出るので、そのファイルの
|
|
579
|
+
// `sparkle-disable-file` もここで読んでおく。
|
|
580
|
+
// en: CSP findings point at `next.config.*`, usually outside the scanned
|
|
581
|
+
// targets, so read its file-level suppressions too.
|
|
582
|
+
for (const finding of cspFindings) {
|
|
583
|
+
const relativePath = toRelativeReportPath(finding.filePath);
|
|
584
|
+
if (fileSuppressions.has(relativePath)) continue;
|
|
585
|
+
let content;
|
|
586
|
+
try {
|
|
587
|
+
content = fs.readFileSync(finding.filePath, 'utf8');
|
|
588
|
+
} catch {
|
|
589
|
+
// 直前に CSP 検査で読めたファイル。読めなくなっていたら除外なしとして扱う。
|
|
590
|
+
// en: Just read by the CSP check; if it vanished, treat as no suppression.
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const ids = getFileSuppressedRules(content);
|
|
594
|
+
if (ids.size > 0) fileSuppressions.set(relativePath, ids);
|
|
595
|
+
}
|
|
482
596
|
findings.push(...cspFindings.map((f) => ({ ...f, filePath: toRelativeReportPath(f.filePath) })));
|
|
483
597
|
|
|
598
|
+
// ファイル単位のコメントと設定ファイルの除外は、組み込み検査(フォント @import /
|
|
599
|
+
// CSP)の finding も含めてここでまとめてかける。
|
|
600
|
+
// en: Apply file comments and config ignores to every finding, built-ins included.
|
|
601
|
+
const visibleFindings = findings.filter((finding) => {
|
|
602
|
+
if (fileSuppressions.get(finding.filePath)?.has(finding.id)) {
|
|
603
|
+
ignored.file += 1;
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
if (isPathIgnored(ignoreEntries, finding.filePath, finding.id)) {
|
|
607
|
+
ignored.config += 1;
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
// check.allowTokens(#104): shadcn-token の指摘のうち、プロジェクトが意図して使うと
|
|
611
|
+
// 宣言したクラスと完全一致するものは出さない。宣言していないものは従来どおり検出する。
|
|
612
|
+
// en: Hide shadcn-token findings whose class the project declared intentional;
|
|
613
|
+
// undeclared ones are still reported.
|
|
614
|
+
if (
|
|
615
|
+
finding.id === ALLOW_TOKENS_RULE_ID &&
|
|
616
|
+
isTokenAllowed(allowTokenEntries, finding.filePath, finding.snippet)
|
|
617
|
+
) {
|
|
618
|
+
ignored.token += 1;
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
return true;
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// 存在しないルール ID を除外指定していたら警告する。typo やプラグイン未導入だと
|
|
625
|
+
// 「除外したつもりが何も外していない」まま気付けない。
|
|
626
|
+
// en: Warn about ignore rule IDs no active rule has (typos, missing plugins).
|
|
627
|
+
const knownRuleIds = [...rules.map((rule) => rule.id), ...BUILTIN_REPORT_RULE_IDS];
|
|
628
|
+
const ignoreWarnings = [
|
|
629
|
+
...unknownIgnoreRuleIds(ignoreEntries, knownRuleIds).map((ruleId) => ({
|
|
630
|
+
source: 'config',
|
|
631
|
+
ruleId,
|
|
632
|
+
})),
|
|
633
|
+
];
|
|
634
|
+
const knownSet = new Set(knownRuleIds);
|
|
635
|
+
for (const [filePath, ids] of [...fileSuppressions].sort(([a], [b]) => a.localeCompare(b))) {
|
|
636
|
+
for (const ruleId of ids) {
|
|
637
|
+
if (!knownSet.has(ruleId)) ignoreWarnings.push({ source: 'file', ruleId, filePath });
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// allowTokens に書いたクラスが shadcn-token の検出対象でなければ警告する
|
|
641
|
+
// (typo や、対象外のクラスを書いて「許可したつもり」になるのを防ぐ)。
|
|
642
|
+
// en: Warn about allowTokens shadcn-token never detects (typos, other classes).
|
|
643
|
+
const declaredTokens = [...new Set(allowTokenEntries.flatMap((entry) => entry.tokens))].sort();
|
|
644
|
+
for (const token of declaredTokens) {
|
|
645
|
+
if (!isDetectedByAllowTokensRule(token, rules)) {
|
|
646
|
+
ignoreWarnings.push({ source: 'allowTokens', ruleId: ALLOW_TOKENS_RULE_ID, token });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
for (const warning of ignoreWarnings) {
|
|
650
|
+
if (warning.source === 'allowTokens') {
|
|
651
|
+
console.warn(
|
|
652
|
+
`⚠️ sparkle-design-cli: 設定ファイルの check.allowTokens に指定された "${warning.token}" は ${ALLOW_TOKENS_RULE_ID} の検出対象ではありません(typo か、variant・/50・! を付けたまま書いている可能性があります。クラス名だけで書いてください。allowTokens は ${ALLOW_TOKENS_RULE_ID} の指摘にだけ効きます)`
|
|
653
|
+
);
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const where =
|
|
657
|
+
warning.source === 'config'
|
|
658
|
+
? '設定ファイルの check.ignore'
|
|
659
|
+
: `${warning.filePath} の sparkle-disable-file`;
|
|
660
|
+
console.warn(
|
|
661
|
+
`⚠️ sparkle-design-cli: ${where} に指定されたルール "${warning.ruleId}" は有効なルールにありません(typo か、プラグインが入っていない可能性があります)`
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
484
665
|
// severity の高い順 → ファイル → 行 → ID。移行期は warning が大量に出るため、
|
|
485
666
|
// ファイル順だけで並べると本当に直すべき error が warning に埋もれる。
|
|
486
667
|
// 既存ルールはすべて error なので、error 同士の相対順は従来どおり保たれる。
|
|
487
668
|
// en: Sort by severity first so migration warnings can't bury errors. All
|
|
488
669
|
// pre-existing rules are errors, so their relative order is unchanged.
|
|
489
|
-
|
|
670
|
+
visibleFindings.sort((left, right) => {
|
|
490
671
|
const severityComparison = severityRank(left.severity) - severityRank(right.severity);
|
|
491
672
|
if (severityComparison !== 0) return severityComparison;
|
|
492
673
|
const fileComparison = left.filePath.localeCompare(right.filePath);
|
|
@@ -507,7 +688,14 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
507
688
|
return {
|
|
508
689
|
targets: resolvedTargets,
|
|
509
690
|
checkedFiles,
|
|
510
|
-
findings,
|
|
691
|
+
findings: visibleFindings,
|
|
692
|
+
// 除外(抑制コメント・設定ファイル)で出さなかった件数。除外しすぎの目安(#102)。
|
|
693
|
+
// en: Findings hidden by suppression comments or config ignores.
|
|
694
|
+
ignored: {
|
|
695
|
+
total: ignored.line + ignored.file + ignored.config + ignored.token,
|
|
696
|
+
...ignored,
|
|
697
|
+
},
|
|
698
|
+
ignoreWarnings,
|
|
511
699
|
// 「ルールが落ちて検査されなかった」ことをレポートに載せる。stderr の warn
|
|
512
700
|
// だけだと JSON を読む CI / AI には見えず、findings 0 件・passed true を
|
|
513
701
|
// 「クリーン」と誤読する。
|
|
@@ -518,15 +706,32 @@ function createCheckReport(targets = [], options = {}) {
|
|
|
518
706
|
};
|
|
519
707
|
}
|
|
520
708
|
|
|
709
|
+
function describeIgnored(ignored) {
|
|
710
|
+
if (!ignored || ignored.total === 0) return null;
|
|
711
|
+
const parts = [
|
|
712
|
+
['行コメント', ignored.line],
|
|
713
|
+
['sparkle-disable-file', ignored.file],
|
|
714
|
+
['check.ignore', ignored.config],
|
|
715
|
+
['check.allowTokens', ignored.token],
|
|
716
|
+
]
|
|
717
|
+
.filter(([, count]) => count > 0)
|
|
718
|
+
.map(([label, count]) => `${label} ${count}`);
|
|
719
|
+
return `${ignored.total} 件を除外しました(${parts.join(' / ')})`;
|
|
720
|
+
}
|
|
721
|
+
|
|
521
722
|
function printTextReport(report, options = {}) {
|
|
723
|
+
const ignoredLine = describeIgnored(report.ignored);
|
|
522
724
|
if (report.findings.length === 0) {
|
|
523
725
|
console.log('sparkle-design-cli check: no findings');
|
|
726
|
+
if (ignoredLine) console.log(` ${ignoredLine}`);
|
|
524
727
|
} else {
|
|
525
728
|
const counts = countBySeverity(report.findings);
|
|
526
729
|
const breakdown = SEVERITY_ORDER.filter((severity) => counts[severity] > 0)
|
|
527
730
|
.map((severity) => `${counts[severity]} ${severity}`)
|
|
528
731
|
.join(', ');
|
|
529
|
-
console.log(`sparkle-design-cli check: ${report.findings.length} finding(s) (${breakdown})
|
|
732
|
+
console.log(`sparkle-design-cli check: ${report.findings.length} finding(s) (${breakdown})`);
|
|
733
|
+
if (ignoredLine) console.log(` ${ignoredLine}`);
|
|
734
|
+
console.log('');
|
|
530
735
|
|
|
531
736
|
for (const finding of report.findings) {
|
|
532
737
|
const severity = coerceSeverity(finding.severity);
|
|
@@ -629,6 +834,9 @@ function printJsonReport(report, options = {}) {
|
|
|
629
834
|
// en: Rules that crashed and therefore did not run. Non-zero means the
|
|
630
835
|
// report is incomplete, so --strict fails even with zero findings.
|
|
631
836
|
skippedRuleCount: skippedCount,
|
|
837
|
+
// 抑制コメントや check.ignore で出さなかった件数(内訳はトップレベルの ignored)。
|
|
838
|
+
// en: Findings hidden by suppression; breakdown in top-level ignored.
|
|
839
|
+
ignoredFindingCount: report.ignored?.total ?? 0,
|
|
632
840
|
reminderCount: reminders.length,
|
|
633
841
|
// AI 向けの attention フラグ。reminder が 1 件でもあれば true になり、
|
|
634
842
|
// AI は最終 response で各 reminder ID を echo する必要がある。
|
|
@@ -671,7 +879,8 @@ export async function runCheck(targets = [], options = {}) {
|
|
|
671
879
|
...pluginReminders,
|
|
672
880
|
]);
|
|
673
881
|
|
|
674
|
-
const
|
|
882
|
+
const { ignore, allowTokens } = loadCheckConfig(options.configPath ?? null);
|
|
883
|
+
const report = createCheckReport(targets, { rules, baseReminders, ignore, allowTokens });
|
|
675
884
|
|
|
676
885
|
if (options.format === 'json') {
|
|
677
886
|
printJsonReport(report, options);
|
|
@@ -696,9 +905,12 @@ export {
|
|
|
696
905
|
BUILTIN_RULES as RULES,
|
|
697
906
|
blockingFindings,
|
|
698
907
|
coerceSeverity,
|
|
908
|
+
collectFiles,
|
|
699
909
|
collectFindings,
|
|
700
910
|
countBySeverity,
|
|
701
911
|
createCheckReport,
|
|
912
|
+
getFileSuppressedRules,
|
|
702
913
|
hasBlockingIssues,
|
|
914
|
+
isSuppressed,
|
|
703
915
|
BUILTIN_MANUAL_REVIEW_REMINDERS as MANUAL_REVIEW_REMINDERS,
|
|
704
916
|
};
|