chaffjs 0.4.0 → 0.5.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/dist/adapter-load.d.ts +5 -0
- package/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +39 -3
- package/dist/adapter-load.js.map +1 -1
- package/dist/backends/anthropic.d.ts +2 -2
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +1 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/openai.d.ts +2 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/types.d.ts +0 -1
- package/dist/backends/types.d.ts.map +1 -1
- package/dist/backends/types.js +1 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +22 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/load.d.ts +0 -3
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +1 -1
- package/dist/config/load.js.map +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +4 -2
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +25 -2
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -2
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lexicon.d.ts +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +41 -0
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/signals.d.ts +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +42 -10
- package/dist/detectors/signals.js.map +1 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +8 -1
- package/dist/document.js.map +1 -1
- package/dist/eval.d.ts.map +1 -1
- package/dist/eval.js +15 -1
- package/dist/eval.js.map +1 -1
- package/dist/files.d.ts +0 -10
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +1 -1
- package/dist/files.js.map +1 -1
- package/dist/levels.d.ts +0 -1
- package/dist/levels.d.ts.map +1 -1
- package/dist/levels.js +1 -1
- package/dist/levels.js.map +1 -1
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +8 -1
- package/dist/mask.js.map +1 -1
- package/dist/measure.d.ts +0 -1
- package/dist/measure.d.ts.map +1 -1
- package/dist/measure.js +1 -1
- package/dist/measure.js.map +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/position.d.ts +6 -1
- package/dist/position.d.ts.map +1 -1
- package/dist/position.js +12 -2
- package/dist/position.js.map +1 -1
- package/dist/render/rules-json.d.ts +1 -2
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +0 -1
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/sarif.d.ts +10 -0
- package/dist/render/sarif.d.ts.map +1 -0
- package/dist/render/sarif.js +65 -0
- package/dist/render/sarif.js.map +1 -0
- package/dist/render/semantic.d.ts +0 -1
- package/dist/render/semantic.d.ts.map +1 -1
- package/dist/render/semantic.js +1 -1
- package/dist/render/semantic.js.map +1 -1
- package/dist/render/text.d.ts +0 -1
- package/dist/render/text.d.ts.map +1 -1
- package/dist/render/text.js +1 -1
- package/dist/render/text.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +15 -2
- package/dist/rule-load.js.map +1 -1
- package/dist/run-semantic.d.ts.map +1 -1
- package/dist/run-semantic.js +15 -4
- package/dist/run-semantic.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +44 -4
- package/dist/run.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -3
- package/rules/ai-generated-composite.yaml +36 -0
- package/rules/concrete-evidence-density.yaml +1 -1
- package/rules/contraction-consistency.yaml +28 -0
- package/rules/max-paragraph-length.yaml +1 -1
- package/rules/ngram-repetition.yaml +1 -1
- package/rules/no-doubled-joshi.yaml +1 -1
- package/rules/no-em-dash.yaml +32 -0
- package/rules/no-nakaguro-parallel.yaml +1 -1
- package/rules/preamble-length.yaml +1 -1
- package/rules/taigen-dome-in-prose.yaml +1 -1
- package/src/adapter-load.ts +40 -2
- package/src/backends/anthropic.ts +2 -2
- package/src/backends/openai.ts +1 -1
- package/src/backends/types.ts +1 -1
- package/src/cli.ts +33 -3
- package/src/config/load.ts +1 -1
- package/src/detectors/en-shape.ts +4 -2
- package/src/detectors/heading-echo.ts +29 -3
- package/src/detectors/index.ts +4 -2
- package/src/detectors/lexicon.ts +41 -0
- package/src/detectors/signals.ts +43 -10
- package/src/document.ts +11 -1
- package/src/eval.ts +13 -1
- package/src/files.ts +1 -1
- package/src/levels.ts +1 -1
- package/src/mask.ts +8 -1
- package/src/measure.ts +1 -1
- package/src/plugin.ts +7 -1
- package/src/position.ts +11 -2
- package/src/render/rules-json.ts +1 -3
- package/src/render/sarif.ts +103 -0
- package/src/render/semantic.ts +1 -1
- package/src/render/text.ts +1 -1
- package/src/rule-load.ts +14 -2
- package/src/run-semantic.ts +14 -4
- package/src/run.ts +42 -3
- package/src/version.ts +14 -0
package/src/plugin.ts
CHANGED
|
@@ -32,7 +32,11 @@ export type Segmentation = {
|
|
|
32
32
|
export type LengthUnit = "char" | "word";
|
|
33
33
|
|
|
34
34
|
/** L2 の語彙表。detector は共通で、これだけが言語別。spec §11。 */
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `instead_of` は「同じことを言う別の書きかた」。文体の一貫性を見る rule が使う。
|
|
37
|
+
* 2 つの書きかたのどちらが正しいかは決めず、**1 つの文書で混ざっていないか**だけを見る。
|
|
38
|
+
*/
|
|
39
|
+
export type LexiconEntry = { readonly pattern: string; readonly weight?: number | undefined; readonly instead_of?: string | undefined };
|
|
36
40
|
|
|
37
41
|
export type Lexicon = readonly LexiconEntry[];
|
|
38
42
|
|
|
@@ -163,6 +167,8 @@ export type RuleDefinition = {
|
|
|
163
167
|
readonly where: string | undefined;
|
|
164
168
|
/** adapter に要る capability。"pos" / "lemma"。満たさなければ動かさない。spec §16。 */
|
|
165
169
|
readonly requires: readonly string[];
|
|
170
|
+
/** 複合シグナル。ここに並べた rule のうち何本が出たかを見る。spec §20.2。 */
|
|
171
|
+
readonly from: readonly string[];
|
|
166
172
|
/** 動かす言語。未指定は全言語。「ですます調」のように言語に固有の rule が使う。 */
|
|
167
173
|
readonly languages: readonly string[] | undefined;
|
|
168
174
|
readonly use_for: readonly string[];
|
package/src/position.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export type Place = { readonly line: number; readonly column: number };
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* 行頭のオフセット表。1 文書につき 1 度だけ作る。
|
|
5
|
+
*
|
|
6
|
+
* 数えるのは **UTF-16 単位**。`[...source]` はコードポイントで数えるので、
|
|
7
|
+
* 絵文字より後ろの行がすべて 1 ずれる。指摘が持つオフセットは UTF-16 なので、そちらに合わせる。
|
|
8
|
+
*/
|
|
9
|
+
export const lineStarts = (source: string): number[] => {
|
|
10
|
+
const starts = [0];
|
|
11
|
+
for (let at = source.indexOf("\n"); at !== -1; at = source.indexOf("\n", at + 1)) starts.push(at + 1);
|
|
12
|
+
return starts;
|
|
13
|
+
};
|
|
5
14
|
|
|
6
15
|
export const placeOf = (starts: readonly number[], offset: number): Place => {
|
|
7
16
|
const index = starts.reduce((best, start, at) => (start <= offset ? at : best), 0);
|
package/src/render/rules-json.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { definedLevels, resolve } from "../levels.ts";
|
|
2
2
|
import type { Config } from "../config/load.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type { RuleDefinition } from "../plugin.ts";
|
|
4
4
|
|
|
5
5
|
const now = (rule: RuleDefinition, config: Config, genre: string): Record<string, unknown> => {
|
|
6
6
|
const explicit = config.rules[rule.id];
|
|
@@ -53,5 +53,3 @@ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, lang
|
|
|
53
53
|
null,
|
|
54
54
|
2,
|
|
55
55
|
);
|
|
56
|
-
|
|
57
|
-
export const levelsForCli = (): readonly Level[] => ["strict", "normal", "relaxed", "off"];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { localized, messageOf } from "./text.ts";
|
|
2
|
+
import type { Finding, RuleDefinition, Severity } from "../plugin.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 指摘を SARIF 2.1.0 で出す。GitHub の code scanning に上げるため。
|
|
6
|
+
*
|
|
7
|
+
* 上げると Security タブに並ぶだけでなく、**PR の変更行に直接出る**。
|
|
8
|
+
* ログの中の指摘は読みに行かないと見えないが、行の上の指摘は書いた本人の目の前にある。
|
|
9
|
+
* chaff は文章を直す人のための道具なので、ここが効く。
|
|
10
|
+
*/
|
|
11
|
+
const SCHEMA = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
12
|
+
const INFORMATION_URI = "https://www.npmjs.com/package/chaffjs";
|
|
13
|
+
|
|
14
|
+
/** SARIF に info は無い。上げるときに弾かれるので、note に写す。 */
|
|
15
|
+
type Level = "error" | "warning" | "note";
|
|
16
|
+
|
|
17
|
+
const levelOf = (severity: Severity): Level => (severity === "info" ? "note" : severity);
|
|
18
|
+
|
|
19
|
+
type SarifRule = {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly shortDescription: { readonly text: string };
|
|
23
|
+
readonly fullDescription: { readonly text: string };
|
|
24
|
+
readonly help: { readonly text: string };
|
|
25
|
+
readonly properties: { readonly tags: readonly string[] };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type SarifResult = {
|
|
29
|
+
readonly ruleId: string;
|
|
30
|
+
readonly level: Level;
|
|
31
|
+
readonly message: { readonly text: string };
|
|
32
|
+
readonly locations: readonly {
|
|
33
|
+
readonly physicalLocation: {
|
|
34
|
+
readonly artifactLocation: { readonly uri: string };
|
|
35
|
+
readonly region: { readonly startLine: number; readonly startColumn: number };
|
|
36
|
+
};
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `chaff/<rule>` と名前空間を切る。同じ PR に上がる他のツールの rule id と衝突させない。 */
|
|
41
|
+
const ruleIdOf = (rule: string): string => `chaff/${rule}`;
|
|
42
|
+
|
|
43
|
+
/** 指摘 1 件と、それを描くのに要るもの。言語はファイルごとに違う。 */
|
|
44
|
+
export type Located = { readonly path: string; readonly finding: Finding; readonly language: string; readonly rules: readonly RuleDefinition[] };
|
|
45
|
+
|
|
46
|
+
const ruleFor = (located: Located): RuleDefinition | undefined => located.rules.find((rule) => rule.id === located.finding.rule);
|
|
47
|
+
|
|
48
|
+
const sarifRule = (rule: RuleDefinition, language: string): SarifRule => ({
|
|
49
|
+
id: ruleIdOf(rule.id),
|
|
50
|
+
name: rule.id,
|
|
51
|
+
shortDescription: { text: localized(rule.name, language) },
|
|
52
|
+
// なぜ直すのかと、どう直すのかを一緒に上げる。指摘だけでは書いた人が動けない。
|
|
53
|
+
fullDescription: { text: localized(rule.why, language) },
|
|
54
|
+
help: { text: localized(rule.how_to_fix, language) },
|
|
55
|
+
properties: { tags: [rule.layer, ...rule.use_for] },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const resultOf = (located: Located): SarifResult => {
|
|
59
|
+
const rule = ruleFor(located);
|
|
60
|
+
return {
|
|
61
|
+
ruleId: ruleIdOf(located.finding.rule),
|
|
62
|
+
level: levelOf(located.finding.severity),
|
|
63
|
+
message: { text: rule === undefined ? located.finding.rule : messageOf(rule, located.finding, located.language) },
|
|
64
|
+
locations: [
|
|
65
|
+
{
|
|
66
|
+
physicalLocation: {
|
|
67
|
+
artifactLocation: { uri: located.path },
|
|
68
|
+
// SARIF の行と桁は 1 始まり。0 を上げると弾かれる。
|
|
69
|
+
region: { startLine: Math.max(1, located.finding.line), startColumn: Math.max(1, located.finding.column) },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** 同じ rule が 2 言語で出たら、先に出たほうの言語で 1 度だけ書く。SARIF は rule を 1 つしか持てない。 */
|
|
77
|
+
const usedRules = (located: readonly Located[]): SarifRule[] => {
|
|
78
|
+
const seen = new Map<string, Located>();
|
|
79
|
+
located.forEach((entry) => {
|
|
80
|
+
if (!seen.has(entry.finding.rule)) seen.set(entry.finding.rule, entry);
|
|
81
|
+
});
|
|
82
|
+
return [...seen.values()].flatMap((entry) => {
|
|
83
|
+
const rule = ruleFor(entry);
|
|
84
|
+
return rule === undefined ? [] : [sarifRule(rule, entry.language)];
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const renderSarif = (located: readonly Located[], version: string): string => {
|
|
89
|
+
return `${JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
$schema: SCHEMA,
|
|
92
|
+
version: "2.1.0",
|
|
93
|
+
runs: [
|
|
94
|
+
{
|
|
95
|
+
tool: { driver: { name: "chaff", version, informationUri: INFORMATION_URI, rules: usedRules(located) } },
|
|
96
|
+
results: located.map(resultOf),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
null,
|
|
101
|
+
2,
|
|
102
|
+
)}\n`;
|
|
103
|
+
};
|
package/src/render/semantic.ts
CHANGED
|
@@ -22,7 +22,7 @@ const narrowingLines = (result: SemanticResult): string[] =>
|
|
|
22
22
|
return [` ${name}: ${what} を含む ${narrowing.kept} 文だけを読みました(全 ${narrowing.total} 文)`];
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const aiBanner = (result: SemanticResult): string[] => [
|
|
26
26
|
"",
|
|
27
27
|
`═══ AI による判定 ${"═".repeat(RULE - 17)}`,
|
|
28
28
|
" 文章の意味を読んでいます。実行するたび結果が変わることが",
|
package/src/render/text.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Finding, Localized, RuleDefinition } from "../plugin.ts";
|
|
|
2
2
|
|
|
3
3
|
export const localized = (field: Localized, language: string): string => field[language] ?? field["en"] ?? Object.values(field)[0] ?? "";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const fill = (template: string, values: Readonly<Record<string, string | number>>): string =>
|
|
6
6
|
template.replace(/\{(\w+)\}/gu, (whole, key: string) => {
|
|
7
7
|
const value = values[key];
|
|
8
8
|
return value === undefined ? whole : String(value);
|
package/src/rule-load.ts
CHANGED
|
@@ -63,11 +63,22 @@ const missingFields = (raw: Record<string, unknown>, levels: LevelTable | undefi
|
|
|
63
63
|
...(levels?.normal === undefined ? ["levels.normal"] : []),
|
|
64
64
|
...(typeof raw["how_to_find"] === "string" ? [] : ["how_to_find"]),
|
|
65
65
|
...(Array.isArray(raw["use_for"]) ? [] : ["use_for"]),
|
|
66
|
-
...(isSeverity(raw["severity"]) ? [] : ["severity"]),
|
|
66
|
+
...(isSeverity(raw["severity"]) || isRecord(raw["severity"]) ? [] : ["severity"]),
|
|
67
67
|
];
|
|
68
68
|
|
|
69
69
|
const localizedOf = (value: unknown): Record<string, string> => (isLocalized(value) ? value : {});
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* severity も言語別に書ける。levels と同じ畳みかた。
|
|
73
|
+
* 記号の許容度は言語で大きく違う(ダッシュは英語では普通、日本語の組版では扱いが難しい)。
|
|
74
|
+
*/
|
|
75
|
+
const severityOf = (raw: unknown, language: string): Severity => {
|
|
76
|
+
if (isSeverity(raw)) return raw;
|
|
77
|
+
if (!isRecord(raw)) return "warning";
|
|
78
|
+
const picked: unknown = raw[language] ?? raw["default"];
|
|
79
|
+
return isSeverity(picked) ? picked : "warning";
|
|
80
|
+
};
|
|
81
|
+
|
|
71
82
|
const stringList = (value: unknown): string[] | undefined => (Array.isArray(value) ? value.map((entry) => String(entry)) : undefined);
|
|
72
83
|
|
|
73
84
|
const toRule = (raw: unknown, language: string, file: string): RuleDefinition => {
|
|
@@ -91,9 +102,10 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
|
|
|
91
102
|
what_to_check: isLocalized(raw["what_to_check"]) ? raw["what_to_check"] : undefined,
|
|
92
103
|
where: typeof raw["where"] === "string" ? raw["where"] : undefined,
|
|
93
104
|
requires: stringList(raw["requires"]) ?? [],
|
|
105
|
+
from: stringList(raw["from"]) ?? [],
|
|
94
106
|
languages: stringList(raw["languages"]),
|
|
95
107
|
use_for: Array.isArray(raw["use_for"]) ? raw["use_for"].map((entry) => String(entry)) : [],
|
|
96
|
-
severity:
|
|
108
|
+
severity: severityOf(raw["severity"], language),
|
|
97
109
|
};
|
|
98
110
|
};
|
|
99
111
|
|
package/src/run-semantic.ts
CHANGED
|
@@ -25,7 +25,11 @@ export type Job = {
|
|
|
25
25
|
readonly narrowing?: Narrowing;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 知らない絞り込みを文書全体に落とさない。spec §14 は「絞り込みを持たない L4 rule は
|
|
30
|
+
* 登録できない」と決めている。落とすと、**書いていない範囲が黙って LLM に送られる**。
|
|
31
|
+
*/
|
|
32
|
+
const filterFor = (name: string | undefined): (typeof FILTERS)[string] | undefined => FILTERS[name ?? "whole-document"];
|
|
29
33
|
|
|
30
34
|
const SEVERITY_BY_VALUE: readonly Severity[] = ["info", "info", "warning", "error"];
|
|
31
35
|
|
|
@@ -41,6 +45,9 @@ const builtInJobs = (doc: ProseDocument, rules: readonly RuleDefinition[], setti
|
|
|
41
45
|
.flatMap((rule) => {
|
|
42
46
|
const rubric = rule.what_to_check === undefined ? undefined : localized(rule.what_to_check, doc.language);
|
|
43
47
|
if (rubric === undefined || rubric.length === 0) return [];
|
|
48
|
+
// 絞り込みが無い rule は動かさない。黙って落とさず、下の skipped に出す。
|
|
49
|
+
const filter = filterFor(rule.how_to_find);
|
|
50
|
+
if (filter === undefined) return [];
|
|
44
51
|
return [
|
|
45
52
|
{
|
|
46
53
|
rule: rule.id,
|
|
@@ -48,7 +55,7 @@ const builtInJobs = (doc: ProseDocument, rules: readonly RuleDefinition[], setti
|
|
|
48
55
|
rubric,
|
|
49
56
|
howToFix: localized(rule.how_to_fix, doc.language),
|
|
50
57
|
severity: severityFromLevel(rule, settings[rule.id] ?? "normal"),
|
|
51
|
-
candidates:
|
|
58
|
+
candidates: filter(doc),
|
|
52
59
|
},
|
|
53
60
|
];
|
|
54
61
|
});
|
|
@@ -58,7 +65,7 @@ const userJobs = (doc: ProseDocument, checks: readonly UserCheck[], genre: strin
|
|
|
58
65
|
.filter((check) => check.level !== "off" && check.use_for.some((target) => genre.startsWith(target)))
|
|
59
66
|
.map((check) => {
|
|
60
67
|
// look_at の括弧の中を取り出して絞り込む。取り出せなければ文書全体。spec §14。
|
|
61
|
-
const { candidates, narrowing } = narrow(doc, check.look_at,
|
|
68
|
+
const { candidates, narrowing } = narrow(doc, check.look_at, FILTERS["whole-document"] ?? (() => []));
|
|
62
69
|
return {
|
|
63
70
|
rule: check.id,
|
|
64
71
|
name: check.name,
|
|
@@ -108,7 +115,10 @@ export const runSemantic = async (
|
|
|
108
115
|
): Promise<SemanticResult> => {
|
|
109
116
|
const jobs = planSemantic(doc, rules, checks, settings, genre);
|
|
110
117
|
const starts = lineStarts(doc.source);
|
|
111
|
-
const
|
|
118
|
+
const unfiltered = rules
|
|
119
|
+
.filter((rule) => rule.layer === "L4" && rule.use_for.some((target) => genre.startsWith(target)) && filterFor(rule.how_to_find) === undefined)
|
|
120
|
+
.map((rule) => ({ rule: rule.id, why: `絞り込み ${rule.how_to_find} が無いため(文書全体は送りません)` }));
|
|
121
|
+
const skipped = [...jobs.filter((job) => job.candidates.length === 0).map((job) => ({ rule: job.rule, why: "見るところが無かったため" })), ...unfiltered];
|
|
112
122
|
const live = jobs.filter((job) => job.candidates.length > 0);
|
|
113
123
|
const answers = await Promise.all(
|
|
114
124
|
live.flatMap((job) =>
|
package/src/run.ts
CHANGED
|
@@ -31,12 +31,22 @@ const has = (capabilities: ProseDocument["capabilities"], need: string): boolean
|
|
|
31
31
|
return false;
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 宣言だけでなく、実際に token が来ているかも見る。
|
|
36
|
+
*
|
|
37
|
+
* `capabilities.pos: true` と言いながら token を返さないアダプタでも、rule は
|
|
38
|
+
* `sentence.tokens ?? []` を見るので**例外にならず、指摘 0 件で終わる**。
|
|
39
|
+
* 0 件は「問題なし」と見分けがつかない。
|
|
40
|
+
*/
|
|
41
|
+
const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 || doc.sentences.some((sentence) => sentence.tokens !== undefined);
|
|
42
|
+
|
|
34
43
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
35
44
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
36
45
|
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return `${doc.language} 向けの rule ではないため`;
|
|
37
46
|
const missing = rule.requires.find((need) => !has(doc.capabilities, need));
|
|
38
|
-
if (missing
|
|
39
|
-
|
|
47
|
+
if (missing !== undefined) return `この言語では${CAPABILITY_NAME[missing] ?? missing}が使えないため`;
|
|
48
|
+
if (rule.requires.length > 0 && !hasTokens(doc)) return "アダプタが品詞を返さなかったため";
|
|
49
|
+
return undefined;
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefinition[] =>
|
|
@@ -59,6 +69,29 @@ const place = (starts: readonly number[], finding: Finding): Finding => {
|
|
|
59
69
|
return { ...finding, line: at.line, column: at.column };
|
|
60
70
|
};
|
|
61
71
|
|
|
72
|
+
/**
|
|
73
|
+
* 複合シグナル。他の rule の結果を読むので、detector の形には収まらない。run の二段目。spec §20.2。
|
|
74
|
+
*
|
|
75
|
+
* 1 本ずつでは何も言えないものが揃ったときだけ、1 件にまとめて出す。
|
|
76
|
+
* 元の指摘は消さない。`padded-intro` のように単独でも正しい指摘が混ざっており、
|
|
77
|
+
* まとめるために消すと本物の指摘が見えなくなる。
|
|
78
|
+
*/
|
|
79
|
+
const compositeOf = (rule: RuleDefinition, found: readonly Finding[], limit: number, starts: readonly number[]): Finding[] => {
|
|
80
|
+
const fired = rule.from.filter((id) => found.some((finding) => finding.rule === id));
|
|
81
|
+
if (fired.length < limit) return [];
|
|
82
|
+
const first = found.find((finding) => fired.includes(finding.rule));
|
|
83
|
+
return [
|
|
84
|
+
place(starts, {
|
|
85
|
+
rule: rule.id,
|
|
86
|
+
severity: rule.severity,
|
|
87
|
+
line: 0,
|
|
88
|
+
column: 0,
|
|
89
|
+
quote: first?.quote ?? "",
|
|
90
|
+
values: { word: fired.join("、"), count: fired.length, limit, offset: first === undefined ? 0 : Number(first.values["offset"] ?? 0) },
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
};
|
|
94
|
+
|
|
62
95
|
export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], settings: Settings, experimental: boolean, genre: string): RunResult => {
|
|
63
96
|
const starts = lineStarts(doc.source);
|
|
64
97
|
const applicable = forGenre(rules, genre);
|
|
@@ -78,6 +111,8 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
78
111
|
const why = rule.status === "experimental" && settings[rule.id] === undefined ? "まだ試験中のため" : "設定で止めているため";
|
|
79
112
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
80
113
|
}
|
|
114
|
+
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
115
|
+
if (rule.from.length > 0) return acc;
|
|
81
116
|
const detector = DETECTORS[rule.how_to_find];
|
|
82
117
|
if (detector === undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `検出器 ${rule.how_to_find} がないため` }] };
|
|
83
118
|
const options = {
|
|
@@ -94,5 +129,9 @@ export const runRules = (doc: ProseDocument, rules: readonly RuleDefinition[], s
|
|
|
94
129
|
},
|
|
95
130
|
{ findings: [], skipped: [] },
|
|
96
131
|
);
|
|
97
|
-
|
|
132
|
+
const composites = applicable
|
|
133
|
+
.filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
|
|
134
|
+
.flatMap((rule) => compositeOf(rule, outcome.findings, resolve(rule, levelFor(rule, settings, experimental), genre).limit, starts));
|
|
135
|
+
const all = [...outcome.findings, ...composites];
|
|
136
|
+
return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
|
|
98
137
|
};
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 版は manifest から読む。定数に書くと、上げ忘れたまま古い版を名乗るものが出る。
|
|
7
|
+
* SARIF の tool.version がそれで、指摘を出していない版に指摘が紐づく。
|
|
8
|
+
*/
|
|
9
|
+
const MANIFEST = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
10
|
+
|
|
11
|
+
const versionOf = (raw: unknown): string =>
|
|
12
|
+
typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string" ? raw.version : "0.0.0";
|
|
13
|
+
|
|
14
|
+
export const VERSION = versionOf(JSON.parse(readFileSync(MANIFEST, "utf8")));
|