From b5ea0c1b416f643a181830031960be370be6c1c7 Mon Sep 17 00:00:00 2001 From: Koji Shimba Date: Mon, 15 Jun 2026 15:20:37 +0000 Subject: [PATCH] Rework comment subcommand to the English-first / [Ja] marker format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old "[Ja] marker correctness" model (conditions 1-4) with the English-first / Japanese-marker rule set (conditions 1-8): a Japanese block must be Japanese (1), the English block must not be Japanese (2), an inline Japanese marker preceded by Japanese (3), a Japanese block needs an English block above it (4), the inline end-of-line form is banned (5), the [Ja] marker must be unique (6), the [En] marker is obsolete (7), and a blank line must separate the two blocks (8). Full mode reports only the near-zero-false-positive conditions (1, 2, 3, 7) tree-wide via isFullModeCond; the rest are diff-only. Inline markers are detected only after a sentence end so prose mentions are ignored. The line-classification machinery (classifyLine, isEnglishText, the lineKind kinds, reLatin, reURLOnly) is replaced by commentBody, markerKind, and the inline-marker helpers, and the tests are rewritten to cover every condition. [Ja] comment サブコマンドを「英語先頭 / [Ja] マーカー」形式に作り直す 旧来の「[Ja] マーカーの正しさ」モデル (条件 1-4) を、英語先頭 / 日本語マーカーの 規則 (条件 1-8) に置き換える: 日本語ブロックは日本語 (1)、英語ブロックは日本語を 含まない (2)、前に日本語があるインライン日本語マーカー (3)、日本語ブロックの上に 英語ブロックが必要 (4)、インライン (行末) 形式は禁止 (5)、[Ja] マーカーは一意 (6)、 [En] マーカーは廃止 (7)、2 つのブロックの間に空行が必要 (8)。 全体モードは誤検出がほぼ無い条件 (1, 2, 3, 7) のみを isFullModeCond でツリー全体に 報告し、残りは差分モード限定。インラインマーカーは文末の後だけ検出し、地の文中の 言及は無視する。行分類の仕組み (classifyLine・isEnglishText・lineKind の各種別・ reLatin・reURLOnly) を commentBody・markerKind とインラインマーカー補助関数に置き 換え、テストも全条件を網羅するよう書き直した。 --- internal/comment/comment.go | 475 ++++++++++++++++++------------- internal/comment/comment_test.go | 444 +++++++++++++++++++---------- 2 files changed, 577 insertions(+), 342 deletions(-) diff --git a/internal/comment/comment.go b/internal/comment/comment.go index bd5e513..e322dd4 100644 --- a/internal/comment/comment.go +++ b/internal/comment/comment.go @@ -1,41 +1,43 @@ -// Package comment implements the comment subcommand of koryluslint, which -// checks that bilingual code comments use the [Ja] marker correctly. The -// marker introduces the Japanese translation block, which must sit below a -// corresponding English block and appear at most once per comment group. -// When the English block spans multiple lines, one blank comment line must -// separate it from the marker; when it is a single line, no blank line is -// allowed. The check targets the recurring *misuse* of the marker rather than -// enforcing full bilingual coverage, which keeps false positives near zero; -// for the same reason the blank-line check skips comment groups that contain -// lines it cannot classify as English or Japanese (separators, code examples, -// URL-only lines, and the like). +// Package comment implements the comment subcommand of koryluslint, which checks +// that bilingual code comments follow the English-first / Japanese-marker format. +// A bilingual comment is an English block (unmarked, written first; a doc comment +// on an exported symbol begins with the symbol name per Go convention) followed +// by a Japanese block led by the Japanese marker at the start of its line. There +// is no English marker; an obsolete English marker is reported so it can be +// removed. The inline (end-of-line) form is not allowed. The check targets +// recurring misuses rather than enforcing full bilingual coverage, which keeps +// false positives near zero. // // Two modes: // -// comment [paths...] checks the [Ja]-on-non-Japanese rule across -// the whole tree (default: "."). -// comment -base= [paths...] also checks the marker-placement and -// blank-line rules, limited to lines added -// since . +// comment [paths...] checks the near-zero-false-positive rules +// tree-wide (default: "."): a Japanese block +// that is not Japanese, an English block whose +// line nearest the marker is Japanese, an inline +// Japanese marker with Japanese before it, and an +// obsolete English marker. +// comment -base= [paths...] also checks the English-required, inline-ban, +// duplicate, and required-blank-separator rules, +// limited to lines added since . // // .go files are parsed via go/parser so that "//" inside string literals is // never mistaken for a comment; .templ files (not valid Go) are scanned by line. // -// [Ja] comment パッケージは koryluslint の comment サブコマンドを実装し、 -// コードコメントの英日併記で `[Ja]` マーカーが正しく使われているかをチェックする。 -// `[Ja]` は日本語訳ブロックの冒頭を示すマーカーで、対応する英語ブロックの下に置き、 -// 1 コメント群に 1 つだけ付ける。英語ブロックが複数行のときはマーカーとの間に -// 空行 (コメント記号のみの行) を 1 行置き、1 行のときは空行を置かない。 -// 全併記の強制ではなく、再発している「マーカーの誤用」のみを対象にすることで -// 誤検出をほぼゼロに保つ。同じ理由で、空行の検査は英語とも日本語とも判定できない行 -// (区切り線・コード例・URL のみの行など) を含むコメント群をスキップする。 +// [Ja] comment パッケージは koryluslint の comment サブコマンドを実装し、コードコメントの +// 英日併記が「英語先頭 / 日本語マーカー」形式に従っているかをチェックする。併記コメントは +// 英語ブロック (無マーカーで先に書く。exported シンボルの doc コメントは Go 慣習どおり +// シンボル名で始める) の後に、行頭の日本語マーカーで始まる日本語ブロックを置く。英語マーカー +// は無く、廃止された英語マーカーは見つかれば除去できるよう報告する。インライン (行末) 形式は +// 使わない。全併記の強制ではなく再発する誤用を対象にし、誤検出をほぼゼロに保つ。 // // モードは 2 つ: // -// comment [paths...] 「[Ja] が日本語を含まない行に付く」誤用を -// ツリー全体で検査する (既定は ".")。 -// comment -base= [paths...] マーカー配置・空行の規則も検査する。 -// 以降に追加された行に限定する。 +// comment [paths...] 誤検出がほぼ無い規則をツリー全体で検査する (既定は +// ".")。日本語でない日本語ブロック、マーカーに最も近い行 +// が日本語の英語ブロック、前に日本語があるインライン日本語 +// マーカー、廃止された英語マーカー。 +// comment -base= [paths...] 英語必須・インライン禁止・重複・ブロック間空行 (必須) の +// 規則も検査する。 以降に追加された行に限定する。 // // .go は go/parser で解析し、文字列リテラル中の "//" をコメントと誤認しない。 // .templ (Go として不正) は行単位で走査する。 @@ -58,26 +60,24 @@ import ( "sort" "strconv" "strings" + "unicode/utf8" "github.com/korylus/tools/internal/cli" ) var ( // reJapanese matches any Hiragana, Katakana, or Han (kanji) rune. + // // [Ja] reJapanese はひらがな・カタカナ・漢字のいずれかにマッチする。 reJapanese = regexp.MustCompile(`[\p{Hiragana}\p{Katakana}\p{Han}]`) - // reLatin matches an ASCII Latin letter. - // [Ja] reLatin は ASCII のラテン文字にマッチする。 - reLatin = regexp.MustCompile(`[A-Za-z]`) // reGenerated matches the standard "generated; do not edit" header. + // // [Ja] reGenerated は「生成物・編集禁止」の定型ヘッダーにマッチする。 reGenerated = regexp.MustCompile(`^//\s*Code generated .* DO NOT EDIT\.$`) - // reURLOnly matches a line whose whole content is a single URL. - // [Ja] reURLOnly は本文全体が 1 つの URL である行にマッチする。 - reURLOnly = regexp.MustCompile(`^https?://\S+$`) ) // commentLine is a single physical line of a comment with its 1-based line number. +// // [Ja] commentLine はコメントの 1 物理行と、その 1 始まりの行番号。 type commentLine struct { line int @@ -85,6 +85,7 @@ type commentLine struct { } // finding is one detected violation. +// // [Ja] finding は検出した違反 1 件。 type finding struct { file string @@ -93,31 +94,20 @@ type finding struct { msg string } -// lineKind classifies a non-marker comment line for the blank-line check -// (condition 4). -// -// [Ja] lineKind は空行検査 (条件 4) のために非マーカー行を分類した種別。 -type lineKind int - const ( - kindNone lineKind = iota // no line seen yet. [Ja] まだ行が無い - kindBlank // comment leader only, empty content. [Ja] コメント記号のみで本文が空 - kindEnglish // English text. [Ja] 英文 - kindJapanese // Japanese text. [Ja] 日本語文 - kindOther // neither English nor Japanese (separator, code, URL). [Ja] 英文とも日本語とも判定できない - kindMarker // a [Ja] marker line. [Ja] マーカー行 -) - -const ( - msgCond1 = "[Ja] marker on a line with no Japanese text; it must lead the Japanese translation, not the English block / [Ja] マーカーが日本語を含まない行に付いている" - msgCond2 = "[Ja] marker has no English block above it; write the English block first, then [Ja] / [Ja] の上に対応する英語ブロックが無い" - msgCond3 = "[Ja] marker appears more than once in one comment block; mark only the first line of the Japanese block / [Ja] マーカーが 1 コメント群に複数ある" - msgCond4a = "[Ja] marker after a multi-line English block needs one blank comment line right above it / [Ja] 英語ブロックが複数行のときはマーカーの直前に空行が必要" - msgCond4b = "[Ja] marker after a one-line English comment must not have a blank line above it / [Ja] 英語ブロックが 1 行のときはマーカーの直前に空行を入れない" + msgCond1 = "the lines under [Ja] are not Japanese; a [Ja] block must be the Japanese translation / [Ja] [Ja] ブロックが日本語になっていない" + msgCond2 = "the English block (the lines before [Ja]) contains Japanese; keep it English / [Ja] 英語ブロック ([Ja] より前) に日本語が入っている" + msgCond3 = "inline [Ja] marker with Japanese before it; markers must lead their own line / [Ja] インラインの [Ja] マーカーの前に日本語がある (マーカーは行頭に置く)" + msgCond4 = "[Ja] block has no English block before it; lead with an English block, then [Ja] / [Ja] [Ja] ブロックの前に英語ブロックが無い (英語先頭→[Ja])" + msgCond5 = "inline (end-of-line) bilingual comment is not allowed; put the English block and [Ja] on their own lines above the code / [Ja] インライン (行末) の併記コメントは不可 (英語ブロックと [Ja] を行頭に置く)" + msgCond6 = "duplicate [Ja] marker in one comment group; use a single [Ja] / [Ja] [Ja] マーカーが 1 群に重複 ([Ja] は 1 つ)" + msgCond7 = "obsolete [En] marker; the English block is unmarked now (English first, then [Ja]) / [Ja] [En] マーカーは廃止 (英語ブロックは無マーカー、英語先頭→[Ja])" + msgCond8 = "no blank line between the English and Japanese blocks; put one blank comment line before [Ja] / [Ja] 英語ブロックと日本語ブロックの間に空行が無い ([Ja] の前に空行を 1 行入れる)" ) // Run is the entry point of the comment subcommand. args is what remains after // the subcommand name, and the return value is the process exit code. +// // [Ja] Run は comment サブコマンドのエントリポイント。args はサブコマンド名を除いた // 残りの引数で、戻り値はプロセスの終了コード。 func Run(args []string, stdout, stderr io.Writer) int { @@ -126,6 +116,7 @@ func Run(args []string, stdout, stderr io.Writer) int { opts := cli.RegisterCommon(fs) if err := fs.Parse(args); err != nil { // A -h/--help request is a success, not a usage error. + // // [Ja] -h/--help の要求はエラーではなく成功扱いにする。 if errors.Is(err, flag.ErrHelp) { return 0 @@ -149,20 +140,24 @@ func Run(args []string, stdout, stderr io.Writer) int { for _, f := range findings { fmt.Fprintf(stdout, "%s:%d: %s\n", f.file, f.line, f.msg) } - fmt.Fprintf(stderr, "\nkoryluslint comment: %d bilingual [Ja] marker violation(s)\n", len(findings)) + fmt.Fprintf(stderr, "\nkoryluslint comment: %d bilingual marker violation(s)\n", len(findings)) return 1 } // collectFindings walks the roots and returns the violations to report. In full -// mode only condition 1 (marker on a non-Japanese line) is reported, anywhere in -// the tree. In diff mode all conditions are reported, but only for lines added -// since base. Recoverable problems (an uncomputable diff, a single unparsable -// file) are written to stderr and skipped rather than failing the run. +// mode only the near-zero-false-positive conditions (1, 2, 3, 7) are reported, +// anywhere in the tree: a Japanese block that is not Japanese, an English block +// that contains Japanese, an inline Japanese marker with Japanese before it, and +// an obsolete English marker. In diff mode every condition is reported, but only +// for lines added since base. Recoverable problems (an uncomputable diff, a +// single unparsable file) are written to stderr and skipped rather than failing +// the run. // -// [Ja] collectFindings は roots を走査し、報告すべき違反を返す。全体モードでは条件 1 -// (日本語を含まない行のマーカー) のみをツリー全体で報告する。差分モードでは全条件を -// 報告するが、base 以降に追加された行に限定する。回復可能な問題 (差分を計算できない、 -// 個別ファイルの解析失敗) は stderr に出してスキップし、実行を失敗させない。 +// [Ja] collectFindings は roots を走査し、報告すべき違反を返す。全体モードでは誤検出が +// ほぼ無い条件 (1, 2, 3, 7) のみをツリー全体で報告する。日本語でない日本語ブロック、日本語 +// が入った英語ブロック、前に日本語があるインライン日本語マーカー、廃止された英語マーカー。 +// 差分モードでは全条件を報告するが、base 以降に追加された行に限定する。回復可能な問題 (差分 +// を計算できない、個別ファイルの解析失敗) は stderr に出してスキップし、実行を失敗させない。 func collectFindings(roots []string, base string, stderr io.Writer) ([]finding, error) { diffMode := base != "" @@ -173,6 +168,7 @@ func collectFindings(roots []string, base string, stderr io.Writer) ([]finding, if err != nil { // Be lenient: if the diff cannot be computed, skip the diff-scoped // checks rather than failing the build. + // // [Ja] 差分を計算できない場合はビルドを失敗させず、差分限定の検査を // スキップする。 fmt.Fprintf(stderr, "koryluslint comment: skipping diff checks: %v\n", err) @@ -213,7 +209,7 @@ func collectFindings(roots []string, base string, stderr io.Writer) ([]finding, if aerr != nil || added[abs] == nil || !added[abs][f.line] { continue } - } else if f.cond != 1 { + } else if !isFullModeCond(f.cond) { continue } all = append(all, f) @@ -238,174 +234,256 @@ func collectFindings(roots []string, base string, stderr io.Writer) ([]finding, return all, nil } -// checkGroup evaluates one comment group against the [Ja] marker rules. -// Only lines whose comment content *begins* with the [Ja] marker count as -// marker lines; a mid-sentence mention of "[Ja]" in English prose (such as this -// tool's own docs) is ignored. At most one finding per marker line is produced -// (on the same line condition 1 supersedes condition 2, which supersedes -// condition 4); condition 3 is reported independently. Condition 4 (the blank -// line between the English block and the marker) is evaluated only while every -// line above the marker classifies as English, Japanese, or blank; one -// unclassifiable line (separator, code example, URL-only) disables it for the -// rest of the group, and so does a second marker (condition 3 already reports -// the broken structure, where any blank-line judgement would be a guess). +// isFullModeCond reports whether a condition is checked tree-wide (full mode). +// These are the near-zero-false-positive conditions; the rest are diff-only. // -// [Ja] checkGroup は 1 コメント群を [Ja] マーカー規則で評価する。 -// コメント本文が [Ja] マーカーで「始まる」行だけをマーカー行とみなし、英語の地の文中で -// "[Ja]" に言及しているだけの行 (本ツール自身の説明など) は無視する。1 マーカー行あたり -// 最大 1 件 (同じ行では条件 1 が条件 2 に、条件 2 が条件 4 に優先する)。条件 3 は独立に -// 報告する。条件 4 (英語ブロックとマーカーの間の空行) は、マーカーより上の全行が -// 英文・日本語・空行のいずれかに分類できる間だけ評価し、分類できない行 (区切り線・ -// コード例・URL のみの行) が 1 つでもあれば群の残りでは無効にする。2 つ目以降の -// マーカーでも同様に無効にする (壊れた構造は条件 3 で報告済みで、そこへの空行判定は -// 当て推量になるため)。 +// [Ja] isFullModeCond は条件がツリー全体 (全体モード) で検査されるかを返す。これらは誤検出 +// がほぼ無い条件で、残りは差分モード限定。 +func isFullModeCond(cond int) bool { + switch cond { + case 1, 2, 3, 7: + return true + default: + return false + } +} + +// checkGroup evaluates one comment group against the English-first / [Ja] marker +// rules. A line counts as a marker line only when its content begins with a +// marker; a mid-sentence mention in prose is ignored. The Japanese block must be +// Japanese (condition 1) and must have an English block above it (condition 4), +// and a [Ja] marker must be unique (condition 6). In a bilingual group the line +// nearest above the first marker (skipping blanks) must not be Japanese +// (condition 2). On a non-marker line an inline (end-of-line) marker is detected +// by a marker that follows a sentence end and reported as condition 3 (Japanese +// before an inline Japanese marker) or, when the English side is fine, condition +// 5 (the inline form is banned). The English marker [En] is obsolete and is +// reported as condition 7 wherever it leads a line. A missing blank line between +// the two blocks is reported as condition 8. +// +// [Ja] checkGroup は 1 コメント群を「英語先頭 / [Ja] マーカー」規則で評価する。本文がマー +// カーで「始まる」行だけをマーカー行とみなし、地の文中の言及は無視する。日本語ブロックは +// 日本語 (条件 1) で、その上に英語ブロックが必要 (条件 4)、[Ja] マーカーは一意 (条件 6)。 +// 併記群では最初のマーカーの直上 (空行は飛ばす) の行が日本語であってはならない (条件 2)。非 +// マーカー行では、文末に続くマーカーをインライン (行末) マーカーとして検出し、条件 3 (イン +// ライン日本語マーカーの前に日本語) か、英語側が問題なければ条件 5 (インライン形式は禁止) +// として報告する。英語マーカー [En] は廃止で、行頭にあれば条件 7 として報告する。ブロック間 +// の空行が無いことは条件 8 として報告する。 func checkGroup(lines []commentLine) []finding { var fs []finding - markerCount := 0 - englishSeenAbove := false - // State for condition 4: the count of English lines above the marker, the - // kind of the previous line, and whether an unclassifiable line was seen. + // firstJa is the index of the first line-leading [Ja] marker, or -1. // - // [Ja] 条件 4 のための状態: マーカーより上の英文行の数、直前行の種別、 - // 分類できない行が出現したかどうか。 - englishAbove := 0 - prevKind := kindNone - unclassifiable := false - - for _, cl := range lines { - if !markerAtStart(cl.text) { - if isEnglishText(cl.text) { - englishSeenAbove = true - } - kind := classifyLine(cl.text) - switch kind { - case kindEnglish: - englishAbove++ - case kindOther: - unclassifiable = true - } - prevKind = kind - continue - } - - markerCount++ - if markerCount > 1 { - fs = append(fs, finding{line: cl.line, cond: 3, msg: msgCond3}) + // [Ja] firstJa は行頭 [Ja] マーカーの最初の位置 (無ければ -1)。 + firstJa := -1 + for i, cl := range lines { + if markerKind(cl.text) == "ja" { + firstJa = i + break } + } - switch { - case !hasJapanese(cl.text): - // Condition 1: the marker leads an English (or marker-only) line. - // [Ja] 条件 1: マーカーが英語 (またはマーカーのみ) の行を先導している。 - fs = append(fs, finding{line: cl.line, cond: 1, msg: msgCond1}) - case !englishSeenAbove: - // Condition 2: a marker line still needs an English block above it. - // [Ja] 条件 2: マーカー行でも、その上に英語ブロックが必要。 - fs = append(fs, finding{line: cl.line, cond: 2, msg: msgCond2}) - case unclassifiable || markerCount > 1: - // Skip condition 4: a line above defies classification, or the - // group has more than one marker (already reported as condition - // 3), so the blank-line judgement would be a guess. Prefer a - // false negative. + jaCount := 0 + englishBefore := false + for i, cl := range lines { + beforeJa := firstJa < 0 || i < firstJa + switch markerKind(cl.text) { + case "en": + // Condition 7: the English marker is obsolete; the English block is now unmarked. + // + // [Ja] 条件 7: 英語マーカーは廃止。英語ブロックは無マーカーにする。 + fs = append(fs, finding{line: cl.line, cond: 7, msg: msgCond7}) + if beforeJa { + englishBefore = true + } + case "ja": + jaCount++ + // Condition 1: the Japanese block must be Japanese. + // + // [Ja] 条件 1: 日本語ブロックは日本語でなければならない。 + if !hasJapanese(commentBody(cl.text)) { + fs = append(fs, finding{line: cl.line, cond: 1, msg: msgCond1}) + } + // Condition 4: the first Japanese block needs an English block above it. // - // [Ja] 条件 4 をスキップ: 上の行に分類できない行があるか、群に - // マーカーが複数あり (条件 3 で報告済み)、空行の判定が当て推量に - // なるため。偽陰性側に倒す。 - case prevKind == kindEnglish && englishAbove >= 2: - // Condition 4 (a): a multi-line English block runs straight into - // the marker without the separating blank line. + // [Ja] 条件 4: 最初の日本語ブロックの上に英語ブロックが必要。 + if i == firstJa && !englishBefore { + fs = append(fs, finding{line: cl.line, cond: 4, msg: msgCond4}) + } + // Condition 8: there must be a blank line between the English and + // Japanese blocks; the [Ja] block must not directly follow an English line. + // + // [Ja] 条件 8: 英語ブロックと日本語ブロックの間に空行を 1 行入れる ([Ja] が英語行の + // 直後に続いてはならない)。 + if i == firstJa && englishBefore && i > 0 && commentBody(lines[i-1].text) != "" { + fs = append(fs, finding{line: cl.line, cond: 8, msg: msgCond8}) + } + // Condition 6: the Japanese marker must be unique. // - // [Ja] 条件 4 (a): 複数行の英語ブロックが空行を挟まずマーカーに - // 連続している。 - fs = append(fs, finding{line: cl.line, cond: 4, msg: msgCond4a}) - case prevKind == kindBlank && englishAbove == 1: - // Condition 4 (b): a one-line English comment is separated from - // the marker by a blank line it must not have. + // [Ja] 条件 6: 日本語マーカーは一意でなければならない。 + if jaCount > 1 { + fs = append(fs, finding{line: cl.line, cond: 6, msg: msgCond6}) + } + case "": + // A non-marker line: detect an inline (end-of-line) marker. // - // [Ja] 条件 4 (b): 1 行の英語コメントとマーカーの間に不要な空行が - // 入っている。 - fs = append(fs, finding{line: cl.line, cond: 4, msg: msgCond4b}) + // [Ja] 非マーカー行: インライン (行末) マーカーを検出する。 + switch { + case inlineMarkerMisuse(cl.text): + fs = append(fs, finding{line: cl.line, cond: 3, msg: msgCond3}) + case inlineMarkerPresent(cl.text): + fs = append(fs, finding{line: cl.line, cond: 5, msg: msgCond5}) + } + if beforeJa && commentBody(cl.text) != "" { + englishBefore = true + } } - prevKind = kindMarker } + + // Condition 2: in a bilingual group the English block sits before the first + // Japanese marker; the line nearest that marker (skipping blank lines) must + // not be Japanese. Checking only the nearest line avoids flagging a separate + // Japanese label comment that a missing blank line merged into the group above + // the English block. + // + // [Ja] 条件 2: 併記群では英語ブロックは最初の日本語マーカーより前にあり、そのマーカーに + // 最も近い行 (空行は飛ばす) が日本語であってはならない。最も近い行だけを見ることで、空行 + // 漏れで群に取り込まれた別の日本語ラベルコメントを誤検出しない。 + if firstJa > 0 { + for i := firstJa - 1; i >= 0; i-- { + body := commentBody(lines[i].text) + if body == "" { + continue + } + if hasJapanese(body) { + fs = append(fs, finding{line: lines[i].line, cond: 2, msg: msgCond2}) + } + break + } + } + + sort.Slice(fs, func(i, j int) bool { + if fs[i].line != fs[j].line { + return fs[i].line < fs[j].line + } + return fs[i].cond < fs[j].cond + }) return fs } -// classifyLine classifies one non-marker comment line for the blank-line -// check (condition 4). The comment leader is stripped first — "//" or "/*" -// once, then at most one "*" continuation, so that "/**" classifies as blank -// while a row of stars ("//****") keeps its content and classifies as -// kindOther. Content indented with a tab or with two or more spaces after the -// leader is a code example (prose keeps a single space), and a line whose -// whole content is a URL is not prose, so both classify as kindOther. +// commentBody strips the comment leader ("//", "/*", or a "*" continuation) and +// the surrounding whitespace, returning the bare comment content. // -// [Ja] classifyLine は空行検査 (条件 4) のために非マーカー行を 1 行分類する。 -// 先にコメントリーダーを取り除く。"//" または "/*" を 1 回、続けて継続記号の -// "*" を高々 1 つだけ除去することで、"/**" は空行扱いにしつつ、星の並び -// ("//****") は本文が残って kindOther に分類される。リーダー直後がタブまたは -// スペース 2 つ以上で字下げされた本文はコード例 (地の文はスペース 1 つ)、 -// 本文全体が URL の行は地の文ではないため、いずれも kindOther に分類する。 -func classifyLine(text string) lineKind { +// [Ja] commentBody はコメントリーダー ("//"・"/*"・継続行の "*") と前後の空白を +// 取り除き、コメント本文だけを返す。 +func commentBody(text string) string { s := strings.TrimSpace(text) - for _, leader := range []string{"//", "/*"} { + for _, leader := range []string{"//", "/*", "*"} { if strings.HasPrefix(s, leader) { - s = strings.TrimPrefix(s, leader) + s = strings.TrimSpace(strings.TrimPrefix(s, leader)) break } } - s = strings.TrimPrefix(s, "*") // at most one "*" continuation. [Ja] 継続記号の "*" は高々 1 つ - if strings.HasPrefix(s, "\t") || strings.HasPrefix(s, " ") { - return kindOther - } - s = strings.TrimSpace(s) + return s +} + +// markerKind returns "en" if the comment content begins with the obsolete +// English marker, "ja" if it begins with the Japanese marker, or "" otherwise. A +// marker only counts at the start of the content, which separates a marker use +// from a mid-sentence mention in prose. +// +// [Ja] markerKind はコメント本文が廃止された英語マーカーで始まれば "en"、日本語マーカーで +// 始まれば "ja"、それ以外は "" を返す。マーカーは本文の先頭にあるときだけ数え、マーカーと +// しての使用と地の文中の言及を区別する。 +func markerKind(text string) string { + body := commentBody(text) switch { - case s == "": - return kindBlank - case reURLOnly.MatchString(s): - return kindOther - case hasJapanese(s): - return kindJapanese - case isEnglishText(s): - return kindEnglish + case strings.HasPrefix(body, "[En]"): + return "en" + case strings.HasPrefix(body, "[Ja]"): + return "ja" default: - return kindOther + return "" } } -// markerAtStart reports whether the comment content begins with the [Ja] marker, -// after stripping the comment leader ("//", "/*", or a "*" continuation) and -// surrounding whitespace. This distinguishes a marker use from a prose mention. +// inlineMarkerMisuse reports whether a non-marker line carries an inline Japanese +// marker with Japanese in the block before it (a duplicated Japanese block, or a +// reversed Japanese-then-English pair). To keep false positives near zero against +// a prose mention, the token counts as an inline marker only when the text right +// before it ends a sentence (a "." / "。" and the like); a token preceded by a +// word or particle is a prose mention and is ignored. // -// [Ja] markerAtStart は、コメントリーダー ("//"・"/*"・継続行の "*") と前後の空白を -// 取り除いた本文が [Ja] マーカーで始まるかを返す。マーカーとしての使用と地の文中の -// 言及を区別する。 -func markerAtStart(text string) bool { - s := strings.TrimSpace(text) - for _, leader := range []string{"//", "/*", "*"} { - if strings.HasPrefix(s, leader) { - s = strings.TrimSpace(strings.TrimPrefix(s, leader)) - break +// [Ja] inlineMarkerMisuse は、非マーカー行が、前のブロックに日本語があるインライン日本語 +// マーカー (日本語ブロックの重複、または日本語→英語の逆順ペア) を持つかを返す。地の文中の +// 言及への誤検出をほぼゼロに保つよう、トークンの直前が文末 ("." / "。" など) のときだけ +// インラインマーカーとみなす。直前が単語や助詞のトークンは地の文の言及として無視する。 +func inlineMarkerMisuse(text string) bool { + if markerKind(text) != "" { + return false + } + body := commentBody(text) + idx := strings.Index(body, "[Ja]") + if idx < 0 { + return false + } + left := strings.TrimRight(body[:idx], " \t") + if !hasJapanese(left) { + return false + } + r, _ := utf8.DecodeLastRuneInString(left) + return isSentenceEnd(r) +} + +// inlineMarkerPresent reports whether a non-marker line carries an inline marker +// (English or Japanese), regardless of the language before it. Like +// inlineMarkerMisuse it counts a token as a marker only when a sentence end +// precedes it, so a prose mention is ignored. +// +// [Ja] inlineMarkerPresent は、非マーカー行が、前の言語によらずインラインマーカー (英語 +// または日本語) を持つかを返す。inlineMarkerMisuse と同様、トークンの直前が文末のときだけ +// マーカーとみなすため、地の文中の言及は無視する。 +func inlineMarkerPresent(text string) bool { + if markerKind(text) != "" { + return false + } + body := commentBody(text) + for _, marker := range []string{"[En]", "[Ja]"} { + idx := strings.Index(body, marker) + if idx < 0 { + continue } + left := strings.TrimRight(body[:idx], " \t") + if left == "" { + continue + } + r, _ := utf8.DecodeLastRuneInString(left) + if isSentenceEnd(r) { + return true + } + } + return false +} + +// isSentenceEnd reports whether r is a sentence-ending mark, ASCII or full-width. +// +// [Ja] isSentenceEnd は r が文末記号 (ASCII または全角) かどうかを返す。 +func isSentenceEnd(r rune) bool { + switch r { + case '.', '!', '?', '。', '!', '?': + return true + default: + return false } - return strings.HasPrefix(s, "[Ja]") } // hasJapanese reports whether s contains any kana or kanji. +// // [Ja] hasJapanese は s に仮名・漢字が含まれるかを返す。 func hasJapanese(s string) bool { return reJapanese.MatchString(s) } -// isEnglishText reports whether s looks like English: it has Latin letters and -// no Japanese. A Japanese sentence containing a Latin acronym (e.g. "CSRF") is -// therefore not treated as English. -// -// [Ja] isEnglishText は s が英語に見えるか (ラテン文字を含み日本語を含まない) を返す。 -// "CSRF" のようなラテン略語を含む日本語文は英語扱いにしない。 -func isEnglishText(s string) bool { - return reLatin.MatchString(s) && !reJapanese.MatchString(s) -} - // commentGroups extracts comment groups from a file. Generated files yield none. +// // [Ja] commentGroups はファイルからコメント群を抽出する。生成物は空を返す。 func commentGroups(path, ext string) ([][]commentLine, error) { // path comes from walking the user-specified roots; reading it is the whole @@ -428,6 +506,7 @@ func commentGroups(path, ext string) ([][]commentLine, error) { } // goCommentGroups uses go/parser so that "//" inside string literals is ignored. +// // [Ja] goCommentGroups は go/parser を使い、文字列リテラル中の "//" を無視する。 func goCommentGroups(path string, src []byte) ([][]commentLine, error) { fset := token.NewFileSet() @@ -450,6 +529,7 @@ func goCommentGroups(path string, src []byte) ([][]commentLine, error) { } // templCommentGroups groups maximal runs of full-line "//" comments. +// // [Ja] templCommentGroups は行頭 "//" コメントの連続を 1 群にまとめる。 func templCommentGroups(src []byte) [][]commentLine { var groups [][]commentLine @@ -473,6 +553,7 @@ func templCommentGroups(src []byte) [][]commentLine { } // isGenerated reports whether src carries the standard generated-file header. +// // [Ja] isGenerated は src が定型の生成物ヘッダーを持つかを返す。 func isGenerated(src []byte) bool { sc := bufio.NewScanner(bytes.NewReader(src)) @@ -485,6 +566,7 @@ func isGenerated(src []byte) bool { } // skipDir reports whether a directory should not be walked. +// // [Ja] skipDir は走査しないディレクトリかどうかを返す。 func skipDir(name string) bool { switch name { @@ -548,6 +630,7 @@ func addedLines(base string) (map[string]map[int]bool, error) { } // gitOutput runs a git command and returns its stdout. +// // [Ja] gitOutput は git コマンドを実行し標準出力を返す。 func gitOutput(args ...string) (string, error) { cmd := exec.Command("git", args...) diff --git a/internal/comment/comment_test.go b/internal/comment/comment_test.go index 2dfea43..37a87e5 100644 --- a/internal/comment/comment_test.go +++ b/internal/comment/comment_test.go @@ -9,14 +9,15 @@ import ( "testing" ) -// reMarker matches the [Ja] translation marker. It is only needed by tests, +// reMarker matches the Japanese translation marker. It is only needed by tests, // so it lives here rather than in the production code. // -// [Ja] reMarker は [Ja] 翻訳マーカーにマッチする。テストでのみ必要なため、 -// 本番コードではなくこちらに置く。 +// [Ja] reMarker は日本語訳マーカーにマッチする。テストでのみ必要なため、本番コードでは +// なくこちらに置く。 var reMarker = regexp.MustCompile(`\[Ja\]`) // group builds a comment group from raw "//" lines, numbering them from 1. +// // [Ja] group は "//" 行から 1 始まりで番号付けしたコメント群を作る。 func group(texts ...string) []commentLine { lines := make([]commentLine, len(texts)) @@ -27,6 +28,7 @@ func group(texts ...string) []commentLine { } // condsOf returns the list of condition numbers in findings. +// // [Ja] condsOf は findings に含まれる条件番号の一覧を返す。 func condsOf(fs []finding) []int { got := make([]int, len(fs)) @@ -45,129 +47,139 @@ func TestCheckGroup(t *testing.T) { wantConds []int }{ { - name: "correct multi-line block", - // Two English lines, then a blank comment line, then the marker. - // [Ja] 英文 2 行 → 空行 → マーカーの正しい形。 - lines: group("// New post form.", "// It renders the editor.", "//", "// [Ja] 新規投稿フォーム。", "// エディタを表示する。"), + name: "correct pair with a blank line between the blocks", + // One blank comment line separates the English and Japanese blocks. + // + // [Ja] 英語ブロックと日本語ブロックを空行 1 行で区切る。 + lines: group("// Hash the password.", "//", "// [Ja] パスワードをハッシュ化する。"), wantConds: nil, }, { - name: "correct one-line pair", + name: "missing blank line between the blocks (8)", + // The [Ja] block directly follows the English line, with no blank. + // + // [Ja] [Ja] ブロックが英語行の直後に続き、空行が無い。 lines: group("// Hash the password.", "// [Ja] パスワードをハッシュ化する。"), - wantConds: nil, + wantConds: []int{8}, }, { - name: "correct multi-paragraph English block", - // English paragraphs count as one block across the paragraph break. - // [Ja] 段落区切りをまたいでも英語ブロックは 1 つとして数える。 - lines: group("// First paragraph.", "//", "// Second paragraph here.", "//", "// [Ja] 最初の段落。", "//", "// 2 つ目の段落。"), + name: "correct multi-line block with a blank line between the blocks", + // A blank line separates the multi-line English and Japanese blocks. + // + // [Ja] 複数行でも英日のブロックを空行で区切る。 + lines: group("// First line.", "// Second line.", "//", "// [Ja] 最初の行。", "// 2 行目。"), wantConds: nil, }, { - name: "correct inline pair on a single line", - lines: group("// Hash the password. [Ja] パスワードをハッシュ化する。"), - wantConds: nil, + name: "missing blank line in a multi-line block (8)", + // The [Ja] block directly follows the last English line, with no blank. + // + // [Ja] [Ja] ブロックが英語最終行の直後に続き、空行が無い。 + lines: group("// First line.", "// Second line.", "// [Ja] 最初の行。", "// 2 行目。"), + wantConds: []int{8}, }, { - name: "prose mention of [Ja] in English text is not a marker", - // A [Ja] mention inside English prose must not be treated as a marker. - // [Ja] 英語の地の文中の [Ja] 言及はマーカー扱いしない。 - lines: group("// The [Ja] marker leads the Japanese translation block.", "// [Ja] 日本語訳ブロックを先導するマーカー。"), - wantConds: nil, - }, - { - name: "marker on an English line (001/003 inversion)", - // Japanese leads and the English line carries the marker (the 001/003 inversion). - // [Ja] 英語行に [Ja] が付く誤り。日本語先・英語に [Ja]。 - lines: group("// POST /posts は後続タスクで登録する。", "// [Ja] POST /posts is registered in a later task."), + name: "Japanese block is not Japanese (1)", + // The lines under the Japanese marker carry English text. + // + // [Ja] 日本語マーカーの下の行が英文になっている。 + lines: group("// Hash the password.", "//", "// [Ja] hash the password"), wantConds: []int{1}, }, { - name: "Japanese-only with stray marker, no English above (002)", - // No English block; [Ja] is misused as a separator (same shape as 002). - // [Ja] 英語ブロックが無く [Ja] を区切りに誤用 (002 と同型)。 - lines: group("// CSRF トークンを設定する。", "// [Ja] /new は RequireAuth 配下のため context 経由で渡る。"), + name: "English block contains Japanese, a duplicated Japanese block (2)", + // The unmarked English block is written in Japanese (the duplication misuse). + // + // [Ja] 無マーカーの英語ブロックが日本語で書かれている (重複の誤用)。 + lines: group("// 平文パスワードをハッシュ化する。", "//", "// [Ja] 平文パスワードをハッシュ化する。"), wantConds: []int{2}, }, { - name: "marker-only Japanese comment without any English block", - lines: group("// [Ja] 日本語のみのコメント。"), + name: "Japanese on the line nearest the marker (2)", + // The line just above the marker is Japanese (the canonical misuse). + // + // [Ja] マーカーの直上の行が日本語になっている (典型的な誤用)。 + lines: group("// First line.", "// 二行目に日本語。", "//", "// [Ja] 最初の行。", "// 2 行目。"), wantConds: []int{2}, }, { - name: "Japanese line with a Latin acronym is still Japanese (not an English block)", - // A Japanese line stays Japanese even when it contains the Latin acronym CSRF. - // [Ja] ラテン略語 CSRF を含んでも日本語行は英語ブロックにならない。 - lines: group("// CSRF を検証する。", "// [Ja] これは説明である。"), - wantConds: []int{2}, + name: "Japanese label line merged above an English block is not flagged", + // A missing blank line merges a Japanese label into the group, but only + // the line nearest the marker is the English side, so it is not flagged. + // + // [Ja] 空行漏れで日本語ラベルが群に取り込まれても、マーカーに最も近い行だけが + // 英語側なので誤検出しない。 + lines: group("// 設定する", "// Configure the client.", "//", "// [Ja] クライアントを設定する。"), + wantConds: nil, }, { - name: "more than one marker in a group", - lines: group("// English line.", "// [Ja] 日本語。", "// [Ja] 二つ目のマーカー。"), - wantConds: []int{3}, + name: "obsolete English marker (7)", + // A leading [En] marker is obsolete; the English block is unmarked now. + // + // [Ja] 行頭の [En] マーカーは廃止。英語ブロックは無マーカーにする。 + lines: group("// [En] Hash the password.", "//", "// [Ja] パスワードをハッシュ化する。"), + wantConds: []int{7}, }, { - name: "marker on English line that is also a duplicate", - // The second marker sits on an English line, so both condition 3 and condition 1 fire. - // [Ja] 2 つ目のマーカーが英語行 → 条件 3 と条件 1 の両方。 - lines: group("// English.", "// [Ja] 日本語。", "// [Ja] second english marker."), - wantConds: []int{3, 1}, + name: "obsolete English marker with Japanese in the English block (2 then 7)", + // Both the obsolete marker and the Japanese-in-English misuse are reported. + // + // [Ja] 廃止マーカーと、英語ブロックの日本語混入の両方を報告する。 + lines: group("// [En] 平文パスワードをハッシュ化する。", "//", "// [Ja] 平文パスワードをハッシュ化する。"), + wantConds: []int{2, 7}, }, { - name: "missing blank line after a multi-line English block (4a)", - // Two English lines run straight into the marker (the §2.1.2 violation). - // [Ja] 英文 2 行が空行なしでマーカーに連続している (§2.1.2 違反)。 - lines: group("// Render the page title.", "// The site default is appended.", "// [Ja] ページタイトルをレンダリングする。"), - wantConds: []int{4}, + name: "two obsolete English markers (7 then 7)", + // Each [En] marker line is reported. + // + // [Ja] [En] マーカー行はそれぞれ報告される。 + lines: group("// [En] English one.", "// [En] English two.", "//", "// [Ja] 日本語。"), + wantConds: []int{7, 7}, }, { - name: "unnecessary blank line after a one-line English comment (4b)", - // A blank line follows a one-line English comment (the §2.1.5 bad example). - // [Ja] 英文 1 行なのに空行を挟んでいる (§2.1.5 の悪い例)。 - lines: group("// Hash the password.", "//", "// [Ja] パスワードをハッシュ化する。"), + name: "Japanese-only comment with no English block (4)", + lines: group("// [Ja] 日本語のみのコメント。"), wantConds: []int{4}, }, { - name: "code example above the marker skips condition 4", - // The tab-indented code line is unclassifiable, so the missing blank line is not reported. - // [Ja] タブ字下げのコード行は分類できないため、空行欠落を報告しない。 - lines: group("//\tkoryluslint comment .", "//", "// Run the tool first.", "// Then check the output.", "// [Ja] 先にツールを実行し、出力を確認する。"), - wantConds: nil, - }, - { - name: "space-indented code example above the marker skips condition 4", - // The space-indented code line is unclassifiable, so the missing blank line is not reported. - // [Ja] スペース字下げのコード行は分類できないため、空行欠落を報告しない。 - lines: group("// Run the tool:", "// koryluslint comment .", "// [Ja] ツールを実行する。"), - wantConds: nil, + name: "Japanese marker first, English text after it (4)", + // English after the Japanese marker does not count as an English block above it. + // + // [Ja] 日本語マーカーの後ろの英語は、上の英語ブロックとはみなさない。 + lines: group("// [Ja] 日本語。", "// English."), + wantConds: []int{4}, }, { - name: "separator line above the marker skips condition 4", - // The dash separator is unclassifiable, so the missing blank line is not reported. - // [Ja] ダッシュの区切り線は分類できないため、空行欠落を報告しない。 - lines: group("// ----", "// Run the tool first.", "// Then check the output.", "// [Ja] 先にツールを実行し、出力を確認する。"), - wantConds: nil, + name: "inline marker with a duplicated Japanese block (3)", + // A duplicated Japanese block on one line, with the marker at end of line. + // + // [Ja] 1 行に日本語ブロックが重複し、マーカーが行末にある。 + lines: group("// 値はゼロのまま。[Ja] 値はゼロのまま。"), + wantConds: []int{3}, }, { - name: "star-row separator above the marker skips condition 4", - // A row of stars is unclassifiable (not a blank line), so condition 4 stays silent. - // [Ja] 星のみの区切り線は空行ではなく分類できない行のため、条件 4 を報告しない。 - lines: group("// Hash the password.", "//****", "// [Ja] パスワードをハッシュ化する。"), - wantConds: nil, + name: "inline marker with a reversed Japanese-then-English pair (3)", + // Japanese leads and English follows the end-of-line marker. + // + // [Ja] 日本語が先で、行末マーカーの後ろに英語が続く。 + lines: group("// ドキュメント宣言。[Ja] document declaration"), + wantConds: []int{3}, }, { - name: "second marker in a duplicate-marker group does not add condition 4", - // The duplicate marker is already condition 3; the blank-line check stays silent for it. - // [Ja] 重複マーカーは条件 3 で報告済みのため、空行検査は発火しない。 - lines: group("// First pair.", "// [Ja] 最初のペア。", "// Second pair English.", "// [Ja] 二つ目のペア。"), - wantConds: []int{3}, + name: "inline marker with English before it is still banned (5)", + // A valid-looking old inline pair is banned under the current format. + // + // [Ja] 一見正しい旧インラインペアも現行フォーマットでは禁止。 + lines: group("// Hash the password. [Ja] パスワードをハッシュ化する。"), + wantConds: []int{5}, }, { - name: "URL-only line above the marker skips condition 4", - // A URL-only line is not prose, so condition 4 stays silent for the group. - // [Ja] URL のみの行は地の文ではないため、この群では条件 4 を報告しない。 - lines: group("// See the upstream issue.", "// https://github.com/golang/go/issues/12345", "// [Ja] 上流の issue を参照。"), - wantConds: nil, + name: "duplicate Japanese marker (6)", + // Two Japanese markers in one comment. + // + // [Ja] 1 コメントに日本語マーカーが 2 つ。 + lines: group("// English.", "//", "// [Ja] 日本語。", "// [Ja] 二つ目の日本語。"), + wantConds: []int{6}, }, } @@ -187,62 +199,112 @@ func TestCheckGroupReportsLineNumber(t *testing.T) { t.Parallel() lines := []commentLine{ - {line: 766, text: "// POST /posts は後続タスクで登録する。"}, - {line: 767, text: "// [Ja] POST /posts is registered in a later task."}, + {line: 766, text: "// 値はゼロのまま。[Ja] 値はゼロのまま。"}, } fs := checkGroup(lines) if len(fs) != 1 { t.Fatalf("got %d findings, want 1", len(fs)) } - if fs[0].line != 767 { - t.Errorf("finding line = %d, want 767", fs[0].line) + if fs[0].line != 766 { + t.Errorf("finding line = %d, want 766", fs[0].line) + } +} + +func TestMarkerKind(t *testing.T) { + t.Parallel() + + tests := []struct { + text string + want string + }{ + {"// [En] foo", "en"}, + {"// [Ja] バー", "ja"}, + {"// [En] indented leader is trimmed", "en"}, + {"\t* [En] block-comment continuation", "en"}, + {"// foo", ""}, + {"// The [En] mention is not at the start", ""}, + {"// 値はゼロのまま。[Ja] 値はゼロのまま。", ""}, + } + for _, tt := range tests { + if got := markerKind(tt.text); got != tt.want { + t.Errorf("markerKind(%q) = %q, want %q", tt.text, got, tt.want) + } } } -func TestIsEnglishText(t *testing.T) { +func TestInlineMarkerMisuse(t *testing.T) { t.Parallel() tests := []struct { text string want bool }{ - {"Set the CSRF token on the context.", true}, - {"CSRF トークンを設定する。", false}, // Latin acronym in Japanese is not English. - {"// ", false}, // marker leader only (the "before" slice for "// [Ja] ..."). - {"日本語のみ。", false}, - {"", false}, + // Misuse: a duplicated Japanese block on one line. + // + // [Ja] 誤用: 1 行に日本語ブロックが重複。 + {"// 値はゼロのまま。[Ja] 値はゼロのまま。", true}, + {"// CancelAt はゼロ値 (0) のまま. [Ja] CancelAt はゼロ値 (0) のまま", true}, + // Misuse: reversed pair, Japanese leads and English follows the marker. + // + // [Ja] 誤用: 逆順ペア。日本語が先でマーカーの後ろに英語。 + {"// ドキュメント宣言。[Ja] document declaration", true}, + // English before the marker: no Japanese in the block before it. + // + // [Ja] マーカーの前が英語: 前のブロックに日本語が無い。 + {"// Hash the password. [Ja] パスワードをハッシュ化する。", false}, + {`// "ja" or "en". [Ja] "ja" または "en"`, false}, + // Prose mention: the marker follows a particle, not a sentence end. + // + // [Ja] 地の文の言及: マーカーが助詞の後で文末ではない。 + {"// 本文が [Ja] マーカーで始まる行だけを対象にする。", false}, + // A line-leading marker is handled elsewhere, not here. + // + // [Ja] 行頭マーカーは別で扱うためここでは対象外。 + {"// [Ja] 日本語のみ。", false}, + {"// [En] English only.", false}, + // No marker at all. + // + // [Ja] マーカーが無い。 + {"// 値はゼロのまま。", false}, } for _, tt := range tests { - if got := isEnglishText(tt.text); got != tt.want { - t.Errorf("isEnglishText(%q) = %v, want %v", tt.text, got, tt.want) + if got := inlineMarkerMisuse(tt.text); got != tt.want { + t.Errorf("inlineMarkerMisuse(%q) = %v, want %v", tt.text, got, tt.want) } } } -func TestClassifyLine(t *testing.T) { +func TestInlineMarkerPresent(t *testing.T) { t.Parallel() tests := []struct { text string - want lineKind + want bool }{ - {"// Set the CSRF token on the context.", kindEnglish}, - {"// CSRF トークンを設定する。", kindJapanese}, - {"//", kindBlank}, - {"// ", kindBlank}, - {"/**", kindBlank}, // block-comment opener has no content. [Ja] ブロックコメントの開始行は本文なし - {"\t* Renders the page title.", kindEnglish}, - {"//\tkoryluslint comment .", kindOther}, // godoc-style code block. [Ja] godoc 形式のコード例 - {"// koryluslint comment .", kindOther}, // space-indented code example. [Ja] スペース字下げのコード例 - {"// https://example.com/issues/1", kindOther}, - {"// ----", kindOther}, - {"//****", kindOther}, // a star row keeps its content, unlike "/**". [Ja] 星の並びは "/**" と違い本文が残る - {"*****", kindOther}, // a bare star row inside a block comment. [Ja] ブロックコメント内の星のみの区切り線 - {"// See https://example.com for details.", kindEnglish}, // a URL inside prose stays English. [Ja] 地の文中の URL は英文のまま + // An inline Japanese marker after a sentence end. + // + // [Ja] 文末の後にインライン日本語マーカー。 + {"// Hash the password. [Ja] パスワードをハッシュ化する。", true}, + {"// 値はゼロのまま。[Ja] 値はゼロのまま。", true}, + // An inline English marker after a sentence end. + // + // [Ja] 文末の後にインライン英語マーカー。 + {"// cache it. [En] cache the result", true}, + // Prose mention: no sentence end before the marker. + // + // [Ja] 地の文の言及: マーカーの前に文末が無い。 + {"// The [Ja] marker leads the Japanese block.", false}, + {"// 本文が [Ja] マーカーで始まる。", false}, + // Line-leading markers and lines without any marker. + // + // [Ja] 行頭マーカー、およびマーカーの無い行。 + {"// proper English block line", false}, + {"// [Ja] 行頭の日本語マーカー", false}, + {"// no markers here at all", false}, } for _, tt := range tests { - if got := classifyLine(tt.text); got != tt.want { - t.Errorf("classifyLine(%q) = %v, want %v", tt.text, got, tt.want) + if got := inlineMarkerPresent(tt.text); got != tt.want { + t.Errorf("inlineMarkerPresent(%q) = %v, want %v", tt.text, got, tt.want) } } } @@ -259,6 +321,7 @@ func TestGoCommentGroupsIgnoresStringLiterals(t *testing.T) { "package sample", "", "// Greet returns a greeting.", + "//", "// [Ja] Greet は挨拶を返す。", "func Greet() string {", "\treturn \"// [Ja] this is not a comment\"", @@ -279,7 +342,7 @@ func TestGoCommentGroupsIgnoresStringLiterals(t *testing.T) { } } if markerLines != 1 { - t.Errorf("found %d comment lines with [Ja], want 1 (string literal must be ignored)", markerLines) + t.Errorf("found %d comment lines with the marker, want 1 (string literal must be ignored)", markerLines) } } @@ -293,10 +356,12 @@ func TestTemplCommentGroups(t *testing.T) { // 末尾のマークアップはどの群にも含まれない。 src := []byte(strings.Join([]string{ "// First group.", + "//", "// [Ja] 最初の群。", "templ Page() {", "\t
hello
", "// Second group.", + "//", "// [Ja] 2 つ目の群。", "}", }, "\n")) @@ -308,8 +373,8 @@ func TestTemplCommentGroups(t *testing.T) { if groups[0][0].line != 1 { t.Errorf("first group starts at line %d, want 1", groups[0][0].line) } - if groups[1][0].line != 5 { - t.Errorf("second group starts at line %d, want 5", groups[1][0].line) + if groups[1][0].line != 6 { + t.Errorf("second group starts at line %d, want 6", groups[1][0].line) } } @@ -339,23 +404,23 @@ func TestIsGenerated(t *testing.T) { } } -// TestRunFullMode exercises the subcommand end to end over a temp tree: full -// mode reports only condition 1, writes findings to stdout and a summary to -// stderr, and exits 1. +// TestRunFullMode exercises the subcommand end to end over a temp tree: full mode +// reports a near-zero-false-positive condition (here a Japanese block that is not +// Japanese), writes findings to stdout and a summary to stderr, and exits 1. // -// [Ja] TestRunFullMode は一時ツリー上でサブコマンドを通しで動かす。全体モードは -// 条件 1 のみを報告し、検出を stdout・要約を stderr に書き、終了コード 1 を返す。 +// [Ja] TestRunFullMode は一時ツリー上でサブコマンドを通しで動かす。全体モードは誤検出が +// ほぼ無い条件 (ここでは日本語でない日本語ブロック) を報告し、検出を stdout・要約を +// stderr に書き、終了コード 1 を返す。 func TestRunFullMode(t *testing.T) { t.Parallel() dir := t.TempDir() - // Condition 1: a Japanese line leads and the marker sits on an English line. - // [Ja] 条件 1: 日本語行が先導し、マーカーが英語行に付いている。 writeFile(t, dir, "bad.go", strings.Join([]string{ "package sample", "", - "// 日本語が先の行。", - "// [Ja] English text on the marker line.", + "// valid english block.", + "//", + "// [Ja] this block is not japanese.", "func Bad() {}", }, "\n")) @@ -365,22 +430,106 @@ func TestRunFullMode(t *testing.T) { if code != 1 { t.Fatalf("Run code = %d, want 1 (stderr: %s)", code, stderr.String()) } - if !strings.Contains(stdout.String(), "bad.go:4:") { - t.Errorf("stdout = %q, want a finding at bad.go:4", stdout.String()) + if !strings.Contains(stdout.String(), "bad.go:5:") { + t.Errorf("stdout = %q, want a finding at bad.go:5", stdout.String()) } - if !strings.Contains(stderr.String(), "1 bilingual [Ja] marker violation") { + if !strings.Contains(stderr.String(), "1 bilingual marker violation") { t.Errorf("stderr = %q, want a violation summary", stderr.String()) } } -// TestRunNoViolations confirms a clean tree exits 0 with no output. Conditions -// 2 (no English block above) and 4 (blank line before the marker) are not -// reported in full mode, so neither a Japanese-only comment nor a missing -// blank line must trip the check. +// TestRunFullModeEnglishBlockJapanese confirms full mode reports an English block +// that contains Japanese (the duplication misuse), tree-wide. // -// [Ja] TestRunNoViolations は問題のないツリーが無出力・終了コード 0 になることを -// 確認する。全体モードでは条件 2 (英語ブロック無し) と条件 4 (マーカー前の空行) を -// 報告しないため、日本語のみのコメントや空行欠落で検査が落ちてはならない。 +// [Ja] TestRunFullModeEnglishBlockJapanese は、英語ブロックに日本語が入っている誤用 +// (重複) を全体モードがツリー全体で報告することを確認する。 +func TestRunFullModeEnglishBlockJapanese(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + writeFile(t, dir, "bad.go", strings.Join([]string{ + "package sample", + "", + "// 平文パスワードをハッシュ化する。", + "//", + "// [Ja] 平文パスワードをハッシュ化する。", + "func Bad() {}", + }, "\n")) + + var stdout, stderr bytes.Buffer + code := Run([]string{dir}, &stdout, &stderr) + + if code != 1 { + t.Fatalf("Run code = %d, want 1 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "bad.go:3:") { + t.Errorf("stdout = %q, want a finding at bad.go:3", stdout.String()) + } +} + +// TestRunFullModeObsoleteEnglishMarker confirms full mode reports an obsolete +// English marker, tree-wide. +// +// [Ja] TestRunFullModeObsoleteEnglishMarker は、廃止された英語マーカーを全体モードが +// ツリー全体で報告することを確認する。 +func TestRunFullModeObsoleteEnglishMarker(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + writeFile(t, dir, "bad.go", strings.Join([]string{ + "package sample", + "", + "// [En] Greet returns a greeting.", + "//", + "// [Ja] Greet は挨拶を返す。", + "func Greet() string { return \"hi\" }", + }, "\n")) + + var stdout, stderr bytes.Buffer + code := Run([]string{dir}, &stdout, &stderr) + + if code != 1 { + t.Fatalf("Run code = %d, want 1 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "bad.go:3:") { + t.Errorf("stdout = %q, want a finding at bad.go:3", stdout.String()) + } +} + +// TestRunFullModeInlineMarker confirms full mode reports an inline Japanese +// marker with Japanese before it, tree-wide. +// +// [Ja] TestRunFullModeInlineMarker は、前に日本語があるインライン日本語マーカーを全体 +// モードがツリー全体で報告することを確認する。 +func TestRunFullModeInlineMarker(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + writeFile(t, dir, "bad.go", strings.Join([]string{ + "package sample", + "", + "// 値はゼロのまま。[Ja] 値はゼロのまま。", + "func Bad() {}", + }, "\n")) + + var stdout, stderr bytes.Buffer + code := Run([]string{dir}, &stdout, &stderr) + + if code != 1 { + t.Fatalf("Run code = %d, want 1 (stderr: %s)", code, stderr.String()) + } + if !strings.Contains(stdout.String(), "bad.go:3:") { + t.Errorf("stdout = %q, want a finding at bad.go:3", stdout.String()) + } +} + +// TestRunNoViolations confirms a clean tree exits 0 with no output. The +// English-required, inline-ban, duplicate, and blank-separator rules are diff-mode +// only, so none of them must trip in full mode. +// +// [Ja] TestRunNoViolations は問題のないツリーが無出力・終了コード 0 になることを確認する。 +// 英語必須・インライン禁止・重複・ブロック間空行の規則は差分モード限定のため、全体モードでは +// 発火してはならない。 func TestRunNoViolations(t *testing.T) { t.Parallel() @@ -389,15 +538,15 @@ func TestRunNoViolations(t *testing.T) { "package sample", "", "// Greet returns a greeting.", + "//", "// [Ja] Greet は挨拶を返す。", "func Greet() string { return \"hi\" }", "", - "// [Ja] 日本語のみのコメント。", - "func JapaneseOnly() {}", - "", "// Wave waves at the user.", "// It never returns an error.", - "// [Ja] Wave はユーザーに手を振る (空行欠落だが全体モードでは報告されない)。", + "//", + "// [Ja] Wave はユーザーに手を振る。", + "// エラーは返さない。", "func Wave() {}", }, "\n")) @@ -415,8 +564,8 @@ func TestRunNoViolations(t *testing.T) { // TestRunSkipsGeneratedFiles confirms generated files are not checked even when // they contain a marker misuse. // -// [Ja] TestRunSkipsGeneratedFiles は、生成物がマーカー誤用を含んでいても検査 -// 対象外になることを確認する。 +// [Ja] TestRunSkipsGeneratedFiles は、生成物がマーカー誤用を含んでいても検査対象外に +// なることを確認する。 func TestRunSkipsGeneratedFiles(t *testing.T) { t.Parallel() @@ -425,8 +574,9 @@ func TestRunSkipsGeneratedFiles(t *testing.T) { "// Code generated by stringer. DO NOT EDIT.", "package sample", "", - "// 日本語が先の行。", - "// [Ja] English text on the marker line.", + "// 日本語が英語ブロックに入っている。", + "//", + "// [Ja] 日本語。", "func Gen() {}", }, "\n")) @@ -439,6 +589,7 @@ func TestRunSkipsGeneratedFiles(t *testing.T) { } // TestRunHelpExitsZero confirms a -h request is treated as success. +// // [Ja] TestRunHelpExitsZero は -h 要求が成功扱いになることを確認する。 func TestRunHelpExitsZero(t *testing.T) { t.Parallel() @@ -450,6 +601,7 @@ func TestRunHelpExitsZero(t *testing.T) { } // writeFile writes content to name under dir, failing the test on error. +// // [Ja] writeFile は dir 配下の name に content を書き出し、失敗時にテストを止める。 func writeFile(t *testing.T, dir, name, content string) { t.Helper()