chaffjs 0.0.1
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/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/chaff.js +4 -0
- package/dist/adapter-load.d.ts +9 -0
- package/dist/adapter-load.d.ts.map +1 -0
- package/dist/adapter-load.js +38 -0
- package/dist/adapter-load.js.map +1 -0
- package/dist/baseline.d.ts +23 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +41 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/load.d.ts +13 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +26 -0
- package/dist/config/load.js.map +1 -0
- package/dist/config/write.d.ts +12 -0
- package/dist/config/write.d.ts.map +1 -0
- package/dist/config/write.js +60 -0
- package/dist/config/write.js.map +1 -0
- package/dist/detect-language.d.ts +3 -0
- package/dist/detect-language.d.ts.map +1 -0
- package/dist/detect-language.js +26 -0
- package/dist/detect-language.js.map +1 -0
- package/dist/detect.d.ts +7 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +13 -0
- package/dist/detect.js.map +1 -0
- package/dist/detectors/count-per-section.d.ts +4 -0
- package/dist/detectors/count-per-section.d.ts.map +1 -0
- package/dist/detectors/count-per-section.js +13 -0
- package/dist/detectors/count-per-section.js.map +1 -0
- package/dist/detectors/heading-echo.d.ts +3 -0
- package/dist/detectors/heading-echo.d.ts.map +1 -0
- package/dist/detectors/heading-echo.js +36 -0
- package/dist/detectors/heading-echo.js.map +1 -0
- package/dist/detectors/index.d.ts +4 -0
- package/dist/detectors/index.d.ts.map +1 -0
- package/dist/detectors/index.js +16 -0
- package/dist/detectors/index.js.map +1 -0
- package/dist/detectors/phrase-match.d.ts +7 -0
- package/dist/detectors/phrase-match.d.ts.map +1 -0
- package/dist/detectors/phrase-match.js +38 -0
- package/dist/detectors/phrase-match.js.map +1 -0
- package/dist/detectors/repeated-head.d.ts +3 -0
- package/dist/detectors/repeated-head.d.ts.map +1 -0
- package/dist/detectors/repeated-head.js +21 -0
- package/dist/detectors/repeated-head.js.map +1 -0
- package/dist/detectors/sentence-length.d.ts +3 -0
- package/dist/detectors/sentence-length.d.ts.map +1 -0
- package/dist/detectors/sentence-length.js +19 -0
- package/dist/detectors/sentence-length.js.map +1 -0
- package/dist/detectors/sentence-rhythm.d.ts +3 -0
- package/dist/detectors/sentence-rhythm.d.ts.map +1 -0
- package/dist/detectors/sentence-rhythm.js +28 -0
- package/dist/detectors/sentence-rhythm.js.map +1 -0
- package/dist/document.d.ts +3 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +138 -0
- package/dist/document.js.map +1 -0
- package/dist/files.d.ts +12 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +30 -0
- package/dist/files.js.map +1 -0
- package/dist/genre.d.ts +8 -0
- package/dist/genre.d.ts.map +1 -0
- package/dist/genre.js +35 -0
- package/dist/genre.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +71 -0
- package/dist/init.js.map +1 -0
- package/dist/levels.d.ts +20 -0
- package/dist/levels.d.ts.map +1 -0
- package/dist/levels.js +24 -0
- package/dist/levels.js.map +1 -0
- package/dist/mask.d.ts +3 -0
- package/dist/mask.d.ts.map +1 -0
- package/dist/mask.js +26 -0
- package/dist/mask.js.map +1 -0
- package/dist/plugin.d.ts +116 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +4 -0
- package/dist/plugin.js.map +1 -0
- package/dist/position.d.ts +8 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +7 -0
- package/dist/position.js.map +1 -0
- package/dist/render/compact.d.ts +5 -0
- package/dist/render/compact.d.ts.map +1 -0
- package/dist/render/compact.js +15 -0
- package/dist/render/compact.js.map +1 -0
- package/dist/render/explain.d.ts +4 -0
- package/dist/render/explain.d.ts.map +1 -0
- package/dist/render/explain.js +29 -0
- package/dist/render/explain.js.map +1 -0
- package/dist/render/friendly.d.ts +4 -0
- package/dist/render/friendly.d.ts.map +1 -0
- package/dist/render/friendly.js +55 -0
- package/dist/render/friendly.js.map +1 -0
- package/dist/render/rules-json.d.ts +6 -0
- package/dist/render/rules-json.d.ts.map +1 -0
- package/dist/render/rules-json.js +39 -0
- package/dist/render/rules-json.js.map +1 -0
- package/dist/render/summary.d.ts +10 -0
- package/dist/render/summary.d.ts.map +1 -0
- package/dist/render/summary.js +18 -0
- package/dist/render/summary.js.map +1 -0
- package/dist/render/suppressions.d.ts +13 -0
- package/dist/render/suppressions.d.ts.map +1 -0
- package/dist/render/suppressions.js +50 -0
- package/dist/render/suppressions.js.map +1 -0
- package/dist/render/text.d.ts +6 -0
- package/dist/render/text.d.ts.map +1 -0
- package/dist/render/text.js +8 -0
- package/dist/render/text.js.map +1 -0
- package/dist/rule-load.d.ts +3 -0
- package/dist/rule-load.d.ts.map +1 -0
- package/dist/rule-load.js +68 -0
- package/dist/rule-load.js.map +1 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +47 -0
- package/dist/run.js.map +1 -0
- package/dist/stet.d.ts +19 -0
- package/dist/stet.d.ts.map +1 -0
- package/dist/stet.js +61 -0
- package/dist/stet.js.map +1 -0
- package/dist/watch.d.ts +12 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +40 -0
- package/dist/watch.js.map +1 -0
- package/package.json +63 -0
- package/rules/bold-density.yaml +28 -0
- package/rules/closing-cliche.yaml +30 -0
- package/rules/empty-intensifier.yaml +29 -0
- package/rules/heading-echo.yaml +30 -0
- package/rules/max-sentence-length.yaml +28 -0
- package/rules/padded-intro.yaml +30 -0
- package/rules/repeated-sentence-head.yaml +28 -0
- package/rules/sentence-rhythm.yaml +30 -0
- package/src/adapter-load.ts +31 -0
- package/src/baseline.ts +51 -0
- package/src/cli.ts +259 -0
- package/src/config/load.ts +38 -0
- package/src/config/write.ts +63 -0
- package/src/detect-language.ts +28 -0
- package/src/detect.ts +15 -0
- package/src/detectors/count-per-section.ts +15 -0
- package/src/detectors/heading-echo.ts +40 -0
- package/src/detectors/index.ts +17 -0
- package/src/detectors/phrase-match.ts +41 -0
- package/src/detectors/repeated-head.ts +29 -0
- package/src/detectors/sentence-length.ts +21 -0
- package/src/detectors/sentence-rhythm.ts +28 -0
- package/src/document.ts +154 -0
- package/src/files.ts +34 -0
- package/src/genre.ts +36 -0
- package/src/index.ts +4 -0
- package/src/init.ts +76 -0
- package/src/levels.ts +27 -0
- package/src/mask.ts +33 -0
- package/src/plugin.ts +124 -0
- package/src/position.ts +9 -0
- package/src/render/compact.ts +18 -0
- package/src/render/explain.ts +31 -0
- package/src/render/friendly.ts +62 -0
- package/src/render/rules-json.ts +46 -0
- package/src/render/summary.ts +22 -0
- package/src/render/suppressions.ts +57 -0
- package/src/render/text.ts +13 -0
- package/src/rule-load.ts +78 -0
- package/src/run.ts +61 -0
- package/src/stet.ts +79 -0
- package/src/watch.ts +52 -0
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// 言語アダプタと genre pack が依存してよい唯一の面。spec §6。
|
|
2
|
+
// ここに実装を置かない。型だけを置く。
|
|
3
|
+
|
|
4
|
+
export type Span = { readonly start: number; readonly end: number };
|
|
5
|
+
|
|
6
|
+
export type Sentence = { readonly span: Span; readonly text: string };
|
|
7
|
+
|
|
8
|
+
/** 品詞は Universal Dependencies の UPOS に統一する。アダプタ独自の体系を露出させない。 */
|
|
9
|
+
export type Token = { readonly span: Span; readonly surface: string; readonly pos: string; readonly lemma?: string };
|
|
10
|
+
|
|
11
|
+
export type Segmentation = {
|
|
12
|
+
readonly sentences: readonly Sentence[];
|
|
13
|
+
readonly words?: readonly Span[];
|
|
14
|
+
readonly tokens?: readonly Token[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type LengthUnit = "char" | "word";
|
|
18
|
+
|
|
19
|
+
/** L2 の語彙表。detector は共通で、これだけが言語別。spec §11。 */
|
|
20
|
+
export type LexiconEntry = { readonly pattern: string; readonly weight?: number | undefined };
|
|
21
|
+
|
|
22
|
+
export type Lexicon = readonly LexiconEntry[];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* rule は required な capability を宣言し、満たされなければ skip される。
|
|
26
|
+
* 日本語の品詞解析が 18MB の辞書を要することを、この一枚で吸収する。spec §16。
|
|
27
|
+
*/
|
|
28
|
+
export type AdapterCapabilities = {
|
|
29
|
+
readonly sentenceSplit: true;
|
|
30
|
+
readonly wordSplit: boolean;
|
|
31
|
+
readonly pos: boolean;
|
|
32
|
+
readonly lemma: boolean;
|
|
33
|
+
readonly lengthUnit: LengthUnit;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type LanguageAdapter = {
|
|
37
|
+
readonly kind: "language";
|
|
38
|
+
/** BCP 47 の primary subtag。"ja" / "en"。 */
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly apiVersion: 1;
|
|
41
|
+
readonly capabilities: AdapterCapabilities;
|
|
42
|
+
/** この言語である確からしさ。0..1。 */
|
|
43
|
+
readonly detect: (source: string) => number;
|
|
44
|
+
readonly segment: (text: string) => Segmentation;
|
|
45
|
+
/** L2 rule が word_list で引く。アダプタが自分の言語のぶんだけを持つ。 */
|
|
46
|
+
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ───────── 文書モデルと rule ─────────
|
|
50
|
+
|
|
51
|
+
export type Section = {
|
|
52
|
+
/** 見出しの深さ。見出しより前の導入部は 0。 */
|
|
53
|
+
readonly depth: number;
|
|
54
|
+
readonly heading: string;
|
|
55
|
+
readonly span: Span;
|
|
56
|
+
readonly sentences: readonly Sentence[];
|
|
57
|
+
/** この節に含まれる強調(Markdown の ** **)の数。 */
|
|
58
|
+
readonly strongCount: number;
|
|
59
|
+
/** 見出し直後の最初の文。heading-echo が見る。 */
|
|
60
|
+
readonly firstSentence: Sentence | undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* detector に渡る唯一の入り口。core が I/O を済ませてから呼ぶ。
|
|
65
|
+
* detector は純関数で、fs / network / clock に触れない。spec §6。
|
|
66
|
+
*/
|
|
67
|
+
export type ProseDocument = {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly source: string;
|
|
70
|
+
readonly language: string;
|
|
71
|
+
readonly lengthUnit: LengthUnit;
|
|
72
|
+
readonly sections: readonly Section[];
|
|
73
|
+
readonly sentences: readonly Sentence[];
|
|
74
|
+
/** アダプタが持つ語彙表。detector は言語を知らずにこれを引く。 */
|
|
75
|
+
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type Severity = "error" | "warning" | "info";
|
|
79
|
+
|
|
80
|
+
export type Finding = {
|
|
81
|
+
readonly rule: string;
|
|
82
|
+
readonly severity: Severity;
|
|
83
|
+
readonly line: number;
|
|
84
|
+
readonly column: number;
|
|
85
|
+
/** 引用して見せる範囲。非エンジニア向け出力が使う。 */
|
|
86
|
+
readonly quote: string;
|
|
87
|
+
/** message のプレースホルダに入れる値。 */
|
|
88
|
+
readonly values: Readonly<Record<string, string | number>>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** rule 定義が threshold を渡す。数値は 4 語から解決済み。 */
|
|
92
|
+
export type DetectorOptions = {
|
|
93
|
+
readonly limit: number;
|
|
94
|
+
/** L2 のみ。rule 定義の word_list から解決した語彙表。 */
|
|
95
|
+
readonly lexicon?: Lexicon | undefined;
|
|
96
|
+
/** 見る範囲。opening は冒頭 2 段落、closing は最後の節、whole は全体。 */
|
|
97
|
+
readonly where?: string | undefined;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type Detector = (doc: ProseDocument, options: DetectorOptions) => Finding[];
|
|
101
|
+
|
|
102
|
+
export type Localized = Readonly<Record<string, string>>;
|
|
103
|
+
|
|
104
|
+
export type Level = "strict" | "normal" | "relaxed" | "off";
|
|
105
|
+
|
|
106
|
+
export type LevelTable = Readonly<Partial<Record<Exclude<Level, "off">, number>>>;
|
|
107
|
+
|
|
108
|
+
export type RuleDefinition = {
|
|
109
|
+
readonly id: string;
|
|
110
|
+
readonly layer: "L1" | "L2" | "L3" | "L4";
|
|
111
|
+
readonly status: "experimental" | "stable" | "deprecated";
|
|
112
|
+
readonly name: Localized;
|
|
113
|
+
readonly why: Localized;
|
|
114
|
+
readonly how_to_fix: Localized;
|
|
115
|
+
readonly message: Localized;
|
|
116
|
+
/** 4 語と数値の対応。2 つ以上。未定義の段は normal に落ちる。spec §18.1。
|
|
117
|
+
* 言語別の閾値を持つ rule(max-sentence-length)は、読み込み時に言語で平坦化済み。 */
|
|
118
|
+
readonly levels: LevelTable;
|
|
119
|
+
readonly how_to_find: string;
|
|
120
|
+
readonly word_list: string | undefined;
|
|
121
|
+
readonly where: string | undefined;
|
|
122
|
+
readonly use_for: readonly string[];
|
|
123
|
+
readonly severity: Severity;
|
|
124
|
+
};
|
package/src/position.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type Place = { readonly line: number; readonly column: number };
|
|
2
|
+
|
|
3
|
+
/** 行頭のオフセット表。1 文書につき 1 度だけ作る。 */
|
|
4
|
+
export const lineStarts = (source: string): number[] => [...source].reduce<number[]>((acc, char, index) => (char === "\n" ? [...acc, index + 1] : acc), [0]);
|
|
5
|
+
|
|
6
|
+
export const placeOf = (starts: readonly number[], offset: number): Place => {
|
|
7
|
+
const index = starts.reduce((best, start, at) => (start <= offset ? at : best), 0);
|
|
8
|
+
return { line: index + 1, column: offset - (starts[index] ?? 0) + 1 };
|
|
9
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { RuleDefinition } from "../plugin.ts";
|
|
2
|
+
import type { RunResult } from "../run.ts";
|
|
3
|
+
import { messageOf } from "./text.ts";
|
|
4
|
+
|
|
5
|
+
/** エンジニア向け。1 件 2 行。 */
|
|
6
|
+
export const renderCompact = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
7
|
+
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
8
|
+
const lines = result.findings.flatMap((finding) => {
|
|
9
|
+
const rule = byId.get(finding.rule);
|
|
10
|
+
if (rule === undefined) return [];
|
|
11
|
+
const at = `${finding.line}:${finding.column}`.padEnd(8);
|
|
12
|
+
return [` ${at}${finding.severity.padEnd(8)}${messageOf(rule, finding, language)}`, ` ${" ".repeat(8)}${finding.rule}`];
|
|
13
|
+
});
|
|
14
|
+
const tail = [`${result.findings.length} findings`, result.skipped.length > 0 ? `${result.skipped.length} rules not run` : undefined].filter(
|
|
15
|
+
(part) => part !== undefined,
|
|
16
|
+
);
|
|
17
|
+
return ["", header, "", ...lines, "", tail.join(", "), ""].join("\n");
|
|
18
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { definedLevels, resolve } from "../levels.ts";
|
|
2
|
+
import { localized } from "./text.ts";
|
|
3
|
+
import type { Level, RuleDefinition } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
const levelLine = (rule: RuleDefinition, level: Level, current: Level): string => {
|
|
6
|
+
const mark = level === current ? "\u2192" : " ";
|
|
7
|
+
const name = level.padEnd(9);
|
|
8
|
+
return level === "off" ? ` ${mark} ${name}見ない` : ` ${mark} ${name}${resolve(rule, level).limit}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** rule の意図と根拠を読む。指摘に納得できないときの入口。 */
|
|
12
|
+
export const renderExplain = (rule: RuleDefinition, current: Level, language: string, unit: string): string => {
|
|
13
|
+
const experimental = rule.status === "experimental" ? [" このルールはまだ試験中で、既定では動きません(--experimental で動きます)。"] : [];
|
|
14
|
+
return [
|
|
15
|
+
"",
|
|
16
|
+
` ${localized(rule.name, language)} (${rule.id})`,
|
|
17
|
+
"",
|
|
18
|
+
` ${localized(rule.why, language)}`,
|
|
19
|
+
"",
|
|
20
|
+
` 直しかた: ${localized(rule.how_to_fix, language)}`,
|
|
21
|
+
"",
|
|
22
|
+
` 設定できる値(単位: ${unit}):`,
|
|
23
|
+
...definedLevels(rule).map((level) => levelLine(rule, level, current)),
|
|
24
|
+
"",
|
|
25
|
+
` いまは ${current} です。`,
|
|
26
|
+
...experimental,
|
|
27
|
+
"",
|
|
28
|
+
` 変える: npx chaff relax ${rule.id} --why "理由"`,
|
|
29
|
+
"",
|
|
30
|
+
].join("\n");
|
|
31
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Finding, RuleDefinition } from "../plugin.ts";
|
|
2
|
+
import type { RunResult } from "../run.ts";
|
|
3
|
+
import { MARK, localized, messageOf } from "./text.ts";
|
|
4
|
+
import { tally } from "./summary.ts";
|
|
5
|
+
|
|
6
|
+
const RULE = 60;
|
|
7
|
+
const QUOTE_LIMIT = 120;
|
|
8
|
+
|
|
9
|
+
const indent = (text: string, pad: string): string[] => text.split("\n").map((line) => `${pad}${line}`);
|
|
10
|
+
|
|
11
|
+
const quoteOf = (finding: Finding): string[] => {
|
|
12
|
+
const text = finding.quote.replace(/\s+/gu, " ").trim();
|
|
13
|
+
if (text.length === 0) return [];
|
|
14
|
+
return ["", ...indent(text.length > QUOTE_LIMIT ? `${text.slice(0, QUOTE_LIMIT)}…` : text, " ")];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 1 件につき 4 つを出す。どれが欠けても、書いた人は動けない。
|
|
19
|
+
* 引用 / 何が起きているか / なぜ問題か / どう直すか
|
|
20
|
+
* rule の id は「ゆるめる」コマンドの中にだけ出す。最初に読むのは name。
|
|
21
|
+
*/
|
|
22
|
+
const block = (finding: Finding, rule: RuleDefinition, language: string): string[] => [
|
|
23
|
+
"",
|
|
24
|
+
`─── ${finding.line} 行目 ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
|
|
25
|
+
...quoteOf(finding),
|
|
26
|
+
"",
|
|
27
|
+
` ${MARK[finding.severity] ?? "·"} ${localized(rule.name, language)}`,
|
|
28
|
+
"",
|
|
29
|
+
...indent(messageOf(rule, finding, language), " "),
|
|
30
|
+
...indent(localized(rule.why, language), " "),
|
|
31
|
+
"",
|
|
32
|
+
...indent(`→ ${localized(rule.how_to_fix, language)}`, " "),
|
|
33
|
+
"",
|
|
34
|
+
` このルールをゆるめる: npx chaff relax ${finding.rule}`,
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export const renderFriendly = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
|
|
39
|
+
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
40
|
+
const blocks = result.findings.flatMap((finding) => {
|
|
41
|
+
const rule = byId.get(finding.rule);
|
|
42
|
+
return rule === undefined ? [] : block(finding, rule, language);
|
|
43
|
+
});
|
|
44
|
+
const notes = [
|
|
45
|
+
"",
|
|
46
|
+
"─".repeat(RULE),
|
|
47
|
+
"",
|
|
48
|
+
` ${tally(result.findings)} すべて機械による判定です`,
|
|
49
|
+
" (同じ文章なら何度実行しても同じ結果になります)",
|
|
50
|
+
"",
|
|
51
|
+
" 文章は書き換えていません。直すのは書いた人です。",
|
|
52
|
+
];
|
|
53
|
+
const forced =
|
|
54
|
+
result.forcedExperimental.length > 0
|
|
55
|
+
? ["", ` 試験中の rule を ${result.forcedExperimental.length} 件、設定により有効にしています: ${result.forcedExperimental.join(", ")}`]
|
|
56
|
+
: [];
|
|
57
|
+
const skipped =
|
|
58
|
+
result.skipped.length > 0
|
|
59
|
+
? ["", ` ${result.skipped.length} 件の rule は動いていません:`, ...result.skipped.map((entry) => ` ${entry.rule}(${entry.why})`)]
|
|
60
|
+
: [];
|
|
61
|
+
return ["", header, ...blocks, ...notes, ...forced, ...skipped, ""].join("\n");
|
|
62
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { definedLevels, resolve } from "../levels.ts";
|
|
2
|
+
import type { Config } from "../config/load.ts";
|
|
3
|
+
import type { Level, RuleDefinition } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
const now = (rule: RuleDefinition, config: Config): Record<string, unknown> => {
|
|
6
|
+
const explicit = config.rules[rule.id];
|
|
7
|
+
if (explicit === "off") return { level: "off", why_off: "設定で止めている" };
|
|
8
|
+
if (explicit !== undefined) return { level: explicit, limit: resolve(rule, explicit).limit };
|
|
9
|
+
if (rule.status === "experimental" && !config.experimental) {
|
|
10
|
+
return { level: "off", why_off: "experimental な rule は既定で動かさない", turn_on_with: `npx chaff lint --experimental` };
|
|
11
|
+
}
|
|
12
|
+
return { level: "normal", limit: resolve(rule, "normal").limit };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** AI に設定を書かせるときの入口。推測せずに書けるだけの情報を 1 つに入れる。spec §19.3。 */
|
|
16
|
+
export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string =>
|
|
17
|
+
JSON.stringify(
|
|
18
|
+
{
|
|
19
|
+
schema_version: 1,
|
|
20
|
+
config_file: "chaff.yaml",
|
|
21
|
+
detected: { genre, language },
|
|
22
|
+
values_you_can_use: ["strict", "normal", "relaxed", "off"],
|
|
23
|
+
values_note: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
|
|
24
|
+
rules: rules.map((rule) => ({
|
|
25
|
+
id: rule.id,
|
|
26
|
+
layer: rule.layer,
|
|
27
|
+
status: rule.status,
|
|
28
|
+
name: rule.name,
|
|
29
|
+
why: rule.why,
|
|
30
|
+
how_to_fix: rule.how_to_fix,
|
|
31
|
+
use_for: rule.use_for,
|
|
32
|
+
levels: rule.levels,
|
|
33
|
+
levels_you_can_set: definedLevels(rule),
|
|
34
|
+
your_setting: config.rules[rule.id] === undefined ? null : { level: config.rules[rule.id], from: config.path },
|
|
35
|
+
now: now(rule, config),
|
|
36
|
+
})),
|
|
37
|
+
how_to_change: {
|
|
38
|
+
by_command: ['npx chaff relax <rule-id> --why "<理由>"', 'npx chaff strict <rule-id> --why "<理由>"', 'npx chaff off <rule-id> --why "<理由>"'],
|
|
39
|
+
by_file: "chaff.yaml の rules に <rule-id>: <level> を足す。既定のままのものは書かない。",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
null,
|
|
43
|
+
2,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const levelsForCli = (): readonly Level[] => ["strict", "normal", "relaxed", "off"];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Finding } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
export type FileOutcome = { readonly path: string; readonly findings: readonly Finding[]; readonly notRun: number };
|
|
4
|
+
|
|
5
|
+
const count = (findings: readonly Finding[], severity: string): number => findings.filter((finding) => finding.severity === severity).length;
|
|
6
|
+
|
|
7
|
+
export const tally = (findings: readonly Finding[]): string => {
|
|
8
|
+
const parts = [
|
|
9
|
+
count(findings, "error") > 0 ? `エラー ${count(findings, "error")} 件` : undefined,
|
|
10
|
+
count(findings, "warning") > 0 ? `注意 ${count(findings, "warning")} 件` : undefined,
|
|
11
|
+
count(findings, "info") > 0 ? `参考 ${count(findings, "info")} 件` : undefined,
|
|
12
|
+
].filter((part) => part !== undefined);
|
|
13
|
+
return parts.length === 0 ? "指摘はありません" : parts.join("、");
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** 複数ファイルを見たときの締め。1 ファイルのときは出さない。 */
|
|
17
|
+
export const renderSummary = (outcomes: readonly FileOutcome[]): string[] => {
|
|
18
|
+
if (outcomes.length < 2) return [];
|
|
19
|
+
const all = outcomes.flatMap((outcome) => outcome.findings);
|
|
20
|
+
const touched = outcomes.filter((outcome) => outcome.findings.length > 0).length;
|
|
21
|
+
return ["", "═".repeat(60), "", ` ${outcomes.length} ファイルを見て、${touched} ファイルに指摘がありました`, ` ${tally(all)}`, ""];
|
|
22
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Suppressed, Suppression } from "../stet.ts";
|
|
2
|
+
|
|
3
|
+
export type PerFile = { readonly path: string; readonly suppressed: readonly Suppressed[]; readonly reasonless: readonly Suppression[] };
|
|
4
|
+
|
|
5
|
+
/** これを超えたら、その rule は (b) ではなく (c) にすべきというサイン。 */
|
|
6
|
+
const NUDGE_AT = 5;
|
|
7
|
+
|
|
8
|
+
type Group = { readonly rule: string; readonly count: number; readonly files: readonly string[]; readonly reasons: readonly string[] };
|
|
9
|
+
|
|
10
|
+
const groupByRule = (files: readonly PerFile[]): Group[] => {
|
|
11
|
+
const rows = files.flatMap((file) => file.suppressed.map((entry) => ({ path: file.path, rule: entry.finding.rule, reason: entry.suppression.reason })));
|
|
12
|
+
const ids = [...new Set(rows.map((row) => row.rule))];
|
|
13
|
+
return ids
|
|
14
|
+
.map((rule) => {
|
|
15
|
+
const mine = rows.filter((row) => row.rule === rule);
|
|
16
|
+
return {
|
|
17
|
+
rule,
|
|
18
|
+
count: mine.length,
|
|
19
|
+
files: [...new Set(mine.map((row) => row.path))],
|
|
20
|
+
reasons: [...new Set(mine.flatMap((row) => (row.reason === undefined ? [] : [row.reason])))],
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
.sort((left, right) => right.count - left.count);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const groupLines = (group: Group): string[] => {
|
|
27
|
+
const nudge = group.count >= NUDGE_AT ? " ← 設定の見直しを検討してください" : "";
|
|
28
|
+
const shown = group.files.slice(0, 3).join(", ");
|
|
29
|
+
const rest = group.files.length > 3 ? ` ほか ${group.files.length - 3} ファイル` : "";
|
|
30
|
+
return [
|
|
31
|
+
"",
|
|
32
|
+
` ${group.rule.padEnd(26)}${String(group.count).padStart(3)} 件${nudge}`,
|
|
33
|
+
` ${shown}${rest}`,
|
|
34
|
+
...(group.count >= NUDGE_AT && group.reasons.length > 0
|
|
35
|
+
? [` 理由: ${group.reasons.slice(0, 2).join(" / ")}`, ` ルールごとゆるめる: npx chaff relax ${group.rule} --why "..."`]
|
|
36
|
+
: []),
|
|
37
|
+
];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 抑制が溜まったことに気づけないと、規範と現実の乖離が静かに進む。
|
|
42
|
+
* 同じ rule を何度も黙らせているなら、それは (b) ではなく (c) のサイン。
|
|
43
|
+
* workflow spec §7.3。
|
|
44
|
+
*/
|
|
45
|
+
export const renderSuppressions = (files: readonly PerFile[]): string => {
|
|
46
|
+
const groups = groupByRule(files);
|
|
47
|
+
const total = groups.reduce((sum, group) => sum + group.count, 0);
|
|
48
|
+
if (total === 0) return "\n 抑制されている指摘はありません。\n";
|
|
49
|
+
const reasonless = files.flatMap((file) => file.reasonless.map(() => file.path));
|
|
50
|
+
return [
|
|
51
|
+
"",
|
|
52
|
+
` 抑制されている指摘: ${total} 件`,
|
|
53
|
+
...groups.flatMap(groupLines),
|
|
54
|
+
...(reasonless.length > 0 ? ["", ` 理由が書かれていない抑制: ${reasonless.length} 件`, ` ${[...new Set(reasonless)].join(", ")}`] : []),
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Finding, Localized, RuleDefinition } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
export const localized = (field: Localized, language: string): string => field[language] ?? field["en"] ?? Object.values(field)[0] ?? "";
|
|
4
|
+
|
|
5
|
+
export const fill = (template: string, values: Readonly<Record<string, string | number>>): string =>
|
|
6
|
+
template.replace(/\{(\w+)\}/gu, (whole, key: string) => {
|
|
7
|
+
const value = values[key];
|
|
8
|
+
return value === undefined ? whole : String(value);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const messageOf = (rule: RuleDefinition, finding: Finding, language: string): string => fill(localized(rule.message, language), finding.values);
|
|
12
|
+
|
|
13
|
+
export const MARK: Readonly<Record<string, string>> = { error: "✖", warning: "⚠", info: "·" };
|
package/src/rule-load.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import type { LevelTable, RuleDefinition, Severity } from "./plugin.ts";
|
|
6
|
+
|
|
7
|
+
const RULES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "rules");
|
|
8
|
+
|
|
9
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
|
|
11
|
+
const isLocalized = (value: unknown): value is Record<string, string> => isRecord(value) && Object.values(value).every((entry) => typeof entry === "string");
|
|
12
|
+
|
|
13
|
+
const isLevelTable = (value: unknown): value is LevelTable =>
|
|
14
|
+
isRecord(value) && Object.entries(value).every(([key, entry]) => ["strict", "normal", "relaxed"].includes(key) && typeof entry === "number");
|
|
15
|
+
|
|
16
|
+
/** 言語別の levels を、その言語のぶんだけに平坦化する。単位が言語で違う rule のため。 */
|
|
17
|
+
const flattenLevels = (raw: unknown, language: string): LevelTable | undefined => {
|
|
18
|
+
if (isLevelTable(raw)) return raw;
|
|
19
|
+
if (!isRecord(raw)) return undefined;
|
|
20
|
+
const forLanguage = raw[language] ?? raw["default"];
|
|
21
|
+
return isLevelTable(forLanguage) ? forLanguage : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const SEVERITIES: readonly Severity[] = ["error", "warning", "info"];
|
|
25
|
+
const isSeverity = (value: unknown): value is Severity => SEVERITIES.some((entry) => entry === value);
|
|
26
|
+
|
|
27
|
+
const LAYERS = ["L1", "L2", "L3", "L4"] as const;
|
|
28
|
+
const STATUSES = ["experimental", "stable", "deprecated"] as const;
|
|
29
|
+
|
|
30
|
+
const pick = <T>(value: unknown, allowed: readonly T[]): T | undefined => allowed.find((entry) => entry === value);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 必須フィールドが欠けた rule は読み込まない。spec §18.2。
|
|
34
|
+
* message だけでは、非エンジニアは何が悪いのか分からない。why と how_to_fix を必須にする。
|
|
35
|
+
*/
|
|
36
|
+
const REQUIRED_TEXT = ["name", "why", "how_to_fix", "message"] as const;
|
|
37
|
+
|
|
38
|
+
const missingFields = (raw: Record<string, unknown>, levels: LevelTable | undefined): string[] => [
|
|
39
|
+
...(typeof raw["id"] === "string" ? [] : ["id"]),
|
|
40
|
+
...(pick(raw["layer"], LAYERS) === undefined ? ["layer"] : []),
|
|
41
|
+
...(pick(raw["status"], STATUSES) === undefined ? ["status"] : []),
|
|
42
|
+
...REQUIRED_TEXT.filter((field) => !isLocalized(raw[field])),
|
|
43
|
+
...(levels?.normal === undefined ? ["levels.normal"] : []),
|
|
44
|
+
...(typeof raw["how_to_find"] === "string" ? [] : ["how_to_find"]),
|
|
45
|
+
...(Array.isArray(raw["use_for"]) ? [] : ["use_for"]),
|
|
46
|
+
...(isSeverity(raw["severity"]) ? [] : ["severity"]),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const localizedOf = (value: unknown): Record<string, string> => (isLocalized(value) ? value : {});
|
|
50
|
+
|
|
51
|
+
const toRule = (raw: unknown, language: string, file: string): RuleDefinition => {
|
|
52
|
+
if (!isRecord(raw)) throw new Error(`${file}: rule は object であること`);
|
|
53
|
+
const levels = flattenLevels(raw["levels"], language);
|
|
54
|
+
const missing = missingFields(raw, levels);
|
|
55
|
+
if (missing.length > 0) throw new Error(`${file}: 必須フィールドがありません: ${missing.join(", ")}`);
|
|
56
|
+
if (levels === undefined) throw new Error(`${file}: levels を解決できません`);
|
|
57
|
+
return {
|
|
58
|
+
id: String(raw["id"]),
|
|
59
|
+
layer: pick(raw["layer"], LAYERS) ?? "L1",
|
|
60
|
+
status: pick(raw["status"], STATUSES) ?? "experimental",
|
|
61
|
+
name: localizedOf(raw["name"]),
|
|
62
|
+
why: localizedOf(raw["why"]),
|
|
63
|
+
how_to_fix: localizedOf(raw["how_to_fix"]),
|
|
64
|
+
message: localizedOf(raw["message"]),
|
|
65
|
+
levels,
|
|
66
|
+
how_to_find: String(raw["how_to_find"]),
|
|
67
|
+
word_list: typeof raw["word_list"] === "string" ? raw["word_list"] : undefined,
|
|
68
|
+
where: typeof raw["where"] === "string" ? raw["where"] : undefined,
|
|
69
|
+
use_for: Array.isArray(raw["use_for"]) ? raw["use_for"].map((entry) => String(entry)) : [],
|
|
70
|
+
severity: isSeverity(raw["severity"]) ? raw["severity"] : "warning",
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const loadRules = (language: string, dir: string = RULES_DIR): RuleDefinition[] =>
|
|
75
|
+
readdirSync(dir)
|
|
76
|
+
.filter((file) => file.endsWith(".yaml"))
|
|
77
|
+
.sort((left, right) => left.localeCompare(right, "en"))
|
|
78
|
+
.map((file) => toRule(parse(readFileSync(join(dir, file), "utf8")), language, file));
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DETECTORS } from "./detectors/index.ts";
|
|
2
|
+
import { resolve } from "./levels.ts";
|
|
3
|
+
import type { Finding, Level, ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
|
+
import { lineStarts, placeOf } from "./position.ts";
|
|
5
|
+
|
|
6
|
+
export type Skipped = { readonly rule: string; readonly why: string };
|
|
7
|
+
|
|
8
|
+
export type RunResult = {
|
|
9
|
+
readonly findings: readonly Finding[];
|
|
10
|
+
readonly skipped: readonly Skipped[];
|
|
11
|
+
/** 設定で明示的に有効にした experimental な rule。実行ごとに一度報告する。spec §18.4。 */
|
|
12
|
+
readonly forcedExperimental: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Settings = Readonly<Record<string, Level>>;
|
|
16
|
+
|
|
17
|
+
const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolean): Level => {
|
|
18
|
+
const explicit = settings[rule.id];
|
|
19
|
+
// 明示設定は status の既定に勝つ。名指しで有効にしたものを黙って無効にしない。
|
|
20
|
+
if (explicit !== undefined) return explicit;
|
|
21
|
+
if (rule.status === "experimental" && !experimental) return "off";
|
|
22
|
+
return "normal";
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
26
|
+
const offset = finding.values["offset"];
|
|
27
|
+
const at = placeOf(starts, typeof offset === "number" ? offset : 0);
|
|
28
|
+
return { ...finding, line: at.line, column: at.column };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], settings: Settings, experimental: boolean, genre: string): RunResult => {
|
|
32
|
+
const starts = lineStarts(doc.source);
|
|
33
|
+
const applicable = rules.filter((rule) => rule.use_for.some((target) => genre.startsWith(target)));
|
|
34
|
+
const forced = applicable
|
|
35
|
+
.filter((rule) => rule.status === "experimental" && settings[rule.id] !== undefined && settings[rule.id] !== "off")
|
|
36
|
+
.map((rule) => rule.id);
|
|
37
|
+
const outcome = applicable.reduce<{ findings: Finding[]; skipped: Skipped[] }>(
|
|
38
|
+
(acc, rule) => {
|
|
39
|
+
const level = levelFor(rule, settings, experimental);
|
|
40
|
+
if (level === "off") {
|
|
41
|
+
const why = rule.status === "experimental" && settings[rule.id] === undefined ? "まだ試験中のため" : "設定で止めているため";
|
|
42
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
43
|
+
}
|
|
44
|
+
const detector = DETECTORS[rule.how_to_find];
|
|
45
|
+
if (detector === undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `検出器 ${rule.how_to_find} がないため` }] };
|
|
46
|
+
const options = {
|
|
47
|
+
limit: resolve(rule, level).limit,
|
|
48
|
+
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
49
|
+
where: rule.where,
|
|
50
|
+
};
|
|
51
|
+
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
52
|
+
if (rule.word_list !== undefined && options.lexicon === undefined) {
|
|
53
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `${doc.language} の語彙表 ${rule.word_list} が無いため` }] };
|
|
54
|
+
}
|
|
55
|
+
const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
|
|
56
|
+
return { findings: [...acc.findings, ...found], skipped: acc.skipped };
|
|
57
|
+
},
|
|
58
|
+
{ findings: [], skipped: [] },
|
|
59
|
+
);
|
|
60
|
+
return { ...outcome, findings: [...outcome.findings].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
61
|
+
};
|
package/src/stet.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Finding, Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
export type Suppression = {
|
|
4
|
+
readonly rules: readonly string[];
|
|
5
|
+
readonly reason: string | undefined;
|
|
6
|
+
readonly line: number;
|
|
7
|
+
readonly scope: "next" | "section" | "file";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 校正記号 stet(ラテン語で「そのままにせよ」)。
|
|
12
|
+
* 指摘は一般には正しいが、この箇所は意図的だ、と書く手段。
|
|
13
|
+
* これが無いと、残された手は「ルールごと切る」しかなくなる。workflow spec §7.1。
|
|
14
|
+
*
|
|
15
|
+
* 中身の切り分けは正規表現でやらない。入れ子の量指定子はバックトラックが超線形になり、
|
|
16
|
+
* 長い HTML コメントを 1 つ置かれるだけで検査が止まる。ここは ">" を含まない塊として
|
|
17
|
+
* 取り出し、あとはコードで割る。
|
|
18
|
+
*/
|
|
19
|
+
const PATTERN = /<!--\s*(stet|stet-section|stet-file)\s*:([^>]*)-->/gu;
|
|
20
|
+
|
|
21
|
+
const SCOPE: Readonly<Record<string, Suppression["scope"]>> = { stet: "next", "stet-section": "section", "stet-file": "file" };
|
|
22
|
+
|
|
23
|
+
/** 理由は em dash のあと。書かなくてもよいが、書かないと chaff suppressions に並ぶ。 */
|
|
24
|
+
const REASON_MARK = "\u2014";
|
|
25
|
+
|
|
26
|
+
const RULE_ID = /^[a-z][a-z0-9-]*$/u;
|
|
27
|
+
|
|
28
|
+
const lineOf = (source: string, offset: number): number => source.slice(0, offset).split("\n").length;
|
|
29
|
+
|
|
30
|
+
const splitBody = (body: string): { rules: string[]; reason: string | undefined } => {
|
|
31
|
+
const at = body.indexOf(REASON_MARK);
|
|
32
|
+
const head = at === -1 ? body : body.slice(0, at);
|
|
33
|
+
// 残っているのはコメント終端の "--" だけ。正規表現で任意個の "-" を剥がすと、
|
|
34
|
+
// 長いダッシュ列でバックトラックする(そして著者が書いたダッシュまで消す)。
|
|
35
|
+
const raw = at === -1 ? "" : body.slice(at + REASON_MARK.length);
|
|
36
|
+
const tail = (raw.endsWith("--") ? raw.slice(0, -2) : raw).trim();
|
|
37
|
+
return {
|
|
38
|
+
rules: head
|
|
39
|
+
.split(",")
|
|
40
|
+
.map((rule) => rule.trim())
|
|
41
|
+
.filter((rule) => RULE_ID.test(rule)),
|
|
42
|
+
reason: tail.length === 0 ? undefined : tail,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const parseSuppressions = (source: string): Suppression[] =>
|
|
47
|
+
[...source.matchAll(PATTERN)].map((match) => ({
|
|
48
|
+
...splitBody(match[2] ?? ""),
|
|
49
|
+
line: lineOf(source, match.index),
|
|
50
|
+
scope: SCOPE[match[1] ?? "stet"] ?? "next",
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
/** stet が効く範囲。next は直後の塊、section は次の見出しまで、file は全体。 */
|
|
54
|
+
const NEXT_LINES = 6;
|
|
55
|
+
|
|
56
|
+
const covers = (suppression: Suppression, finding: Finding, sectionEnds: readonly number[], lastLine: number): boolean => {
|
|
57
|
+
if (!suppression.rules.includes(finding.rule)) return false;
|
|
58
|
+
if (suppression.scope === "file") return true;
|
|
59
|
+
// 抑制は「この先の箇所」に対して書く。遡って効くと、意図せず黙る範囲が広がる。
|
|
60
|
+
if (finding.line < suppression.line) return false;
|
|
61
|
+
if (suppression.scope === "next") return finding.line - suppression.line <= NEXT_LINES;
|
|
62
|
+
return finding.line <= (sectionEnds.find((line) => line > suppression.line) ?? lastLine);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type Suppressed = { readonly finding: Finding; readonly suppression: Suppression };
|
|
66
|
+
|
|
67
|
+
export type Applied = { readonly kept: readonly Finding[]; readonly suppressed: readonly Suppressed[]; readonly unusedReasonless: readonly Suppression[] };
|
|
68
|
+
|
|
69
|
+
export const applySuppressions = (source: string, findings: readonly Finding[], sections: readonly Span[]): Applied => {
|
|
70
|
+
const suppressions = parseSuppressions(source);
|
|
71
|
+
const sectionEnds = sections.map((span) => lineOf(source, span.end));
|
|
72
|
+
const lastLine = source.split("\n").length;
|
|
73
|
+
const matched = findings.map((finding) => ({ finding, suppression: suppressions.find((entry) => covers(entry, finding, sectionEnds, lastLine)) }));
|
|
74
|
+
return {
|
|
75
|
+
kept: matched.filter((entry) => entry.suppression === undefined).map((entry) => entry.finding),
|
|
76
|
+
suppressed: matched.flatMap((entry) => (entry.suppression === undefined ? [] : [{ finding: entry.finding, suppression: entry.suppression }])),
|
|
77
|
+
unusedReasonless: suppressions.filter((entry) => entry.reason === undefined),
|
|
78
|
+
};
|
|
79
|
+
};
|