hanlint 0.0.17 → 0.0.19
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 +23 -20
- package/data/calqueFrame.json +60 -0
- package/data/cliches.json +39 -29
- package/data/collocationStops.txt +28 -0
- package/data/confusable.json +20 -20
- package/data/easyWords.json +12 -246
- package/data/exemplars.json +50 -29
- package/data/hiddenDeps.txt +11 -11
- package/data/japaneseLoan.json +27 -27
- package/data/koreanProse.txt +64 -0
- package/data/patterns.json +26 -26
- package/data/phraseRepeat.json +11 -0
- package/data/platformApis.txt +11 -11
- package/data/redundantPair.json +16 -16
- package/data/ruleCategories.json +2 -0
- package/data/ruleDocs.json +56 -54
- package/data/ruleMechanisms.json +2 -0
- package/data/screenNarration.json +10 -10
- package/data/screenSentence.json +60 -12
- package/data/screenTone.json +6 -6
- package/data/screenWord.json +70 -110
- package/data/spacing.json +12 -12
- package/data/spelling.json +29 -29
- package/data/translationese.json +23 -24
- package/data/usageCounts.report.json +1676 -0
- package/data/version.json +1 -1
- package/package.json +1 -1
- package/src/analysis/grammar/ending.js +1 -1
- package/src/analysis/grammar/hangul.js +4 -4
- package/src/analysis/grammar/josa.js +1 -1
- package/src/analysis/grammar/register.js +1 -1
- package/src/analysis/grammar/voice.js +1 -1
- package/src/analysis/index.js +3 -3
- package/src/analysis/josa.js +1 -1
- package/src/analysis/splitSentences.js +1 -1
- package/src/analysis/tokenize.js +41 -26
- package/src/baseline/store.js +8 -8
- package/src/cli/help.js +11 -10
- package/src/cli/main.js +332 -182
- package/src/cli/walk.js +2 -2
- package/src/cli/welcome.js +8 -8
- package/src/config/loadConfig.js +3 -3
- package/src/config/patch.js +6 -6
- package/src/config/readerContract.js +26 -26
- package/src/config/settings.js +44 -25
- package/src/config/toml.js +9 -9
- package/src/data/exemplars.js +21 -21
- package/src/data/load.js +6 -6
- package/src/data/operations.js +14 -14
- package/src/data/patches.js +10 -10
- package/src/data/patterns.js +5 -5
- package/src/data/profiles.js +6 -6
- package/src/data/prose.js +27 -0
- package/src/document/model.js +4 -4
- package/src/document/parseMarkdown.js +7 -7
- package/src/document/plainText.js +3 -3
- package/src/document/sourceText.js +117 -19
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +18 -9
- package/src/fingerprint/codeBlocks.js +3 -3
- package/src/fingerprint/codeMarkers.js +4 -4
- package/src/fingerprint/dictionaries.js +34 -7
- package/src/fingerprint/markers.js +27 -5
- package/src/fingerprint/readerState.js +9 -9
- package/src/fingerprint/sourceSentence.js +1 -1
- package/src/fingerprint/topics.js +1 -1
- package/src/guard/contract.js +6 -6
- package/src/guard/editLocality.js +1 -1
- package/src/guard/outline.js +1 -1
- package/src/guard/receipt.js +13 -13
- package/src/guard/surface.js +4 -4
- package/src/index.js +9 -7
- package/src/learn/edits.js +2 -2
- package/src/learn/operations.js +1 -1
- package/src/regex.js +2 -2
- package/src/report/compactReport.js +7 -3
- package/src/report/fingerprintJson.js +2 -2
- package/src/report/githubReport.js +1 -1
- package/src/report/jsonReport.js +3 -3
- package/src/report/registerMatch.js +2 -2
- package/src/report/sheet.js +0 -0
- package/src/report/textReport.js +35 -16
- package/src/report/writingSpec.js +17 -17
- package/src/rules/code/inputFileSource.js +1 -1
- package/src/rules/code/installImport.js +2 -2
- package/src/rules/code/platformApi.js +1 -1
- package/src/rules/document/countMismatch.js +2 -1
- package/src/rules/document/enoughOnce.js +2 -1
- package/src/rules/document/fieldEcho.js +10 -6
- package/src/rules/document/keywordHeading.js +1 -1
- package/src/rules/document/keywordMissing.js +1 -1
- package/src/rules/document/noQuestion.js +2 -2
- package/src/rules/document/promiseRecall.js +1 -1
- package/src/rules/document/tableOddCell.js +1 -1
- package/src/rules/finding.js +4 -4
- package/src/rules/paragraph/factListParagraph.js +1 -1
- package/src/rules/paragraph/paraFragment.js +1 -1
- package/src/rules/readerDebt.js +1 -1
- package/src/rules/registry.js +30 -26
- package/src/rules/screen/screenWord.js +2 -1
- package/src/rules/sentence/calqueFrame.js +11 -0
- package/src/rules/sentence/cliche.js +7 -3
- package/src/rules/sentence/connectorRepeat.js +2 -1
- package/src/rules/sentence/danglingDeixis.js +1 -1
- package/src/rules/sentence/dash.js +1 -1
- package/src/rules/sentence/deixis.js +1 -1
- package/src/rules/sentence/doubleNegative.js +2 -1
- package/src/rules/sentence/doublePassive.js +2 -1
- package/src/rules/sentence/draftHistory.js +2 -1
- package/src/rules/sentence/endingRepeat.js +3 -2
- package/src/rules/sentence/euiChain.js +1 -1
- package/src/rules/sentence/fillerOpener.js +1 -1
- package/src/rules/sentence/hardWord.js +6 -3
- package/src/rules/sentence/imperativePeriod.js +1 -1
- package/src/rules/sentence/japaneseLoan.js +7 -3
- package/src/rules/sentence/longSentence.js +1 -1
- package/src/rules/sentence/negationRedefine.js +1 -1
- package/src/rules/sentence/nounPile.js +10 -4
- package/src/rules/sentence/numberOrphan.js +1 -1
- package/src/rules/sentence/outsideProfile.js +8 -3
- package/src/rules/sentence/phraseRepeat.js +54 -0
- package/src/rules/sentence/redundantPair.js +7 -3
- package/src/rules/sentence/translationese.js +7 -3
- package/src/rules/shared/candidates.js +2 -2
- package/src/rules/shared/dictionaryRule.js +46 -8
- package/src/rules/shared/repeat.js +2 -2
- package/src/rules/structure/bridgeRepeat.js +3 -3
- package/src/rules/structure/emojiBullet.js +1 -1
- package/src/rules/structure/headingSkip.js +1 -1
- package/src/rules/structure/headingUniform.js +2 -1
- package/src/rules/structure/loneSubheading.js +1 -1
- package/src/rules/structure/moreLater.js +1 -1
- package/src/text.js +2 -2
- package/src/usage/counts.js +54 -0
- package/src/usage/index.js +8 -0
- package/src/usage/sentences.js +681 -0
package/src/guard/contract.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Reader Contract
|
|
2
|
+
/** Reader Contract 검사와, 이유를 붙여 한 곳만 바꾸는 Patch의 검증. */
|
|
3
3
|
import { sha256 } from "../digest.js";
|
|
4
4
|
|
|
5
5
|
import { Patch } from "../config/patch.js";
|
|
@@ -19,7 +19,7 @@ function digest(text) {
|
|
|
19
19
|
return sha256(text);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** 원문의
|
|
22
|
+
/** 원문의 지켜야 할 숫자·링크·코드를 모두 담는 version 1 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal */
|
|
23
23
|
export function contractFromText(text, reader, goal) {
|
|
24
24
|
const contract = new Contract(reader, goal, factLines(text));
|
|
25
25
|
const result = surfaceDiff(contract.text, text);
|
|
@@ -27,15 +27,15 @@ export function contractFromText(text, reader, goal) {
|
|
|
27
27
|
for (const kind of ["missingNumbers", "missingUrls", "missingCode", "missingLinks"]) {
|
|
28
28
|
for (const value of result[/** @type {keyof typeof result} */ (kind)]) missing.push(`${kind}=${value}`);
|
|
29
29
|
}
|
|
30
|
-
if (missing.length) throw new Error(`reader
|
|
30
|
+
if (missing.length) throw new Error(`reader나 goal에 원문에 없는 숫자·링크·코드가 들었다: ${missing.join(", ")}`);
|
|
31
31
|
if (surfaceViolationCount(result)) {
|
|
32
32
|
const issues = Object.entries(result).flatMap(([kind, values]) => values.map((value) => `${kind}=${value}`));
|
|
33
|
-
throw new Error(`계약 초안이 원문의
|
|
33
|
+
throw new Error(`계약 초안이 원문의 지켜야 할 숫자·링크·코드를 모두 담지 못했다: ${issues.join(", ")}`);
|
|
34
34
|
}
|
|
35
35
|
return contract;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/** 원문에서 자동으로 찾은 숫자·링크·코드와 한 수준의 제목을 따로 담은 version 2 Contract 초안을 만든다. @param {string} text @param {string} reader @param {string} goal @param {number} [outlineLevel] @param {string[]} [facts] */
|
|
39
39
|
export function contractFromTextV2(text, reader, goal, outlineLevel = 2, facts = []) {
|
|
40
40
|
const doc = parseMarkdown(text);
|
|
41
41
|
const headings = headingsOf(doc, outlineLevel).map((heading) => heading.text);
|
|
@@ -50,7 +50,7 @@ export function contractFromTextV2(text, reader, goal, outlineLevel = 2, facts =
|
|
|
50
50
|
const result = surfaceDiff(contract.text, text);
|
|
51
51
|
if (surfaceViolationCount(result)) {
|
|
52
52
|
const issues = Object.entries(result).flatMap(([kind, values]) => values.map((value) => `${kind}=${value}`));
|
|
53
|
-
throw new Error(`reader, goal
|
|
53
|
+
throw new Error(`reader, goal, facts에 원문에 없는 숫자·링크·코드가 들었다: ${issues.join(", ")}`);
|
|
54
54
|
}
|
|
55
55
|
return contract;
|
|
56
56
|
}
|
package/src/guard/outline.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Reader Contract가
|
|
2
|
+
/** Reader Contract가 고정한 제목 순서와, 글 분석 결과에서 뽑은 구조 요약. */
|
|
3
3
|
|
|
4
4
|
/** @typedef {{position: number, expected: string | null, actual: string | null}} OutlineMismatch */
|
|
5
5
|
/** @typedef {{matches: boolean, level: number, expected: string[], actual: string[], mismatches: OutlineMismatch[]}} OutlineDiff */
|
package/src/guard/receipt.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Reader Contract 검사와 Patch 검증의
|
|
2
|
+
/** Reader Contract 검사와 Patch 검증의 결과 기록. 같은 입력이면 늘 같은 결과다. */
|
|
3
3
|
|
|
4
4
|
import { findingAsDict } from "../rules/finding.js";
|
|
5
5
|
import { readerDebts } from "../rules/readerDebt.js";
|
|
6
6
|
import { compareText, surfaceViolationCount } from "./surface.js";
|
|
7
7
|
|
|
8
8
|
export const CHECK_MEANING = "violationCount는 선언한 보호 원자와 hanlint error의 수다. facts의 관계와 진실, 빠진 의미, 독자 효용과 자연스러움은 검증하지 않는다";
|
|
9
|
-
export const CHECK_MEANING_V2 = "violationCount는
|
|
9
|
+
export const CHECK_MEANING_V2 = "violationCount는 계약에 적은 숫자·링크·코드의 어긋남, 제목 구조의 어긋남, 빠진 고정 문구, hanlint 오류를 더한 수다. facts 사이의 관계와 사실 여부, 빠진 뜻, 독자에게 쓸모 있는지와 자연스러운지는 검사하지 않는다";
|
|
10
10
|
export const PATCH_MEANING = "verified는 정확히 한 자리를 바꾸고 명시한 기존 위반을 줄이며 새 보호 원자 위반과 새 error를 만들지 않았다는 뜻뿐이다. 수정문의 의미와 진실, 자연스러움은 승인하지 않는다";
|
|
11
|
-
export const PATCH_MEANING_V2 = "verified는 정확히 한
|
|
11
|
+
export const PATCH_MEANING_V2 = "verified는 정확히 한 곳을 바꾸고, 적어 둔 기존 위반을 줄이고, 새 계약 위반과 새 오류를 만들지 않고, 고른 수정 예산을 지켰다는 뜻뿐이다. 고친 문장의 뜻이 맞는지, 사실인지, 자연스러운지는 보증하지 않는다";
|
|
12
12
|
|
|
13
13
|
/** @param {import("../rules/finding.js").Finding[]} findings */
|
|
14
14
|
function errorRules(findings) {
|
|
@@ -32,7 +32,7 @@ export class CheckResult {
|
|
|
32
32
|
this.document = document;
|
|
33
33
|
this.missingFacts = missingFacts;
|
|
34
34
|
if (![1, 2].includes(contractVersion)) {
|
|
35
|
-
throw new Error(`check result contractVersion
|
|
35
|
+
throw new Error(`check result의 contractVersion은 1이나 2여야 한다. 받은 값: ${contractVersion}`);
|
|
36
36
|
}
|
|
37
37
|
const hasStructure = outline !== null && document !== null;
|
|
38
38
|
if (hasStructure !== (contractVersion === 2)) {
|
|
@@ -89,7 +89,7 @@ export class PatchResult {
|
|
|
89
89
|
this.resultText = values.resultText;
|
|
90
90
|
this.contractVersion = values.contractVersion;
|
|
91
91
|
if (![1, 2].includes(values.contractVersion)) {
|
|
92
|
-
throw new Error(`patch result contractVersion
|
|
92
|
+
throw new Error(`patch result의 contractVersion은 1이나 2여야 한다. 받은 값: ${values.contractVersion}`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -137,7 +137,7 @@ export class PatchResult {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
/** 사람이 결론, 근거, 다음
|
|
140
|
+
/** 사람이 결론, 근거, 다음 할 일의 순서로 읽는 검사 결과. @param {CheckResult} result */
|
|
141
141
|
export function renderCheck(result) {
|
|
142
142
|
const lines = [result.violationCount === 0 ? "계약 위반 없음" : `계약 위반 ${result.violationCount}건`];
|
|
143
143
|
const labels = [
|
|
@@ -162,19 +162,19 @@ export function renderCheck(result) {
|
|
|
162
162
|
if (result.document) {
|
|
163
163
|
lines.push(`- 글: 문장 ${result.document.sentenceCount}, 문단 ${result.document.paragraphCount}, 절 ${result.document.sectionCount}`);
|
|
164
164
|
result.document.sections.forEach((section, index) => {
|
|
165
|
-
lines.push(` ${index + 1}. ${section.heading} (${section.line}
|
|
165
|
+
lines.push(` ${index + 1}. ${section.heading} (${section.line}줄, 문단 ${section.paragraphCount}, 코드 ${section.codeBlockCount})`);
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
|
-
lines.push(`- lint:
|
|
168
|
+
lines.push(`- lint: 오류 ${result.errorCount}건, 경고 ${result.noticeCount}건`);
|
|
169
169
|
if (result.errorCount) {
|
|
170
170
|
lines.push(` ${Object.entries(errorRules(result.findings)).map(([rule, count]) => `${rule} ${count}`).join(", ")}`);
|
|
171
171
|
}
|
|
172
172
|
const debts = readerDebts(result.findings);
|
|
173
|
-
lines.push(debts.length ? `-
|
|
174
|
-
for (const debt of debts) lines.push(` ${debt.line}
|
|
175
|
-
if (result.violationCount) lines.push("\n다음: 계약과 글 가운데 틀린 쪽을
|
|
176
|
-
else if (result.noticeCount) lines.push("\n다음:
|
|
177
|
-
else lines.push("\n다음:
|
|
173
|
+
lines.push(debts.length ? `- 독자가 막힐 곳: ${debts.length}건` : "- 독자가 막힐 곳: 없음");
|
|
174
|
+
for (const debt of debts) lines.push(` ${debt.line}줄 [${debt.rule}] ${debt.why}`);
|
|
175
|
+
if (result.violationCount) lines.push("\n다음: 계약과 글 가운데 틀린 쪽을 고치고 같은 check를 다시 돌린다");
|
|
176
|
+
else if (result.noticeCount) lines.push("\n다음: 경고를 읽고 고칠지 그대로 둘지 정한다");
|
|
177
|
+
else lines.push("\n다음: 규칙으로 셀 수 있는 계약 위반은 없다. 뜻과 자연스러움은 사람이 읽고 판단한다");
|
|
178
178
|
lines.push("", result.contractVersion === 1 ? CHECK_MEANING : CHECK_MEANING_V2);
|
|
179
179
|
return lines.join("\n");
|
|
180
180
|
}
|
package/src/guard/surface.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 계약 본문과 결과 글
|
|
2
|
+
/** 계약 본문과 결과 글 사이에서 지켜야 할 숫자·링크·코드의 차이. */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @typedef {object} SurfaceDiff
|
|
@@ -107,7 +107,7 @@ export function surfaceViolationCount(diff) {
|
|
|
107
107
|
return Object.values(diff).reduce((total, values) => total + values.length, 0);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/** 원문의
|
|
110
|
+
/** 원문의 지켜야 할 숫자·링크·코드를 뜻과 상관없이 정렬한 집합으로 나눈다. @param {string} text */
|
|
111
111
|
export function protectedSurface(text) {
|
|
112
112
|
const surfaceText = text.normalize("NFC");
|
|
113
113
|
return {
|
|
@@ -118,7 +118,7 @@ export function protectedSurface(text) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
/** 원문의
|
|
121
|
+
/** 원문의 지켜야 할 숫자·링크·코드를 모두 담는 Contract 사실 후보 줄. @param {string} text */
|
|
122
122
|
export function factLines(text) {
|
|
123
123
|
const surfaceText = text.normalize("NFC");
|
|
124
124
|
const seen = new Set();
|
|
@@ -144,7 +144,7 @@ export function factLines(text) {
|
|
|
144
144
|
),
|
|
145
145
|
);
|
|
146
146
|
if (!uncovered.size) {
|
|
147
|
-
throw new Error("원문에 자동으로
|
|
147
|
+
throw new Error("원문에 자동으로 지킬 숫자, URL, 인라인 코드, 링크 목적지가 없다. facts를 직접 작성한다");
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const selected = new Set();
|
package/src/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* hanlint: 한국어 글에서
|
|
3
|
+
* hanlint: 한국어 글에서 규칙으로 잡을 수 있는 잘못을 찾는 검사기. 공개 API는 이 파일 한 곳에 있다.
|
|
4
4
|
*
|
|
5
5
|
* ```js
|
|
6
6
|
* import { lintText, lintFile } from "hanlint";
|
|
7
7
|
* for (const f of lintFile("글.md")) console.log(f.line, f.rule, f.why);
|
|
8
8
|
* ```
|
|
9
9
|
*
|
|
10
|
-
* 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은
|
|
10
|
+
* 합격과 불합격을 판정하지 않는다. 지적 목록이 비어 있다는 것은 규칙으로 잡을 수 있는 잘못이 없다는 뜻이다.
|
|
11
11
|
* 같은 공개 API를 Node와 브라우저가 공유한다. 파일 경로를 읽는 API만 Node 전용이다.
|
|
12
12
|
*/
|
|
13
13
|
import { readExternal } from "./data/read.js";
|
|
@@ -26,7 +26,7 @@ import { configFromMapping, defaultConfig } from "./config/settings.js";
|
|
|
26
26
|
import { loadVersion } from "./data/load.js";
|
|
27
27
|
import { parseMarkdown } from "./document/parseMarkdown.js";
|
|
28
28
|
import { SOURCE_SUFFIXES, replaceLiteral, sourceLiterals } from "./document/sourceText.js";
|
|
29
|
-
import { parseSheet, renderSheet, renderSheetJson } from "./report/sheet.js";
|
|
29
|
+
import { applyRows, parseSheet, renderSheet, renderSheetJson, sheetRows } from "./report/sheet.js";
|
|
30
30
|
import { headingsOf } from "./document/model.js";
|
|
31
31
|
import { applyFixes } from "./edit/applyFixes.js";
|
|
32
32
|
import { buildFingerprint } from "./fingerprint/build.js";
|
|
@@ -54,6 +54,8 @@ export {
|
|
|
54
54
|
parseSheet,
|
|
55
55
|
renderSheet,
|
|
56
56
|
renderSheetJson,
|
|
57
|
+
sheetRows,
|
|
58
|
+
applyRows,
|
|
57
59
|
replaceLiteral,
|
|
58
60
|
sourceLiterals,
|
|
59
61
|
CONTRACT_VERSION,
|
|
@@ -82,7 +84,7 @@ export {
|
|
|
82
84
|
export const version = loadVersion();
|
|
83
85
|
|
|
84
86
|
/**
|
|
85
|
-
* 글을 한 번 읽어
|
|
87
|
+
* 글을 한 번 읽어 분석 결과를 만든다.
|
|
86
88
|
* @param {string} text
|
|
87
89
|
* @param {import("./config/settings.js").Config} [config]
|
|
88
90
|
* @param {string | null} [path]
|
|
@@ -102,7 +104,7 @@ export function lintText(text, config = defaultConfig(), path = null) {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
/**
|
|
105
|
-
* 파일을 UTF-8
|
|
107
|
+
* 파일을 UTF-8로 읽어 검사한다.
|
|
106
108
|
* @param {string} path
|
|
107
109
|
* @param {import("./config/settings.js").Config} [config]
|
|
108
110
|
*/
|
|
@@ -110,7 +112,7 @@ export function lintFile(path, config = defaultConfig()) {
|
|
|
110
112
|
return lintText(readExternal(path), config, path);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
/** 지적, 문체
|
|
115
|
+
/** 지적, 문체 예시, 승인 고침과 분석 결과를 같은 분석에서 준다. @param {string} text @param {import("./config/settings.js").Config} config */
|
|
114
116
|
export function inspectText(text, config = defaultConfig()) {
|
|
115
117
|
const document = fingerprint(text, config);
|
|
116
118
|
const findings = runAll(document, config);
|
|
@@ -130,7 +132,7 @@ export function learnText(before, after, config = defaultConfig()) {
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
/** 자유 원고의
|
|
135
|
+
/** 자유 원고의 숫자·링크·코드와 H2 순서를 비교한다. 빈 글과 반복 제목도 관찰하며 승인 계약을 만들지 않는다. @param {string} before @param {string} after @param {import("./config/settings.js").Config} config */
|
|
134
136
|
export function compareRevision(before, after, config = defaultConfig()) {
|
|
135
137
|
const original = before.normalize("NFC");
|
|
136
138
|
const outline = { level: 2, headings: headingsOf(parseMarkdown(original), 2).map((heading) => heading.text) };
|
package/src/learn/edits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Python learn/edits.py와 같은
|
|
2
|
+
/** Python learn/edits.py와 같은 방법으로 문장을 짝짓고, 지적이 사라진 문장 짝을 후보로 낸다. */
|
|
3
3
|
import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
|
|
4
4
|
import { readerKind } from "../fingerprint/readerState.js";
|
|
5
5
|
import { localCue, SENTENCE } from "../rules/finding.js";
|
|
@@ -21,7 +21,7 @@ export function learnExemplars(beforeDoc, afterDoc, beforeFindings, afterFinding
|
|
|
21
21
|
if (!afterText || before.text.trim() === afterPlain) continue;
|
|
22
22
|
seen.add(key);
|
|
23
23
|
learned.push({ rule: finding.rule, before: (rawBefore.get(before.index) ?? before.text).trim(), after: afterText,
|
|
24
|
-
moved: "
|
|
24
|
+
moved: "실제로 고친 문장으로 바꿨다", why: finding.why, beforeLine: before.line,
|
|
25
25
|
afterLines: [...new Set(after.map((item) => item.line))], sentence: before.text.trim(),
|
|
26
26
|
cue: localCue(finding), reader: readerKind(before, beforeDoc.reader.beforeSentence[finding.at]),
|
|
27
27
|
...(preset ? { presets: [preset] } : {}),
|
package/src/learn/operations.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** Python learn/operations.py의
|
|
2
|
+
/** Python learn/operations.py의 치환 후보와 증거 묶음. */
|
|
3
3
|
import { operationFromApproval } from "../data/operations.js";
|
|
4
4
|
import { sourceSentenceTexts } from "../fingerprint/sourceSentence.js";
|
|
5
5
|
import { changedSentencePairs } from "./pairs.js";
|
package/src/regex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 파이썬 re
|
|
3
|
+
* 파이썬 re와 같은 뜻으로 쓰는 얇은 껍질. search는 첫 일치, at은 문자열 첫머리에서만, all은 전부.
|
|
4
4
|
* 정규식 문법은 데이터 파일과 규칙에서 파이썬과 같은 부분집합만 쓴다 (lookbehind 포함, 이름 그룹 없음).
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -41,7 +41,7 @@ export function compile(source, flags = "") {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape
|
|
44
|
+
/** 정규식 특수문자를 이스케이프한다 (파이썬 re.escape의 쓰임새). @param {string} text */
|
|
45
45
|
export function escape(text) {
|
|
46
46
|
return text.replace(SPECIALS, (c) => BACKSLASH + c);
|
|
47
47
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 한 줄에 지적 하나. AI
|
|
2
|
+
/** 한 줄에 지적 하나. AI와 스크립트가 읽는 형식이다. 파이썬 report/compactReport.py와 같은 글자다. */
|
|
3
3
|
|
|
4
|
-
/** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이
|
|
4
|
+
/** 줄바꿈과 이어진 공백을 하나로 눕힌다. 한 줄은 이 형식의 계약이다. @param {string} text */
|
|
5
5
|
function flat(text) {
|
|
6
6
|
return text.split(/\s+/).filter(Boolean).join(" ");
|
|
7
7
|
}
|
|
@@ -9,6 +9,10 @@ function flat(text) {
|
|
|
9
9
|
/** @param {string} path @param {import("../rules/finding.js").Finding[]} findings */
|
|
10
10
|
export function renderCompact(path, findings) {
|
|
11
11
|
return findings
|
|
12
|
-
.map((f) =>
|
|
12
|
+
.map((f) => {
|
|
13
|
+
const label = f.severity === "notice" ? "경고: " : "";
|
|
14
|
+
const fixLabel = f.severity === "notice" ? "제안" : "고친 뒤";
|
|
15
|
+
return `${path}:${f.line} [${f.rule}] ${label}${flat(f.why)}` + (f.fix ? ` ${fixLabel}: ${flat(f.fix)}` : "");
|
|
16
|
+
})
|
|
13
17
|
.join("\n");
|
|
14
18
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py
|
|
4
|
-
* 아래층을 index
|
|
3
|
+
* 지문 계층의 JSON 꼴. 파이썬 report/fingerprintJson.py와 같은 키 순서다. 층마다 한 번씩만 적고 위층은
|
|
4
|
+
* 아래층을 index로 가리킨다. 실수는 소수 여섯째 자리로 맞춰 두 구현이 같은 글자를 낸다.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { roundHalfEven } from "../text.js";
|
package/src/report/jsonReport.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 기계가 읽는
|
|
4
|
-
* 지적마다 `exemplar
|
|
5
|
-
* 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory
|
|
3
|
+
* 기계가 읽는 형식. 평가 루프의 0층 입력이다. 파이썬 json.dumps(indent=2, ensure_ascii=False)와 같은 글자다.
|
|
4
|
+
* 지적마다 `exemplar`를 붙인다. 승인 원문 완전 일치 `patch`는 맞는 지적에만 별도로 붙인다.
|
|
5
|
+
* 실제 수정 차이는 파이썬의 exemplarLift와 patchMemory 실험 스크립트에서 따로 잰다.
|
|
6
6
|
*/
|
|
7
7
|
import { exemplarFor } from "../data/exemplars.js";
|
|
8
8
|
import { findingAsDict } from "../rules/finding.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
/** 한 문체로 저장한
|
|
2
|
+
/** 한 문체로 저장한 예시와 문형을 검사한 글의 문체에 맞춘다. */
|
|
3
3
|
import { HAPNIDA, REGISTERS, convertTemplate } from "../analysis/grammar/index.js";
|
|
4
4
|
|
|
5
5
|
/** 섞임과 없음은 데이터 정본의 합니다체를 유지한다. @param {string | null | undefined} register */
|
|
@@ -14,7 +14,7 @@ export function exemplarInRegister(exemplar, register) {
|
|
|
14
14
|
const after = convertTemplate(exemplar.after, target);
|
|
15
15
|
if (before.skipped || after.skipped) {
|
|
16
16
|
throw new Error(
|
|
17
|
-
exemplar.rule + "
|
|
17
|
+
exemplar.rule + " 예시의 서술어를 못 풀었다: " + (before.skipped + after.skipped),
|
|
18
18
|
);
|
|
19
19
|
}
|
|
20
20
|
return { rule: exemplar.rule, before: before.text, after: after.text, moved: exemplar.moved, presets: exemplar.presets };
|
package/src/report/sheet.js
CHANGED
|
Binary file
|
package/src/report/textReport.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/**
|
|
3
|
-
* 사람이 읽는 지적 목록.
|
|
4
|
-
* 끝에
|
|
3
|
+
* 사람이 읽는 지적 목록. `파일:줄`로 시작해 편집기가 바로 연다. 파이썬 report/textReport.py와 같은 글자다.
|
|
4
|
+
* 끝에 예시를 붙인다. 전과 후를 제 줄에 둔다. 왜 한 줄이 아닌지는 파이썬 report/textReport.py가 소유한다.
|
|
5
5
|
*/
|
|
6
6
|
import { exemplarFor, shortened, twoLines } from "../data/exemplars.js";
|
|
7
7
|
import { exemplarInRegister } from "./registerMatch.js";
|
|
8
8
|
|
|
9
|
-
/** 그 글에 나온 규칙의
|
|
9
|
+
/** 그 글에 나온 규칙의 예시. 이름 순이고 규칙 하나에 세 줄이다. @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} register @param {string | null | undefined} preset @param {import("../data/exemplars.js").Exemplar[]} customExemplars */
|
|
10
10
|
function exemplarLines(findings, register, preset, customExemplars) {
|
|
11
11
|
const lines = [];
|
|
12
12
|
let cut = false;
|
|
@@ -19,15 +19,15 @@ function exemplarLines(findings, register, preset, customExemplars) {
|
|
|
19
19
|
lines.push(` [${name}]`, ` 전 ${before}`, ` 후 ${after}`);
|
|
20
20
|
}
|
|
21
21
|
if (!lines.length) return [];
|
|
22
|
-
const head = "
|
|
23
|
-
return [cut ? `${head}. 잘린 것은 hanlint explain
|
|
22
|
+
const head = "예시 (고치기 전, 고친 뒤)";
|
|
23
|
+
return [cut ? `${head}. 잘린 것은 hanlint explain <규칙>으로 본다` : head, ...lines];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
/**
|
|
26
|
+
/** 예시 아래에 붙이는 선택지. 지적 자리별이며 순위는 없다. @param {import("../rules/finding.js").Finding[]} findings */
|
|
27
27
|
function candidateLines(findings) {
|
|
28
28
|
const chosen = findings.filter((finding) => finding.candidates.length);
|
|
29
29
|
if (!chosen.length) return [];
|
|
30
|
-
const lines = ["후보 (
|
|
30
|
+
const lines = ["후보 (hanlint가 하나를 고르지 않았다)"];
|
|
31
31
|
for (const finding of chosen) {
|
|
32
32
|
lines.push(` ${finding.line}줄 [${finding.rule}]`);
|
|
33
33
|
for (const candidate of finding.candidates) lines.push(` - ${candidate.text} (${candidate.why})`);
|
|
@@ -35,21 +35,40 @@ function candidateLines(findings) {
|
|
|
35
35
|
return lines;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** 접은 경고. 규칙마다 개수와 줄 번호 한 줄. 뜻은 파이썬 textReport.py의 noticeLines가 소유한다. @param {import("../rules/finding.js").Finding[]} notices */
|
|
39
|
+
function noticeLines(notices) {
|
|
40
|
+
/** @type {Map<string, number[]>} */
|
|
41
|
+
const byRule = new Map();
|
|
42
|
+
for (const finding of notices) {
|
|
43
|
+
if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
|
|
44
|
+
/** @type {number[]} */ (byRule.get(finding.rule)).push(finding.line);
|
|
45
|
+
}
|
|
46
|
+
const lines = [`경고 ${notices.length}건 (접어 두었다. 다 보려면 --notices)`];
|
|
47
|
+
for (const rule of [...byRule.keys()].sort()) {
|
|
48
|
+
const numbers = [...new Set(/** @type {number[]} */ (byRule.get(rule)))].sort((a, b) => a - b);
|
|
49
|
+
lines.push(` ${rule} ${/** @type {number[]} */ (byRule.get(rule)).length} ${numbers.join(", ")}줄`);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string} path @param {import("../rules/finding.js").Finding[]} findings @param {string | null | undefined} [register] @param {string | null | undefined} [preset] @param {import("../data/exemplars.js").Exemplar[]} [customExemplars] @param {boolean} [unfoldNotices] */
|
|
55
|
+
export function renderText(path, findings, register = null, preset = null, customExemplars = [], unfoldNotices = false) {
|
|
56
|
+
if (!findings.length) return `${path} 오류 없음`;
|
|
41
57
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
42
58
|
const notices = findings.length - errors;
|
|
43
|
-
const summary = `${path}
|
|
59
|
+
const summary = `${path} 오류 ${errors}건` + (notices ? `, 경고 ${notices}건` : "");
|
|
44
60
|
const lines = [summary, ""];
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
const folded = unfoldNotices ? [] : findings.filter((f) => f.severity === "notice");
|
|
62
|
+
const shown = findings.filter((f) => unfoldNotices || f.severity === "error");
|
|
63
|
+
for (const f of shown) {
|
|
64
|
+
const tag = `[${f.rule}]` + (f.severity === "notice" ? " 경고" : "");
|
|
47
65
|
lines.push(`${path}:${f.line} ${tag}`, ` ${f.quote}`, ` ${f.why}`);
|
|
48
|
-
if (f.fix) lines.push(` 고친
|
|
66
|
+
if (f.fix) lines.push(` ${f.severity === "notice" ? "제안" : "고친 뒤"}: ${f.fix}`);
|
|
49
67
|
lines.push("");
|
|
50
68
|
}
|
|
51
|
-
lines.push(...
|
|
52
|
-
|
|
69
|
+
if (folded.length) lines.push(...noticeLines(folded), "");
|
|
70
|
+
lines.push(...exemplarLines(shown, register, preset, customExemplars));
|
|
71
|
+
const candidates = candidateLines(shown);
|
|
53
72
|
if (candidates.length) lines.push("", ...candidates);
|
|
54
73
|
return lines.join("\n").replace(/\n+$/, "");
|
|
55
74
|
}
|
|
@@ -27,21 +27,21 @@ function percent(value) {
|
|
|
27
27
|
function selectedProfile(config) {
|
|
28
28
|
if (config.profile) return userProfile(config.profile);
|
|
29
29
|
const kind = PROFILE_OF[config.preset];
|
|
30
|
-
if (kind === null) throw new Error(`${config.preset}
|
|
30
|
+
if (kind === null) throw new Error(`${config.preset} 프리셋은 비교할 프로파일이 없어 스펙을 낼 수 없다`);
|
|
31
31
|
const profile = profileOf(kind);
|
|
32
32
|
if (profile === null) throw new Error(`${kind} 프로파일을 찾지 못했다`);
|
|
33
33
|
return profile;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* 현재
|
|
37
|
+
* 현재 규칙 세트를 쓰기 전 숫자 사양으로 편다. 품질 점수나 새 임계는 만들지 않는다.
|
|
38
38
|
* @param {import("./settings.js").Config} [config]
|
|
39
39
|
* @param {string} [register]
|
|
40
40
|
* @param {number | null} [targetChars]
|
|
41
41
|
*/
|
|
42
42
|
export function writingSpec(config = defaultConfig(), register = HAPNIDA, targetChars = null) {
|
|
43
|
-
if (!REGISTERS.includes(register)) throw new Error(`register
|
|
44
|
-
if (targetChars !== null && targetChars <= 0) throw new Error(`chars
|
|
43
|
+
if (!REGISTERS.includes(register)) throw new Error(`register는 ${REGISTERS.join(", ")} 가운데 하나여야 한다. 받은 값: ${register}`);
|
|
44
|
+
if (targetChars !== null && targetChars <= 0) throw new Error(`chars는 1 이상이어야 한다. 받은 값: ${targetChars}`);
|
|
45
45
|
const profile = selectedProfile(config);
|
|
46
46
|
const sentence = profile.sentence;
|
|
47
47
|
const paragraph = profile.paragraph.get("sentenceCount");
|
|
@@ -62,7 +62,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
62
62
|
}
|
|
63
63
|
rows.push({ id: "amount", label: "분량", guidance: amount, basis: [`profile.${profile.kind}.paragraph.sentenceCount`] });
|
|
64
64
|
|
|
65
|
-
let length =
|
|
65
|
+
let length = `중앙값 ${lengthP50}어절, 문장 90%가 ${percentile(lengthHistogram, 90)}어절 이하`;
|
|
66
66
|
const lengthBasis = [`profile.${profile.kind}.sentence.length`];
|
|
67
67
|
if (enabled(config, "longSentence")) {
|
|
68
68
|
length += `. ${config.longSentenceMax}어절을 넘으면 longSentence가 지적`;
|
|
@@ -71,7 +71,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
71
71
|
rows.push({ id: "sentenceLength", label: "문장 길이", guidance: length, basis: lengthBasis });
|
|
72
72
|
|
|
73
73
|
if (profile.endingRuns !== null) {
|
|
74
|
-
let endingText = `같은 끝맺음 연속
|
|
74
|
+
let endingText = `같은 끝맺음 연속 중앙값 ${percentile(profile.endingRuns, 50)}개, 90%가 ${percentile(profile.endingRuns, 90)}개 이하`;
|
|
75
75
|
const endingBasis = [`profile.${profile.kind}.endingRuns`];
|
|
76
76
|
if (enabled(config, "endingRepeat")) {
|
|
77
77
|
endingText += `. 인과나 독자 호출 없이 ${config.endingRun}개부터 endingRepeat가 지적`;
|
|
@@ -85,13 +85,13 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
85
85
|
rows.push({
|
|
86
86
|
id: "commas",
|
|
87
87
|
label: "쉼표",
|
|
88
|
-
guidance: `문장
|
|
88
|
+
guidance: `문장 중앙값 ${percentile(commas, 50)}개, 90%가 ${percentile(commas, 90)}개 이하, 99%가 ${percentile(commas, 99)}개 이하`,
|
|
89
89
|
basis: [`profile.${profile.kind}.sentence.commas`],
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
const eui = sentence.get("euiCount");
|
|
93
93
|
if (!eui) throw new Error("프로파일에 sentence.euiCount가 없다");
|
|
94
|
-
let euiText = `문장
|
|
94
|
+
let euiText = `문장 중앙값 ${percentile(eui, 50)}개, 90%가 ${percentile(eui, 90)}개 이하`;
|
|
95
95
|
const euiBasis = [`profile.${profile.kind}.sentence.euiCount`];
|
|
96
96
|
if (enabled(config, "euiChain")) {
|
|
97
97
|
euiText += ". 3개부터, 붙은 2개부터 euiChain이 지적";
|
|
@@ -114,7 +114,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
114
114
|
rows.push({
|
|
115
115
|
id: "newTopics",
|
|
116
116
|
label: "새 화제",
|
|
117
|
-
guidance: `문장
|
|
117
|
+
guidance: `문장 중앙값 ${percentile(newTopics, 50)}개, 90%가 ${percentile(newTopics, 90)}개 이하`,
|
|
118
118
|
basis: [`profile.${profile.kind}.sentence.newTopics`],
|
|
119
119
|
});
|
|
120
120
|
|
|
@@ -122,7 +122,7 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
122
122
|
rows.push({
|
|
123
123
|
id: "connector",
|
|
124
124
|
label: "문두 접속",
|
|
125
|
-
guidance: `문서
|
|
125
|
+
guidance: `문서 중앙값은 문장의 ${percent(connector.p50)}%, 문서 90%가 ${percent(connector.p90)}% 이하`,
|
|
126
126
|
basis: [`profile.${profile.kind}.rates.connector`],
|
|
127
127
|
});
|
|
128
128
|
|
|
@@ -132,8 +132,8 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
132
132
|
const numberP90 = percentile(numbers, 90);
|
|
133
133
|
const numberText =
|
|
134
134
|
numberP50 === 0 && numberP90 === 0
|
|
135
|
-
? "문장
|
|
136
|
-
: `문장
|
|
135
|
+
? "문장 중앙값과 90%에서 숫자 없음"
|
|
136
|
+
: `문장 중앙값 ${numberP50}개, 90%가 ${numberP90}개 이하`;
|
|
137
137
|
rows.push({ id: "numbers", label: "숫자", guidance: numberText, basis: [`profile.${profile.kind}.sentence.numbers`] });
|
|
138
138
|
|
|
139
139
|
const question = profile.rates.question;
|
|
@@ -142,10 +142,10 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
142
142
|
if (enabled(config, "noQuestion")) {
|
|
143
143
|
questionText = config.enforceStyle.includes("noQuestion")
|
|
144
144
|
? "선택한 정책: 절이 2개 이상이면 글 전체에 물음표 최소 1개"
|
|
145
|
-
: "
|
|
145
|
+
: "물음표가 없으면 경고다. 질문 없이 충분히 설명했다면 그대로 둔다";
|
|
146
146
|
questionBasis = [`profile.${profile.kind}.rates.question`, "rule.noQuestion"];
|
|
147
147
|
} else {
|
|
148
|
-
questionText = `규칙 요구 없음. 문서
|
|
148
|
+
questionText = `규칙 요구 없음. 문서 중앙값은 문장의 ${percent(question.p50)}%, 문서 90%가 ${percent(question.p90)}% 이하`;
|
|
149
149
|
questionBasis = [`profile.${profile.kind}.rates.question`];
|
|
150
150
|
}
|
|
151
151
|
rows.push({ id: "question", label: "물음표", guidance: questionText, basis: questionBasis });
|
|
@@ -161,14 +161,14 @@ export function writingSpec(config = defaultConfig(), register = HAPNIDA, target
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** 사람이 읽는
|
|
164
|
+
/** 사람이 읽는 형식. basis는 JSON에서 확인한다. @param {ReturnType<typeof writingSpec>} spec */
|
|
165
165
|
export function renderWritingSpec(spec) {
|
|
166
166
|
const target = spec.targetChars !== null ? `, ${spec.targetChars}자` : "";
|
|
167
167
|
const lines = [
|
|
168
|
-
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은
|
|
168
|
+
`hanlint spec ${spec.preset} 종류, ${spec.register}체${target}. 같은 규칙의 기준값과 ${spec.profile} 글의 평소 수치를 쓰기 전에 보여 준다`,
|
|
169
169
|
];
|
|
170
170
|
const width = Math.max(...spec.rows.map((row) => row.label.length));
|
|
171
171
|
for (const row of spec.rows) lines.push(` ${row.label.padEnd(width)} ${row.guidance}`);
|
|
172
|
-
lines.push("", "이
|
|
172
|
+
lines.push("", "이 스펙은 품질 점수가 아니다. 다 쓴 뒤 같은 설정으로 hanlint <글.md>를 실행해 지적을 확인한다");
|
|
173
173
|
return lines.join("\n");
|
|
174
174
|
}
|
|
@@ -42,7 +42,7 @@ export function run(doc) {
|
|
|
42
42
|
if (!isDataFile(path)) continue;
|
|
43
43
|
const name2 = fileName(path);
|
|
44
44
|
if (have.has(name2) || doc.reader.mentionedBefore(block.index, name2)) continue;
|
|
45
|
-
findings.push(finding(name, line, code.trim(), `\`${name2}\`
|
|
45
|
+
findings.push(finding(name, line, code.trim(), `\`${name2}\` 파일을 읽는데 글 어디에서도 만들지 않았다. 독자는 여기서 파일 없음 오류로 멈춘다`, null, "error", DOCUMENT, block.index));
|
|
46
46
|
}
|
|
47
47
|
for (const path of [...writeTargets(code), ...reads]) {
|
|
48
48
|
if (NOT_A_PATH.some((mark) => path.includes(mark))) continue;
|
|
@@ -108,7 +108,7 @@ export function run(doc) {
|
|
|
108
108
|
const acceptable = packageOf().get(top) ?? new Set([normalize(top)]);
|
|
109
109
|
if (![...acceptable].some((pkg) => packages.has(pkg))) {
|
|
110
110
|
const packageNames = [...acceptable].sort().join("`, `");
|
|
111
|
-
findings.push(finding(name, line, code.trim(), `\`${top}\`
|
|
111
|
+
findings.push(finding(name, line, code.trim(), `\`${top}\` 모듈을 import하는데 설치 줄에 그 모듈이 든 패키지(\`${packageNames}\`)가 없다. 독자는 ModuleNotFoundError에서 멈춘다`, null, "error", DOCUMENT, block.index));
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -120,7 +120,7 @@ export function run(doc) {
|
|
|
120
120
|
const name2 = normalize(parsed[1]);
|
|
121
121
|
const extra = parsed[2] ? normalize(parsed[2]) : null;
|
|
122
122
|
if (packages.has(name2) && (extra === null || (packages.get(name2)?.has(extra) ?? false))) continue;
|
|
123
|
-
findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\`
|
|
123
|
+
findings.push(finding(name, line, code.trim(), `이 줄은 \`${requirement}\` 패키지가 있어야 실행되는데 설치 줄에 없다. ${why}`, null, NOTICE, DOCUMENT, block.index));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
|
@@ -27,7 +27,7 @@ export function run(doc) {
|
|
|
27
27
|
for (const [pattern, source, platform, alternative] of platformApis()) {
|
|
28
28
|
pattern.lastIndex = 0;
|
|
29
29
|
if (pattern.test(code)) {
|
|
30
|
-
findings.push(finding(name, line, code.trim(), `\`${source}\`
|
|
30
|
+
findings.push(finding(name, line, code.trim(), `\`${source}\` 부분은 ${platform} 전용이다. 다른 운영체제의 독자는 여기서 멈춘다. ${alternative}`, null, NOTICE, DOCUMENT, block.index));
|
|
31
31
|
break;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
+
import { fitJosa } from "../../analysis/grammar/josa.js";
|
|
2
3
|
import { DOCUMENT, finding } from "../finding.js";
|
|
3
4
|
|
|
4
5
|
export const name = "countMismatch";
|
|
@@ -50,7 +51,7 @@ export function run(doc, config) {
|
|
|
50
51
|
const conflict = promises.slice(i + 1).find((p) => p[1] !== numberA && comparable(doc, lineA, p[0], config.countMismatchSpan));
|
|
51
52
|
if (!conflict) continue;
|
|
52
53
|
const [lineB, , textB] = conflict;
|
|
53
|
-
findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}
|
|
54
|
+
findings.push(finding(name, lineB, textB, `${lineA}번째 줄은 \`${textA}\`${fitJosa(textA, "이라고")} 했는데 여기서는 \`${textB}\`${fitJosa(textB, "이라고")} 한다. 같은 단위로 다른 수를 약속하면 독자가 어느 쪽을 믿을지 모른다`, null, "error", DOCUMENT, -1));
|
|
54
55
|
break;
|
|
55
56
|
}
|
|
56
57
|
}
|