lib/pronto/spell.rb
# frozen_string_literal: true
require 'pronto'
require 'ffi/aspell'
module Pronto
class Spell < Runner
CONFIG_FILE = '.pronto_spell.yml'
def ignored_words
@ignored_words ||= begin
Set.new(spelling_config['ignored_words'].to_a.flatten.map(&:downcase))
end
end
def keywords
@keywords ||= begin
Set.new(spelling_config['only_lines_matching'].to_a.flatten.map(&:downcase))
end
end
def run
return [] if !@patches || @patches.count.zero?
@patches
.select { |patch| patch.additions.positive? }
.map { |patch| inspect(patch) }
.flatten.compact
end
private
def inspect(patch)
patch.added_lines.map do |line|
if keywords.any? && !keywords_regexp.match(line.content)
next
end
words = line.content.scan(/([A-Z]{2,})|([A-Z]{0,1}[a-z]+)/)
.flatten.compact.uniq
words
.select { |word| misspelled?(word) }
.map { |word| new_message(word, line) }
end
end
def new_message(word, line)
path = line.patch.delta.new_file[:path]
level = :warning
suggestions = speller.suggestions(word)
msg = %("#{word}" might not be spelled correctly.)
if suggestions.any?
suggestions_text = suggestions[0..max_suggestions_number - 1].join(', ')
msg += " Spelling suggestions: #{suggestions_text}"
end
Message.new(path, line, level, msg, nil, self.class)
end
def speller
@speller ||= FFI::Aspell::Speller.new(
language, 'sug-mode': suggestion_mode
)
end
def spelling_config
@spelling_config ||= begin
config_path = File.join(repo_path, CONFIG_FILE)
File.exist?(config_path) ? YAML.load_file(config_path) : {}
end
end
def keywords_regexp
@keywords_regexp ||= %r{#{keywords.to_a.join('|')}}
end
def language
spelling_config['language'] || 'en_US'
end
def suggestion_mode
spelling_config['suggestion_mode'] || 'fast'
end
def min_word_length
spelling_config['min_word_length'] || 5
end
def max_word_length
spelling_config['max_word_length'] || Float::INFINITY
end
def max_suggestions_number
spelling_config['max_suggestions_number'] || 3
end
def misspelled?(word)
lintable_word?(word) && !speller.correct?(word)
end
def lintable_word?(word)
(min_word_length..max_word_length).cover?(word.length) &&
!ignored_words.include?(word.downcase)
end
end
end