hanlint 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/README.md +27 -0
- package/bin/hanlint.js +4 -0
- package/data/causalMarkers.txt +25 -0
- package/data/cliches.json +99 -0
- package/data/confusable.json +76 -0
- package/data/connectors.txt +18 -0
- package/data/countUnits.txt +6 -0
- package/data/coverageTypes.txt +7 -0
- package/data/deixis.txt +8 -0
- package/data/easyWords.json +340 -0
- package/data/emphasisWords.txt +11 -0
- package/data/endings.txt +9 -0
- package/data/euiNouns.txt +40 -0
- package/data/hedges.txt +15 -0
- package/data/hiddenDeps.txt +11 -0
- package/data/holeKinds.json +145 -0
- package/data/japaneseLoan.json +82 -0
- package/data/josa.txt +54 -0
- package/data/koreanNumbers.txt +22 -0
- package/data/passiveStems.txt +53 -0
- package/data/platformApis.txt +12 -0
- package/data/promiseMarkers.txt +7 -0
- package/data/pythonPackages.txt +35 -0
- package/data/pythonStdlib.txt +193 -0
- package/data/readerCalls.txt +14 -0
- package/data/recallMarkers.txt +6 -0
- package/data/redundantPair.json +70 -0
- package/data/ruleDocs.json +44 -0
- package/data/spacing.json +76 -0
- package/data/spelling.json +166 -0
- package/data/stopwords.txt +33 -0
- package/data/translationese.json +104 -0
- package/data/verbTails.txt +56 -0
- package/data/version.json +1 -0
- package/package.json +39 -0
- package/src/analysis/index.js +34 -0
- package/src/analysis/surface/splitSentences.js +36 -0
- package/src/analysis/surface/tokenize.js +162 -0
- package/src/cli/main.js +376 -0
- package/src/config/loadConfig.js +51 -0
- package/src/config/settings.js +80 -0
- package/src/config/toml.js +239 -0
- package/src/data/load.js +63 -0
- package/src/document/model.js +55 -0
- package/src/document/parseMarkdown.js +183 -0
- package/src/document/plainText.js +43 -0
- package/src/edit/applyFixes.js +0 -0
- package/src/fingerprint/build.js +284 -0
- package/src/fingerprint/dictionaries.js +118 -0
- package/src/fingerprint/markers.js +175 -0
- package/src/fingerprint/topics.js +42 -0
- package/src/index.js +68 -0
- package/src/regex.js +47 -0
- package/src/report/compactReport.js +9 -0
- package/src/report/fingerprintJson.js +126 -0
- package/src/report/githubReport.js +13 -0
- package/src/report/jsonReport.js +16 -0
- package/src/report/textReport.js +19 -0
- package/src/rules/code/duplicateBlock.js +48 -0
- package/src/rules/code/firstResultDistance.js +23 -0
- package/src/rules/code/inputFileSource.js +103 -0
- package/src/rules/code/installImport.js +126 -0
- package/src/rules/code/platformApi.js +36 -0
- package/src/rules/document/countMismatch.js +42 -0
- package/src/rules/document/keywordMissing.js +21 -0
- package/src/rules/document/noQuestion.js +11 -0
- package/src/rules/document/promiseRecall.js +12 -0
- package/src/rules/document/readerAbsent.js +11 -0
- package/src/rules/finding.js +53 -0
- package/src/rules/orthography/confusable.js +9 -0
- package/src/rules/orthography/spacing.js +9 -0
- package/src/rules/orthography/spelling.js +9 -0
- package/src/rules/paragraph/factListParagraph.js +27 -0
- package/src/rules/paragraph/paraFragment.js +29 -0
- package/src/rules/paragraph/topicBreak.js +25 -0
- package/src/rules/registry.js +147 -0
- package/src/rules/sentence/cliche.js +9 -0
- package/src/rules/sentence/connectorRepeat.js +23 -0
- package/src/rules/sentence/danglingDeixis.js +19 -0
- package/src/rules/sentence/dash.js +21 -0
- package/src/rules/sentence/deixis.js +19 -0
- package/src/rules/sentence/doubleNegative.js +28 -0
- package/src/rules/sentence/doublePassive.js +17 -0
- package/src/rules/sentence/ellipsis.js +17 -0
- package/src/rules/sentence/endingRepeat.js +25 -0
- package/src/rules/sentence/euiChain.js +18 -0
- package/src/rules/sentence/fillerOpener.js +18 -0
- package/src/rules/sentence/hardWord.js +10 -0
- package/src/rules/sentence/imperativePeriod.js +20 -0
- package/src/rules/sentence/japaneseLoan.js +9 -0
- package/src/rules/sentence/longSentence.js +20 -0
- package/src/rules/sentence/negationRedefine.js +22 -0
- package/src/rules/sentence/nounPile.js +20 -0
- package/src/rules/sentence/redundantPair.js +9 -0
- package/src/rules/sentence/translationese.js +9 -0
- package/src/rules/shared/codeBlocks.js +44 -0
- package/src/rules/shared/dictionaryRule.js +35 -0
- package/src/rules/shared/runs.js +22 -0
- package/src/rules/structure/emojiBullet.js +34 -0
- package/src/rules/structure/headingSentence.js +17 -0
- package/src/rules/structure/headingSkip.js +18 -0
- package/src/rules/structure/headingUniform.js +36 -0
- package/src/rules/structure/introLong.js +20 -0
- package/src/rules/structure/sectionNoProse.js +16 -0
- package/src/rules/structure/sectionResult.js +37 -0
- package/src/text.js +63 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { DOCUMENT, NOTICE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "readerAbsent";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
if (doc.bodySections.length < 2 || doc.questionCount > 0 || doc.readerCallCount > 0) return [];
|
|
9
|
+
const first = doc.sentences.length ? doc.sentences[0] : null;
|
|
10
|
+
return [finding(name, first ? first.line : 1, first ? first.text : "", "질문도 독자를 부르는 문장도 없다. 독자가 할 행동을 동사로 끝내는 문장을 한 번은 넣는다", null, NOTICE, DOCUMENT, -1)];
|
|
11
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** 지적 하나의 모양. 파이썬 rules/finding.py 와 같은 필드와 같은 JSON 순서다. */
|
|
3
|
+
|
|
4
|
+
export const ERROR = "error";
|
|
5
|
+
export const NOTICE = "notice";
|
|
6
|
+
|
|
7
|
+
export const SENTENCE = "sentence";
|
|
8
|
+
export const PARAGRAPH = "paragraph";
|
|
9
|
+
export const SECTION = "section";
|
|
10
|
+
export const DOCUMENT = "document";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} Finding
|
|
14
|
+
* @property {string} rule
|
|
15
|
+
* @property {number} line
|
|
16
|
+
* @property {string} quote
|
|
17
|
+
* @property {string} why
|
|
18
|
+
* @property {string | null} fix 기계가 낼 수 있을 때만
|
|
19
|
+
* @property {string} severity error 는 규칙 위반, notice 는 확인이 필요한 것
|
|
20
|
+
* @property {string} scope sentence, paragraph, section, document
|
|
21
|
+
* @property {number} at scope 가 가리키는 지문의 index
|
|
22
|
+
* @property {string | null} fragment 원문에서 바꿀 조각. hanlint fix 가 replacement 로 바꾼다
|
|
23
|
+
* @property {string | null} replacement
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} rule
|
|
28
|
+
* @param {number} line
|
|
29
|
+
* @param {string} quote
|
|
30
|
+
* @param {string} why
|
|
31
|
+
* @param {string | null} [fix]
|
|
32
|
+
* @param {string} [severity]
|
|
33
|
+
* @param {string} [scope]
|
|
34
|
+
* @param {number} [at]
|
|
35
|
+
* @param {string | null} [fragment]
|
|
36
|
+
* @param {string | null} [replacement]
|
|
37
|
+
* @returns {Finding}
|
|
38
|
+
*/
|
|
39
|
+
export function finding(rule, line, quote, why, fix = null, severity = ERROR, scope = SENTENCE, at = -1, fragment = null, replacement = null) {
|
|
40
|
+
return { rule, line, quote, why, fix, severity, scope, at, fragment, replacement };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {Finding} f */
|
|
44
|
+
export function findingAsDict(f) {
|
|
45
|
+
/** @type {Record<string, unknown>} */
|
|
46
|
+
const data = { rule: f.rule, line: f.line, severity: f.severity, scope: f.scope, at: f.at, quote: f.quote, why: f.why };
|
|
47
|
+
if (f.fix) data.fix = f.fix;
|
|
48
|
+
if (f.replacement !== null) {
|
|
49
|
+
data.fragment = f.fragment;
|
|
50
|
+
data.replacement = f.replacement;
|
|
51
|
+
}
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
|
|
4
|
+
export const name = "confusable";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
return dictionaryFindings(doc, "confusable", name);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
|
|
4
|
+
export const name = "spacing";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
return dictionaryFindings(doc, "spacing", name);
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
|
|
4
|
+
export const name = "spelling";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
return dictionaryFindings(doc, "spelling", name);
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { NOTICE, PARAGRAPH, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "factListParagraph";
|
|
5
|
+
|
|
6
|
+
/** 문장 사이를 잇는 표지가 하나라도 있는가. @param {import("../../fingerprint/build.js").ParagraphPrint} paragraph */
|
|
7
|
+
function isLinked(paragraph) {
|
|
8
|
+
if (paragraph.causalTotal > 0) return true;
|
|
9
|
+
return paragraph.sentences.some((s) => s.connectorStart || s.mood === "의문" || s.readerCall);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
14
|
+
* @param {import("../../config/settings.js").Config} config
|
|
15
|
+
*/
|
|
16
|
+
export function run(doc, config) {
|
|
17
|
+
const findings = [];
|
|
18
|
+
for (const paragraph of doc.paragraphs) {
|
|
19
|
+
if (paragraph.sentenceCount < config.factListMinSentences || isLinked(paragraph)) continue;
|
|
20
|
+
if (paragraph.meanLength <= config.factListMaxMeanLength) {
|
|
21
|
+
findings.push(
|
|
22
|
+
finding(name, paragraph.startLine, paragraph.sentences[0].text, `문장 ${paragraph.sentenceCount}개에 인과 표지가 하나도 없다. 사실만 나열한 문단일 수 있다. 그래서, 때문에 로 잇는다`, null, NOTICE, PARAGRAPH, paragraph.index),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return findings;
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { PARAGRAPH, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "paraFragment";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
8
|
+
* @param {import("../../config/settings.js").Config} config
|
|
9
|
+
*/
|
|
10
|
+
export function run(doc, config) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const section of doc.sections) {
|
|
13
|
+
/** @type {import("../../fingerprint/build.js").ParagraphPrint[]} */
|
|
14
|
+
let run = [];
|
|
15
|
+
for (const paragraph of section.paragraphs) {
|
|
16
|
+
const short = paragraph.sentenceCount <= 2;
|
|
17
|
+
if (short && (!run.length || paragraph.followsProseDirectly)) run.push(paragraph);
|
|
18
|
+
else if (short) run = [paragraph];
|
|
19
|
+
else run = [];
|
|
20
|
+
if (run.length === config.fragmentRun) {
|
|
21
|
+
const first = run[0];
|
|
22
|
+
findings.push(
|
|
23
|
+
finding(name, first.startLine, first.sentences.length ? first.sentences[0].text : "", `${run.length}개 문단이 연달아 한두 문장씩이다. 화제가 같으면 한 문단으로 묶는다. 줄바꿈은 화제가 바뀌는 자리에만`, null, "error", PARAGRAPH, first.index),
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { NOTICE, PARAGRAPH, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "topicBreak";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
8
|
+
* @param {import("../../config/settings.js").Config} config
|
|
9
|
+
*/
|
|
10
|
+
export function run(doc, config) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const section of doc.sections) {
|
|
13
|
+
const paragraphs = section.paragraphs;
|
|
14
|
+
for (let i = 0; i + 1 < paragraphs.length; i++) {
|
|
15
|
+
const previous = paragraphs[i];
|
|
16
|
+
const paragraph = paragraphs[i + 1];
|
|
17
|
+
if (paragraph.overlapWithPrevious !== 0) continue;
|
|
18
|
+
if (Math.min(previous.sentenceCount, paragraph.sentenceCount) < config.topicBreakMinSentences) continue;
|
|
19
|
+
findings.push(
|
|
20
|
+
finding(name, paragraph.startLine, paragraph.sentences[0].text, "앞 문단과 화제어가 하나도 겹치지 않는다. 앞에서 만든 것의 이름을 첫 문장에서 다시 부른다", null, NOTICE, PARAGRAPH, paragraph.index),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return findings;
|
|
25
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 규칙 등록부. 규칙 하나는 파일 하나이고 `name` 과 `run(doc, config)` 을 내보낸다. 파이썬은 폴더를 걸어 찾고
|
|
4
|
+
* 여기는 목록으로 든다. test/rules.test.js 가 목록과 폴더와 ruleDocs.json 이 같은지 본다.
|
|
5
|
+
* 기술서 (왜, 어디서, 고치기, 안 잡는 것) 는 파이썬 docstring 의 투영 data/ruleDocs.json 에서 읽는다.
|
|
6
|
+
*/
|
|
7
|
+
import { enabled } from "../config/settings.js";
|
|
8
|
+
import { loadRuleDocs } from "../data/load.js";
|
|
9
|
+
import * as countMismatch from "./document/countMismatch.js";
|
|
10
|
+
import * as keywordMissing from "./document/keywordMissing.js";
|
|
11
|
+
import * as noQuestion from "./document/noQuestion.js";
|
|
12
|
+
import * as promiseRecall from "./document/promiseRecall.js";
|
|
13
|
+
import * as readerAbsent from "./document/readerAbsent.js";
|
|
14
|
+
import * as duplicateBlock from "./code/duplicateBlock.js";
|
|
15
|
+
import * as firstResultDistance from "./code/firstResultDistance.js";
|
|
16
|
+
import * as inputFileSource from "./code/inputFileSource.js";
|
|
17
|
+
import * as installImport from "./code/installImport.js";
|
|
18
|
+
import * as platformApi from "./code/platformApi.js";
|
|
19
|
+
import * as confusable from "./orthography/confusable.js";
|
|
20
|
+
import * as spacing from "./orthography/spacing.js";
|
|
21
|
+
import * as spelling from "./orthography/spelling.js";
|
|
22
|
+
import * as factListParagraph from "./paragraph/factListParagraph.js";
|
|
23
|
+
import * as paraFragment from "./paragraph/paraFragment.js";
|
|
24
|
+
import * as topicBreak from "./paragraph/topicBreak.js";
|
|
25
|
+
import * as cliche from "./sentence/cliche.js";
|
|
26
|
+
import * as hardWord from "./sentence/hardWord.js";
|
|
27
|
+
import * as connectorRepeat from "./sentence/connectorRepeat.js";
|
|
28
|
+
import * as danglingDeixis from "./sentence/danglingDeixis.js";
|
|
29
|
+
import * as dash from "./sentence/dash.js";
|
|
30
|
+
import * as deixis from "./sentence/deixis.js";
|
|
31
|
+
import * as doubleNegative from "./sentence/doubleNegative.js";
|
|
32
|
+
import * as doublePassive from "./sentence/doublePassive.js";
|
|
33
|
+
import * as ellipsis from "./sentence/ellipsis.js";
|
|
34
|
+
import * as endingRepeat from "./sentence/endingRepeat.js";
|
|
35
|
+
import * as euiChain from "./sentence/euiChain.js";
|
|
36
|
+
import * as fillerOpener from "./sentence/fillerOpener.js";
|
|
37
|
+
import * as imperativePeriod from "./sentence/imperativePeriod.js";
|
|
38
|
+
import * as japaneseLoan from "./sentence/japaneseLoan.js";
|
|
39
|
+
import * as longSentence from "./sentence/longSentence.js";
|
|
40
|
+
import * as negationRedefine from "./sentence/negationRedefine.js";
|
|
41
|
+
import * as nounPile from "./sentence/nounPile.js";
|
|
42
|
+
import * as redundantPair from "./sentence/redundantPair.js";
|
|
43
|
+
import * as translationese from "./sentence/translationese.js";
|
|
44
|
+
import * as emojiBullet from "./structure/emojiBullet.js";
|
|
45
|
+
import * as headingSentence from "./structure/headingSentence.js";
|
|
46
|
+
import * as headingSkip from "./structure/headingSkip.js";
|
|
47
|
+
import * as headingUniform from "./structure/headingUniform.js";
|
|
48
|
+
import * as introLong from "./structure/introLong.js";
|
|
49
|
+
import * as sectionNoProse from "./structure/sectionNoProse.js";
|
|
50
|
+
import * as sectionResult from "./structure/sectionResult.js";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @typedef {object} Rule
|
|
54
|
+
* @property {string} name
|
|
55
|
+
* @property {(doc: import("../fingerprint/build.js").DocumentPrint, config: import("../config/settings.js").Config) => import("./finding.js").Finding[]} run
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** @type {Rule[]} */
|
|
59
|
+
export const RULES = [
|
|
60
|
+
cliche,
|
|
61
|
+
connectorRepeat,
|
|
62
|
+
danglingDeixis,
|
|
63
|
+
dash,
|
|
64
|
+
deixis,
|
|
65
|
+
doubleNegative,
|
|
66
|
+
doublePassive,
|
|
67
|
+
ellipsis,
|
|
68
|
+
endingRepeat,
|
|
69
|
+
euiChain,
|
|
70
|
+
fillerOpener,
|
|
71
|
+
hardWord,
|
|
72
|
+
imperativePeriod,
|
|
73
|
+
japaneseLoan,
|
|
74
|
+
longSentence,
|
|
75
|
+
negationRedefine,
|
|
76
|
+
nounPile,
|
|
77
|
+
redundantPair,
|
|
78
|
+
translationese,
|
|
79
|
+
factListParagraph,
|
|
80
|
+
paraFragment,
|
|
81
|
+
topicBreak,
|
|
82
|
+
emojiBullet,
|
|
83
|
+
headingSentence,
|
|
84
|
+
headingSkip,
|
|
85
|
+
headingUniform,
|
|
86
|
+
introLong,
|
|
87
|
+
sectionNoProse,
|
|
88
|
+
sectionResult,
|
|
89
|
+
countMismatch,
|
|
90
|
+
keywordMissing,
|
|
91
|
+
noQuestion,
|
|
92
|
+
promiseRecall,
|
|
93
|
+
readerAbsent,
|
|
94
|
+
spelling,
|
|
95
|
+
spacing,
|
|
96
|
+
confusable,
|
|
97
|
+
inputFileSource,
|
|
98
|
+
installImport,
|
|
99
|
+
duplicateBlock,
|
|
100
|
+
firstResultDistance,
|
|
101
|
+
platformApi,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const BY_NAME = new Map(RULES.map((rule) => [rule.name, rule]));
|
|
105
|
+
|
|
106
|
+
/** 이름 순. */
|
|
107
|
+
export function ruleNames() {
|
|
108
|
+
return [...BY_NAME.keys()].sort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @param {string} name */
|
|
112
|
+
export function ruleDoc(name) {
|
|
113
|
+
const docs = loadRuleDocs();
|
|
114
|
+
if (!(name in docs) || !BY_NAME.has(name)) throw new Error(`모르는 규칙: ${name}. hanlint rules 로 목록을 본다`);
|
|
115
|
+
return docs[name];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** @param {string} name */
|
|
119
|
+
export function ruleSummary(name) {
|
|
120
|
+
return ruleDoc(name).split("\n")[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 인라인 제어가 그 줄에서 그 규칙을 껐는가. `*` 는 전부다.
|
|
125
|
+
* @param {import("./finding.js").Finding} f
|
|
126
|
+
* @param {[string, number, number][]} disabled
|
|
127
|
+
*/
|
|
128
|
+
export function isDisabledAt(f, disabled) {
|
|
129
|
+
return disabled.some(([name, start, end]) => (name === "*" || name === f.rule) && start <= f.line && f.line <= end);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {import("../fingerprint/build.js").DocumentPrint} doc
|
|
134
|
+
* @param {import("../config/settings.js").Config} config
|
|
135
|
+
* @returns {import("./finding.js").Finding[]}
|
|
136
|
+
*/
|
|
137
|
+
export function runAll(doc, config) {
|
|
138
|
+
/** @type {import("./finding.js").Finding[]} */
|
|
139
|
+
const findings = [];
|
|
140
|
+
for (const name of ruleNames()) {
|
|
141
|
+
if (!enabled(config, name)) continue;
|
|
142
|
+
const rule = /** @type {Rule} */ (BY_NAME.get(name));
|
|
143
|
+
for (const f of rule.run(doc, config)) if (!isDisabledAt(f, doc.disabled)) findings.push(f);
|
|
144
|
+
}
|
|
145
|
+
findings.sort((a, b) => a.line - b.line || (a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
|
|
4
|
+
export const name = "cliche";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
return dictionaryFindings(doc, "cliches", name);
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "connectorRepeat";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const paragraph of doc.paragraphs) {
|
|
10
|
+
/** @type {string | null} */
|
|
11
|
+
let previous = null;
|
|
12
|
+
for (const sentence of paragraph.sentences) {
|
|
13
|
+
const current = sentence.connectorStart;
|
|
14
|
+
if (current && current === previous) {
|
|
15
|
+
findings.push(
|
|
16
|
+
finding(name, sentence.line, sentence.text, `\`${current}\` 로 시작하는 문장이 연달아 온다. 앞에서 만든 것의 이름을 불러 잇는다`, null, "error", SENTENCE, sentence.index),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
previous = current;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return findings;
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { overlap } from "../../fingerprint/topics.js";
|
|
3
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "danglingDeixis";
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const sentence of doc.sentences) {
|
|
11
|
+
if (!sentence.deixis.length) continue;
|
|
12
|
+
const previous = sentence.index > 0 ? doc.sentences[sentence.index - 1] : null;
|
|
13
|
+
if (previous !== null && overlap(previous.topics, sentence.topics) > 0) continue;
|
|
14
|
+
findings.push(
|
|
15
|
+
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\` 이 가리킬 것이 앞 문장에 없다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return findings;
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { DOCUMENT, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "dash";
|
|
5
|
+
// 리터럴로 쓰면 이 파일이 자기 게이트에 걸린다. 코드포인트로 만든다.
|
|
6
|
+
const DASHES = new RegExp(`[${String.fromCharCode(0x2013)}${String.fromCharCode(0x2014)}]`);
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const block of doc.blocks) {
|
|
12
|
+
block.text.split("\n").forEach((lineText, offset) => {
|
|
13
|
+
if (DASHES.test(lineText)) {
|
|
14
|
+
findings.push(
|
|
15
|
+
finding(name, block.startLine + offset, lineText.trim(), "긴 줄표다. 부연은 마침표로 끊거나 괄호로, 범위는 물결표 ~ 로 쓴다", null, "error", DOCUMENT, block.index),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return findings;
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { overlap } from "../../fingerprint/topics.js";
|
|
3
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
4
|
+
|
|
5
|
+
export const name = "deixis";
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const sentence of doc.sentences) {
|
|
11
|
+
if (!sentence.deixis.length || sentence.index === 0) continue;
|
|
12
|
+
const previous = doc.sentences[sentence.index - 1];
|
|
13
|
+
if (overlap(previous.topics, sentence.topics) === 0) continue;
|
|
14
|
+
findings.push(
|
|
15
|
+
finding(name, sentence.line, sentence.text, `\`${sentence.deixis[0]}\` 은 독자가 스크롤을 되돌려야 하는 지시어다. 가리키는 파일, 값, 코드의 이름을 쓴다`, null, "error", SENTENCE, sentence.index),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return findings;
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "doubleNegative";
|
|
5
|
+
const DOUBLE_NEGATIVE = /(하지 않으면 안 (?:된다|됩니다)|하지 않을 수 없(?:다|습니다)|지 않으면 안 (?:된다|됩니다)|지 않을 수 없(?:다|습니다))/;
|
|
6
|
+
/** @type {Record<string, string>} */
|
|
7
|
+
const FIXES = {
|
|
8
|
+
"하지 않으면 안 된다": "해야 한다",
|
|
9
|
+
"하지 않으면 안 됩니다": "해야 합니다",
|
|
10
|
+
"하지 않을 수 없다": "한다",
|
|
11
|
+
"하지 않을 수 없습니다": "합니다",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
15
|
+
export function run(doc) {
|
|
16
|
+
const findings = [];
|
|
17
|
+
for (const sentence of doc.sentences) {
|
|
18
|
+
const match = DOUBLE_NEGATIVE.exec(sentence.text);
|
|
19
|
+
if (!match) continue;
|
|
20
|
+
const replacement = FIXES[match[1]];
|
|
21
|
+
const start = /** @type {number} */ (match.index);
|
|
22
|
+
const fix = replacement ? sentence.text.slice(0, start) + replacement + sentence.text.slice(start + match[0].length) : null;
|
|
23
|
+
findings.push(
|
|
24
|
+
finding(name, sentence.line, sentence.text, `\`${match[1]}\` 는 이중 부정이다. 긍정으로 쓴다`, fix, "error", SENTENCE, sentence.index, replacement ? match[1] : null, replacement ?? null),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "doublePassive";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
for (const sentence of doc.sentences) {
|
|
10
|
+
if (sentence.passives.length) {
|
|
11
|
+
findings.push(
|
|
12
|
+
finding(name, sentence.line, sentence.text, `\`${sentence.passives[0]}\` 는 피동에 어지다 를 또 붙인 이중 피동이다. 하나만 남긴다`, null, "error", SENTENCE, sentence.index),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return findings;
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "ellipsis";
|
|
5
|
+
// 괄호 안의 점 셋 (`OVER (...)`) 은 인라인 코드의 생략 표기라 뺀다.
|
|
6
|
+
const ELLIPSIS = /(?<![(\[])(…|\.{3,})(?![)\]])/;
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const sentence of doc.sentences) {
|
|
12
|
+
if (ELLIPSIS.test(sentence.text)) {
|
|
13
|
+
findings.push(finding(name, sentence.line, sentence.text, "말줄임표로 말끝을 흐리면 독자가 결론을 받아 가지 못한다. 문장을 끝까지 쓴다", null, "error", SENTENCE, sentence.index));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return findings;
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { NOTICE, SENTENCE, finding } from "../finding.js";
|
|
3
|
+
import { runsOf } from "../shared/runs.js";
|
|
4
|
+
|
|
5
|
+
export const name = "endingRepeat";
|
|
6
|
+
const COUNTED_ENDINGS = new Set(["니다", "다", "것이다", "요", "죠"]);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
10
|
+
* @param {import("../../config/settings.js").Config} config
|
|
11
|
+
*/
|
|
12
|
+
export function run(doc, config) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const section of doc.sections) {
|
|
15
|
+
const sentences = section.paragraphs.flatMap((p) => p.sentences);
|
|
16
|
+
const endings = sentences.map((s) => (COUNTED_ENDINGS.has(s.ending) ? s.ending : `__${s.index}`));
|
|
17
|
+
for (const [start, length, ending] of runsOf(endings, config.endingRun)) {
|
|
18
|
+
const first = sentences[start];
|
|
19
|
+
findings.push(
|
|
20
|
+
finding(name, first.line, first.text, `\`${ending}\` 로 끝나는 문장이 ${length}개 이어진다. 질문, 인과, 행동 동사로 리듬을 바꾼다`, null, NOTICE, SENTENCE, first.index),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return findings;
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "euiChain";
|
|
5
|
+
const ADJACENT = /[가-힣]+의\s+[가-힣]+의(?=[\s,.)\]]|$)/;
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const sentence of doc.sentences) {
|
|
11
|
+
if (sentence.euiCount >= 3 || (sentence.euiCount >= 2 && ADJACENT.test(sentence.text))) {
|
|
12
|
+
findings.push(
|
|
13
|
+
finding(name, sentence.line, sentence.text, `한 문장에 \`의\` 가 ${sentence.euiCount}번이다. 명사를 쌓은 자리라 관계가 표시되지 않는다. 동사로 편다`, null, "error", SENTENCE, sentence.index),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return findings;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "fillerOpener";
|
|
5
|
+
const FILLER = /^(다음으로|이어서|마지막에는|마지막으로)(?=[\s,])/;
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const sentence of doc.sentences) {
|
|
11
|
+
const match = FILLER.exec(sentence.text);
|
|
12
|
+
if (!match) continue;
|
|
13
|
+
findings.push(
|
|
14
|
+
finding(name, sentence.line, sentence.text, `\`${match[1]}\` 만 붙여 잇는 문장이다. 앞에서 만든 파일이나 값을 이름으로 다시 부르고 다음 행동을 붙인다`, null, "error", SENTENCE, sentence.index),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return findings;
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { NOTICE } from "../finding.js";
|
|
3
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
4
|
+
|
|
5
|
+
export const name = "hardWord";
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
return dictionaryFindings(doc, "easyWords", name, NOTICE);
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "imperativePeriod";
|
|
5
|
+
const IMPERATIVE_PERIOD = /(세요|십시오|합시다|봅시다|하자|해라)\.(?=\s|$)/;
|
|
6
|
+
|
|
7
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
8
|
+
export function run(doc) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
for (const sentence of doc.sentences) {
|
|
11
|
+
const match = IMPERATIVE_PERIOD.exec(sentence.text);
|
|
12
|
+
if (!match) continue;
|
|
13
|
+
const start = /** @type {number} */ (match.index);
|
|
14
|
+
const fix = sentence.text.slice(0, start) + match[1] + sentence.text.slice(start + match[0].length);
|
|
15
|
+
findings.push(
|
|
16
|
+
finding(name, sentence.line, sentence.text, `\`${match[1]}\` 처럼 명령형과 청유형 뒤에는 마침표를 붙이지 않는다`, fix, "error", SENTENCE, sentence.index, match[0], match[1]),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return findings;
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { dictionaryFindings } from "../shared/dictionaryRule.js";
|
|
3
|
+
|
|
4
|
+
export const name = "japaneseLoan";
|
|
5
|
+
|
|
6
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
7
|
+
export function run(doc) {
|
|
8
|
+
return dictionaryFindings(doc, "japaneseLoan", name);
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { NOTICE, SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "longSentence";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("../../fingerprint/build.js").DocumentPrint} doc
|
|
8
|
+
* @param {import("../../config/settings.js").Config} config
|
|
9
|
+
*/
|
|
10
|
+
export function run(doc, config) {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const sentence of doc.sentences) {
|
|
13
|
+
if (sentence.length > config.longSentenceMax) {
|
|
14
|
+
findings.push(
|
|
15
|
+
finding(name, sentence.line, sentence.text, `어절 ${sentence.length}개다. 상한은 ${config.longSentenceMax}. 마침표로 끊거나 나열이면 목록으로 꺼낸다`, null, NOTICE, SENTENCE, sentence.index),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return findings;
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SENTENCE, finding } from "../finding.js";
|
|
3
|
+
|
|
4
|
+
export const name = "negationRedefine";
|
|
5
|
+
const NEGATION = /단순(?:한|히)\s?.{0,15}?(?:이|가)\s?아(?:닙니다|니다|니에요|니죠|닌)/;
|
|
6
|
+
const COPULA = /(입니다|이다|이에요|예요)[.!]?$/;
|
|
7
|
+
|
|
8
|
+
/** @param {import("../../fingerprint/build.js").DocumentPrint} doc */
|
|
9
|
+
export function run(doc) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
for (const paragraph of doc.paragraphs) {
|
|
12
|
+
const sentences = paragraph.sentences;
|
|
13
|
+
for (let i = 0; i + 1 < sentences.length; i++) {
|
|
14
|
+
const current = sentences[i];
|
|
15
|
+
const following = sentences[i + 1];
|
|
16
|
+
if (NEGATION.test(current.text) && COPULA.test(following.text.trim())) {
|
|
17
|
+
findings.push(finding(name, current.line, current.text, "단순한 X 가 아닙니다 뒤에 Y 입니다 로 재정의하는 공식이다. 앞 문장을 지우고 Y 를 바로 쓴다", null, "error", SENTENCE, current.index));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return findings;
|
|
22
|
+
}
|