Class: VivlioStarter::CLI::LintCommands::LintRunner

Inherits:
Object
  • Object
show all
Defined in:
lib/vivlio_starter/cli/lint.rb

Overview

text:lint 実行ロジックをまとめたランナー

Defined Under Namespace

Classes: TargetResolver

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(targets, options) ⇒ LintRunner

Returns a new instance of LintRunner.



90
91
92
93
# File 'lib/vivlio_starter/cli/lint.rb', line 90

def initialize(targets, options)
  @targets = Array(targets)
  @options = normalize_options(options)
end

Instance Attribute Details

#optionsObject (readonly)

Returns the value of attribute options.



88
89
90
# File 'lib/vivlio_starter/cli/lint.rb', line 88

def options
  @options
end

#targetsObject (readonly)

Returns the value of attribute targets.



88
89
90
# File 'lib/vivlio_starter/cli/lint.rb', line 88

def targets
  @targets
end

Instance Method Details

#apply_prose_fixes!(files) ⇒ Array<String>

交ぜ書きの置換を原稿へ適用する(--fix 指定時のみ)。 対比の指摘は自動修正しない(どちらが X するのかは著者しか知らないため)。

Returns:

  • (Array<String>)

    実際に書き換えた原稿パス



193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/vivlio_starter/cli/lint.rb', line 193

def apply_prose_fixes!(files)
  return [] if disabled_rules.include?(Lint::ProseChecker::MAZEGAKI_RULE)

  files.filter_map do |path|
    original = File.read(path, encoding: 'UTF-8')
    fixed    = Lint::ProseChecker.fix_mazegaki(original, prose_allowlist)
    next if fixed == original

    atomic_write(path, fixed)
    path
  end
end

#callObject



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/vivlio_starter/cli/lint.rb', line 101

def call
  ensure_textlint_available! unless spellcheck_only?
  ensure_config_present! unless spellcheck_only?
  ensure_support_yaml_files! unless spellcheck_only?

  files = resolve_targets
  if files.empty?
    Common.log_warn('検査対象となる Markdown ファイルが見つかりません。')
    return 0
  end

  lint_info  = { exit: 0, lint_count: 0, fixable_count: 0, fixed_files: [] }
  prose_info = { exit: 0, prose_count: 0, fixed_files: [] }
  spell_info = { exit: 0, spell_count: 0 }

  unless spellcheck_only?
    # 交ぜ書きの置換を先に済ませてから textlint を走らせる。逆順にすると
    # textlint が見る原稿と、置換後に残る原稿が食い違う。
    prose_fixed = options[:fix] ? apply_prose_fixes!(files) : []
    lint_info   = run_textlint(files)
    prose_info  = run_prose_check(files).merge(fixed_files: prose_fixed)
  end
  spell_info = run_spellcheck(files) unless textlint_only?

  print_combined_summary(lint_info, prose_info, spell_info)
  [lint_info[:exit], prose_info[:exit], spell_info[:exit]].max
rescue LintError => e
  Common.log_error(e.message)
  1
end

#check_prose(path) ⇒ Object



206
207
208
# File 'lib/vivlio_starter/cli/lint.rb', line 206

def check_prose(path)
  Lint::ProseChecker.check(path, disabled_rules: disabled_rules, allowlist: prose_allowlist)
end

#disabled_rulesObject

book.yml lint.disabled_rules(ルール ID で丸ごと無効化)



218
219
220
# File 'lib/vivlio_starter/cli/lint.rb', line 218

def disabled_rules
  Array(Common::CONFIG.lint.disabled_rules).map(&:to_s)
end


227
228
229
230
231
232
233
234
235
236
# File 'lib/vivlio_starter/cli/lint.rb', line 227

def print_textlint_aggregated(result)
  result[:files].each do |file|
    Common.log_always "📄 #{file[:path]}  (textlint)"
    file[:rows].each do |row|
      Common.log_always format('  %3d件  %s', row[:count], row[:label])
      Common.log_always format('         行: %s', row[:lines])
    end
    Common.log_always ''
  end
end

#prose_allowlistObject

config/textlint_allowlist.yml の語で交ぜ書きの指摘を黙らせる。 textlint と同じファイルを読むので、著者が窓口を二度覚えなくて済む。



212
213
214
215
# File 'lib/vivlio_starter/cli/lint.rb', line 212

def prose_allowlist
  @prose_allowlist ||=
    Lint::ProseChecker.allowlist_from(Common.resolve_path_from_root(TEXTLINT_ALLOWLIST_RELATIVE))
end

#run_prose_check(files) ⇒ Object

textlint では扱えない指摘(交ぜ書き・二通りに読める対比)を当てる。 件数は「日本語校正」へ合算する——著者から見れば textlint の指摘と区別する 理由がない。仕様: lint-japanese-prose-rules-spec.md §5



179
180
181
182
183
184
185
186
187
188
# File 'lib/vivlio_starter/cli/lint.rb', line 179

def run_prose_check(files)
  findings_by_file = files.to_h { [it, check_prose(it)] }
                          .reject { |_path, findings| findings.empty? }
  Lint::ProseChecker.print_errors(findings_by_file)

  all = findings_by_file.values.flatten
  { exit: all.empty? ? 0 : 1,
    prose_count: all.size,
    fixable_count: all.count { it.rule == Lint::ProseChecker::MAZEGAKI_RULE } }
end

#run_textlint(files) ⇒ Object

textlint 本体を実行して結果サマリーを返す。 出力は常にルール単位の集約表示(textlint --format json を取得して整形)。

--fix 指定時は「修正パス → 解析パス」の 2 段構成を採る。修正パスは記法ガードを 通さない一時ファイルを textlint に直させて原稿へ書き戻し、解析パスはガード済みの 一時ファイルから残存指摘を集める。ガードによる記法の中和は非可逆なので、 ガード済みの内容を原稿へ書き戻すことはできない(両立させるための 2 パス)。 仕様: lint-notation-guard-spec.md §2.3



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/vivlio_starter/cli/lint.rb', line 140

def run_textlint(files)
  fixed_files = options[:fix] ? apply_textlint_fixes!(files) : []

  converted_files = convert_vs_lint_comments(files)
  # textlint は一時ファイルを検査するため、出力の一時パスを元ファイル名へ戻すマップ
  path_map = files.zip(converted_files).to_h { |orig, tmp| [File.expand_path(tmp), orig] }
  hushed = suppressed_lines_map(files, converted_files)
  run_textlint_aggregated(converted_files, path_map, hushed).merge(fixed_files: fixed_files)
ensure
  cleanup_temp_files(converted_files) if converted_files
  @runtime_config_tmp&.unlink
end

#run_textlint_aggregated(files, path_map = {}, suppressed_lines = {}) ⇒ Object

ルール単位で集約した独自表示(--format json で取得して整形)



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/vivlio_starter/cli/lint.rb', line 154

def run_textlint_aggregated(files, path_map = {}, suppressed_lines = {})
  command = build_command(files, format: 'json')
  stdout, stderr, status = Open3.capture3(*command)
  $stderr.print(stderr) unless stderr.nil? || stderr.empty?

  result = TextlintFormatter.aggregate_json(
    stdout, disabled_rules: disabled_rules, trim_long_vowel: trim_long_vowel?,
            suppressed_lines: suppressed_lines
  )
  if result.nil?
    # JSON 解釈に失敗(textlint 自体のエラー等)。生出力をそのまま見せる。
    $stdout.print(stdout) unless stdout.nil? || stdout.empty?
    return { exit: textlint_exit(status), lint_count: 0, fixable_count: 0 }
  end

  # 一時ファイルのパスを元ファイル名へ戻す
  result[:files].each { |f| f[:path] = path_map[File.expand_path(f[:path])] || f[:path] }
  print_textlint_aggregated(result)
  # 無効化で除外した分は問題数に数えない(残り 0 なら成功扱い)
  { exit: result[:total].positive? ? 1 : 0, lint_count: result[:total], fixable_count: result[:fixable] }
end

#spellcheck_only?Boolean

--spellcheck-only / --textlint-only / --register による実行範囲。 --register はスペルチェック専用の操作なので、暗黙に spellcheck 単独で動く (--spellcheck-only を併記する必要はない)。

Returns:

  • (Boolean)


98
# File 'lib/vivlio_starter/cli/lint.rb', line 98

def spellcheck_only? = options[:register] || options[:spellcheck_only]

#textlint_exit(status) ⇒ Object



238
# File 'lib/vivlio_starter/cli/lint.rb', line 238

def textlint_exit(status) = status.success? ? 0 : (status.exitstatus || 1)

#textlint_only?Boolean

Returns:

  • (Boolean)


99
# File 'lib/vivlio_starter/cli/lint.rb', line 99

def textlint_only?   = !options[:register] && options[:textlint_only]

#trim_long_vowel?Boolean

book.yml lint.trim_long_vowel(末尾長音を足す指摘を抑止:技術者向け文体)

Returns:

  • (Boolean)


223
224
225
# File 'lib/vivlio_starter/cli/lint.rb', line 223

def trim_long_vowel?
  Common.truthy?(Common::CONFIG.lint.trim_long_vowel)
end