rubocop-spellcheck 0.1.0

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.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Spellcheck
6
+ # Checks Ruby comments, Markdown prose, and plain text with shared dictionaries.
7
+ class Spelling < Base
8
+ BUNDLED_DATA = File.expand_path('../../../../data', __dir__).freeze
9
+ DIRECTIVE = /
10
+ \A\#\s*(?:!|(?:rubocop|standard|coding|encoding|frozen_string_literal|
11
+ warn_indent|shareable_constant_value|typed)\s*:)|\A\#.*-\*-\s*(?:coding|encoding):
12
+ /x
13
+ ADDRESS = %r{(?:https?://|www\.)[^\s<>]+|[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}}
14
+
15
+ def on_new_investigation
16
+ super
17
+ if RuboCop::Spellcheck::Markdown.file?(processed_source.path)
18
+ check_markdown
19
+ else
20
+ processed_source.comments.each do |comment|
21
+ next if DIRECTIVE.match?(comment.text)
22
+
23
+ check_comment(comment)
24
+ end
25
+ end
26
+ end
27
+
28
+ def on_other_file
29
+ if RuboCop::Spellcheck::ProseSource.text_file?(processed_source.path)
30
+ check_text
31
+ elsif RuboCop::Spellcheck::Markdown.file?(processed_source.path)
32
+ check_markdown
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def relevant_file?(file)
39
+ return false unless super
40
+ return false if File.expand_path(file).start_with?("#{BUNDLED_DATA}/")
41
+ return true unless RuboCop::Spellcheck::ProseSource.text_file?(file)
42
+
43
+ !checker.dictionary_file?(file)
44
+ end
45
+
46
+ def external_dependency_checksum
47
+ checker.checksum
48
+ end
49
+
50
+ private
51
+
52
+ def front_matter_enabled?
53
+ cop_config.fetch('Markdown', {}).fetch('FrontMatter', {}).fetch('Enabled', true)
54
+ end
55
+
56
+ def check_markdown
57
+ if front_matter_enabled?
58
+ RuboCop::Spellcheck::FrontMatter.new(prose_buffer).each_span do |text, positions|
59
+ check_markdown_span(text, positions)
60
+ end
61
+ end
62
+ RuboCop::Spellcheck::Markdown.new(prose_buffer).each_span do |text, positions|
63
+ check_markdown_span(text, positions)
64
+ end
65
+ end
66
+
67
+ def check_text
68
+ checker.misspellings(mask_addresses(processed_source.buffer.source)).each do |word, position|
69
+ next unless word.match?(/\p{L}/)
70
+
71
+ report_word(word, position, position + word.length)
72
+ end
73
+ end
74
+
75
+ def checker
76
+ @checker ||= RuboCop::Spellcheck::Checker.new(
77
+ dictionary_files: cop_config['DictionaryFiles'],
78
+ base_directory: config.base_dir_for_path_parameters
79
+ )
80
+ end
81
+
82
+ def check_comment(comment)
83
+ checker.misspellings(prose(comment.text)).each do |word, position|
84
+ first = comment.source_range.begin_pos + position
85
+ report_word(word, first, first + word.length)
86
+ end
87
+ end
88
+
89
+ def check_markdown_span(text, positions)
90
+ text = text.sub(/\A['’]s\b/, ' ')
91
+ checker.misspellings(mask_addresses(text)).each do |word, position|
92
+ next unless word.match?(/\p{L}/)
93
+
94
+ report_word(word, positions.fetch(position).first, positions.fetch(position + word.length - 1).last)
95
+ end
96
+ end
97
+
98
+ def report_word(word, first, last)
99
+ range = Parser::Source::Range.new(prose_buffer, first, last)
100
+ range = markdown_document.processed_range(range) if markdown_document
101
+ add_offense(range, message: spelling_message(word))
102
+ end
103
+
104
+ def markdown_document
105
+ processed_source.markdown_document if processed_source.respond_to?(:markdown_document)
106
+ end
107
+
108
+ def prose_buffer
109
+ markdown_document ? markdown_document.buffer : processed_source.buffer
110
+ end
111
+
112
+ def prose(text)
113
+ # Preserve character offsets while removing non-prose addresses and block delimiters.
114
+ mask_addresses(text).gsub(/^=(?:begin|end)\b[^\n]*/) { |marker| ' ' * marker.length }
115
+ end
116
+
117
+ def mask_addresses(text)
118
+ text.gsub(ADDRESS) { |address| ' ' * address.length }
119
+ end
120
+
121
+ def spelling_message(word)
122
+ suggestions = checker.suggestions(word)
123
+ message = "Misspelled word: `#{word}`."
124
+ return message if suggestions.empty?
125
+
126
+ "#{message} Did you mean #{suggestions.map { |suggestion| "`#{suggestion}`" }.join(', ')}?"
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'kotoshu'
5
+ require 'pathname'
6
+
7
+ module RuboCop
8
+ module Spellcheck
9
+ # Checks words against explicitly selected, local language dictionaries.
10
+ class Checker
11
+ DEFAULT_FILES = { 'en' => ['gem:rubocop-spellcheck/data/en/en.dic'].freeze }.freeze
12
+ ENGLISH_WORD = /(?<![\p{L}\p{M}\p{N}_])[\p{Latin}\p{M}]+(?:['’][\p{Latin}\p{M}]+)*(?![\p{L}\p{M}\p{N}_])/
13
+
14
+ def initialize(dictionary_files: nil, base_directory: Dir.pwd)
15
+ @base_directory = base_directory
16
+ @paths = []
17
+ @checkers = build_checkers(dictionary_files.nil? ? DEFAULT_FILES : dictionary_files)
18
+ @verdicts = {}
19
+ @suggestions = {}
20
+ @checksum = Digest::SHA256.hexdigest(
21
+ [Kotoshu::VERSION, *@paths.map { |path| Digest::SHA256.file(path).hexdigest }].join("\0")
22
+ )
23
+ end
24
+
25
+ attr_reader :checksum
26
+
27
+ def dictionary_file?(path)
28
+ return false unless File.exist?(path)
29
+
30
+ @paths.any? { |dictionary| File.identical?(dictionary, path) }
31
+ end
32
+
33
+ def misspellings(text)
34
+ @checkers.flat_map { |checker| tokens(checker, text) }.uniq
35
+ .sort_by(&:last).reject { |word, _position| correct?(word) }
36
+ end
37
+
38
+ def suggestions(word)
39
+ @suggestions[word] ||= @checkers.flat_map do |checker|
40
+ checker.suggest(word, max_suggestions: 3).to_words
41
+ end.uniq.first(3)
42
+ end
43
+
44
+ private
45
+
46
+ def tokens(checker, text)
47
+ return checker.tokenize(text) unless checker.config.language.match?(/\Aen(?:[-_]|\z)/i)
48
+
49
+ text.to_enum(:scan, ENGLISH_WORD).map { [Regexp.last_match[0], Regexp.last_match.begin(0)] }
50
+ end
51
+
52
+ def build_checkers(files)
53
+ validate!(files)
54
+ files.flat_map do |language, paths|
55
+ paths.map { |path| build_checker(language, resolve_path(path)) }
56
+ end
57
+ end
58
+
59
+ def validate!(files)
60
+ valid = files.is_a?(Hash) && !files.empty? &&
61
+ files.all? { |language, paths| valid_language?(language) && valid_paths?(paths) }
62
+ return if valid
63
+
64
+ raise RuboCop::ValidationError, 'Spellcheck/DictionaryFiles must map language codes to nonempty lists of paths.'
65
+ end
66
+
67
+ def valid_language?(language)
68
+ language.is_a?(String) && language.match?(/\A[a-zA-Z]+(?:[-_][a-zA-Z0-9]+)*\z/)
69
+ end
70
+
71
+ def valid_paths?(paths)
72
+ paths.is_a?(Array) && !paths.empty? && paths.all? { |path| path.is_a?(String) && !path.empty? }
73
+ end
74
+
75
+ def resolve_path(path)
76
+ return File.expand_path(path, @base_directory) unless path.start_with?('gem:')
77
+
78
+ match = %r{\Agem:([^/]+)/(.+)\z}.match(path)
79
+ raise RuboCop::ValidationError, "Invalid gem dictionary path: #{path}" unless match
80
+
81
+ root = Gem::Specification.find_by_name(match[1]).full_gem_path
82
+ resolved = File.expand_path(match[2], root)
83
+ raise RuboCop::ValidationError, "Path escapes gem: #{path}" unless resolved.start_with?("#{root}/")
84
+
85
+ resolved
86
+ rescue Gem::LoadError => e
87
+ raise RuboCop::ValidationError, "Cannot resolve dictionary #{path}: #{e.message}"
88
+ end
89
+
90
+ def build_checker(language, path)
91
+ dictionary = load_dictionary(language, path)
92
+ settings = Kotoshu::Configuration.new(
93
+ language: language, offline: true, personal_dictionary: false, backend: 'ruby'
94
+ )
95
+ Kotoshu::Spellchecker.new(dictionary: dictionary, config: settings)
96
+ end
97
+
98
+ def load_dictionary(language, path)
99
+ case File.extname(path)
100
+ when '.dic'
101
+ hunspell_dictionary(language, path)
102
+ when '.txt'
103
+ words = read_file(path).lines.filter_map { |line| dictionary_word(line, path) }
104
+ Kotoshu::Dictionary::PlainText.from_words(words, language_code: language)
105
+ else
106
+ raise RuboCop::ValidationError, "Expected a .dic or .txt dictionary: #{path}"
107
+ end
108
+ end
109
+
110
+ def hunspell_dictionary(language, path)
111
+ aff = path.sub(/\.dic\z/, '.aff')
112
+ [path, aff].each { |file| read_file(file) }
113
+ Kotoshu::Dictionary::Hunspell.new(dic_path: path, aff_path: aff, language_code: language)
114
+ end
115
+
116
+ def read_file(path)
117
+ content = File.read(path, encoding: 'UTF-8')
118
+ @paths << path
119
+ content
120
+ rescue SystemCallError => e
121
+ raise RuboCop::ValidationError, "Cannot read spelling dictionary #{path}: #{e.message}"
122
+ end
123
+
124
+ def dictionary_word(line, path)
125
+ word = line.strip
126
+ return if word.empty? || word.start_with?('#')
127
+ raise RuboCop::ValidationError, "Expected one word per line in #{path}: #{word.inspect}" if word.match?(/\s/)
128
+
129
+ word
130
+ end
131
+
132
+ def correct?(word)
133
+ return @verdicts[word] if @verdicts.key?(word)
134
+
135
+ @verdicts[word] = @checkers.any? { |checker| checker.correct?(word) }
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'psych'
4
+ require 'date'
5
+
6
+ module RuboCop
7
+ module Spellcheck
8
+ # Reads YAML string values without constructing application objects.
9
+ class FrontMatter
10
+ HEADER = /\A(?:\uFEFF)?---[ \t]*\n.*?^(?:---|\.\.\.)[ \t]*(?:\n|\z)/m
11
+ ESCAPE = /\\(?:U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)/m
12
+
13
+ def initialize(buffer)
14
+ @buffer = buffer
15
+ @scanner = Psych::ScalarScanner.new(Psych::ClassLoader.new)
16
+ end
17
+
18
+ def each_span
19
+ return enum_for(__method__) unless block_given?
20
+
21
+ root = yaml_root
22
+ string_values(root) { |node| yield scalar_span(node) } if root
23
+ end
24
+
25
+ private
26
+
27
+ def yaml_root
28
+ return unless (header = HEADER.match(@buffer.source))
29
+
30
+ yaml = header[0].sub(/^(?:---|\.\.\.)[ \t]*\n?\z/, '')
31
+ Psych.parse_stream(yaml).children.first&.root
32
+ rescue Psych::SyntaxError => e
33
+ raise RuboCop::ValidationError, "Invalid YAML front matter in #{@buffer.name}: #{e.message}"
34
+ end
35
+
36
+ def string_values(node, &block)
37
+ case node
38
+ when Psych::Nodes::Mapping
39
+ node.children.each_slice(2) { |_key, value| string_values(value, &block) }
40
+ when Psych::Nodes::Sequence
41
+ node.children.each { |value| string_values(value, &block) }
42
+ when Psych::Nodes::Scalar
43
+ yield node if string_scalar?(node)
44
+ end
45
+ end
46
+
47
+ def string_scalar?(node)
48
+ return node.tag == 'tag:yaml.org,2002:str' if node.tag
49
+ return true unless node.plain
50
+
51
+ @scanner.tokenize(node.value).is_a?(String)
52
+ end
53
+
54
+ def position(line, column)
55
+ @buffer.line_range(line + 1).begin_pos + column
56
+ end
57
+
58
+ def scalar_span(node)
59
+ first = position(node.start_line, node.start_column)
60
+ last = position(node.end_line, node.end_column)
61
+ raw = @buffer.source[first...last]
62
+ prefix = raw[/\A(?:(?:&\S+|!\S+)[ \t]+)+/].to_s.length
63
+ map_pieces(raw[prefix..], node.style, first + prefix)
64
+ end
65
+
66
+ def map_pieces(raw, style, first)
67
+ text = +''
68
+ positions = []
69
+ scalar_pieces(raw, style).each do |piece, offset, decoded|
70
+ text << decoded
71
+ decoded.length.times { positions << [first + offset, first + offset + piece.length] }
72
+ end
73
+ [text, positions]
74
+ end
75
+
76
+ def scalar_pieces(raw, style)
77
+ return enum_for(__method__, raw, style) unless block_given?
78
+
79
+ pattern = style == Psych::Nodes::Scalar::DOUBLE_QUOTED ? /#{ESCAPE}|./m : /''|./m
80
+ raw.to_enum(:scan, pattern).each do
81
+ match = Regexp.last_match
82
+ piece = match[0]
83
+ decoded = decode(piece, style)
84
+ decoded = ' ' * piece.length if ignored_piece?(raw, match.begin(0), style)
85
+ yield piece, match.begin(0), decoded
86
+ end
87
+ end
88
+
89
+ def ignored_piece?(raw, offset, style)
90
+ case style
91
+ when Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED
92
+ offset < raw.index("\n").to_i
93
+ when Psych::Nodes::Scalar::SINGLE_QUOTED, Psych::Nodes::Scalar::DOUBLE_QUOTED
94
+ offset.zero? || offset == raw.length - 1
95
+ else
96
+ false
97
+ end
98
+ end
99
+
100
+ def decode(piece, style)
101
+ if style == Psych::Nodes::Scalar::DOUBLE_QUOTED && piece.start_with?('\\')
102
+ Psych.safe_load("\"#{piece}\"") || ''
103
+ elsif style == Psych::Nodes::Scalar::SINGLE_QUOTED && piece == "''"
104
+ "'"
105
+ else
106
+ piece
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'commonmarker'
4
+
5
+ module RuboCop
6
+ module Spellcheck
7
+ # Extracts visible text while retaining character ranges in the original source.
8
+ class Markdown
9
+ EXTENSIONS = %w[.md .markdown].freeze
10
+ ENTITY = /&(?:\#x[0-9a-f]+|\#\d+|[a-z][a-z0-9]+);/i
11
+ PIECE = /#{ENTITY}|\\[[:punct:]]|./m
12
+ LIQUID = /\{\{.*?\}\}|\{%.*?%\}/m
13
+
14
+ def self.file?(path)
15
+ path && EXTENSIONS.include?(File.extname(path).downcase)
16
+ end
17
+
18
+ def initialize(buffer)
19
+ @buffer = buffer
20
+ @lines = buffer.source.lines
21
+ end
22
+
23
+ def each_span
24
+ return enum_for(__method__) unless block_given?
25
+
26
+ document = Commonmarker.parse(parser_source, options: {
27
+ parse: { smart: false },
28
+ extension: { shortcodes: false, footnotes: true }
29
+ })
30
+ document.walk do |node|
31
+ next unless node.type == :text
32
+
33
+ text, positions = mapped_text(node)
34
+ yield text, positions unless text.empty?
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parser_source
41
+ without_front_matter.gsub(LIQUID) do |expression|
42
+ expression.gsub(/[^\n]/) { |character| 'x' * character.bytesize }
43
+ end
44
+ end
45
+
46
+ def without_front_matter
47
+ @buffer.source.sub(/\A(?:\uFEFF)?---[ \t]*\n.*?^(?:---|\.\.\.)[ \t]*(?:\n|\z)/m) do |header|
48
+ header.gsub(/[^\n]/, ' ')
49
+ end
50
+ end
51
+
52
+ def character_position(line, byte_column)
53
+ prefix = @lines.fetch(line - 1).byteslice(0, byte_column)
54
+ @buffer.line_range(line).begin_pos + prefix.length
55
+ end
56
+
57
+ def mapped_text(node)
58
+ location = node.source_position
59
+ first = character_position(location.fetch(:start_line), location.fetch(:start_column) - 1)
60
+ last = character_position(location.fetch(:end_line), location.fetch(:end_column))
61
+ decode_text(@buffer.source[first...last], first)
62
+ end
63
+
64
+ def decode_text(raw, first)
65
+ raw = without_liquid(raw)
66
+ text = +''
67
+ positions = []
68
+ raw.to_enum(:scan, PIECE).each do
69
+ match = Regexp.last_match
70
+ decoded = decode(match[0])
71
+ text << decoded
72
+ decoded.length.times { positions << [first + match.begin(0), first + match.end(0)] }
73
+ end
74
+ [text, positions]
75
+ end
76
+
77
+ def without_liquid(raw)
78
+ raw.gsub(LIQUID) { |expression| expression.gsub(/[^\n]/, ' ') }
79
+ end
80
+
81
+ def decode(piece)
82
+ return piece.delete_prefix('\\') if piece.start_with?('\\')
83
+ return piece unless ENTITY.match?(piece)
84
+
85
+ document = Commonmarker.parse(piece, options: { parse: { smart: false } })
86
+ document.walk.select { |node| node.type == :text }.map(&:string_content).join
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+
5
+ module RuboCop
6
+ module Spellcheck
7
+ # Registers the extension and its defaults with RuboCop.
8
+ class Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: 'rubocop-spellcheck',
12
+ version: VERSION,
13
+ homepage: 'https://github.com/dblock/rubocop-spellcheck',
14
+ description: 'Spellcheck Ruby comments, Markdown prose, and plain text.'
15
+ )
16
+ end
17
+
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
21
+
22
+ def rules(_context)
23
+ LintRoller::Rules.new(
24
+ type: :path, config_format: :rubocop,
25
+ value: Pathname.new(__dir__).join('../../../config/default.yml')
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ # Prose files use the non-Ruby callback, with an untouched source buffer.
6
+ module ProseSource
7
+ def self.file?(path)
8
+ Markdown.file?(path) || text_file?(path)
9
+ end
10
+
11
+ def self.text_file?(path)
12
+ path && File.extname(path).downcase == '.txt'
13
+ end
14
+
15
+ def valid_syntax?
16
+ ProseSource.file?(path) && !markdown_preprocessor? ? false : super
17
+ end
18
+
19
+ private
20
+
21
+ def parse(source, *)
22
+ return super if !ProseSource.file?(path) || markdown_preprocessor?
23
+
24
+ super('', *)
25
+ @buffer = Parser::Source::Buffer.new(path, source: source)
26
+ end
27
+
28
+ def markdown_preprocessor?
29
+ Markdown.file?(path) && respond_to?(:markdown_document)
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ RuboCop::ProcessedSource.prepend(RuboCop::Spellcheck::ProseSource)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Spellcheck
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+ require_relative 'rubocop/spellcheck/version'
5
+ require_relative 'rubocop/spellcheck/plugin'
6
+ require_relative 'rubocop/spellcheck/checker'
7
+ require_relative 'rubocop/spellcheck/front_matter'
8
+ require_relative 'rubocop/spellcheck/markdown'
9
+ require_relative 'rubocop/spellcheck/prose_source'
10
+ require_relative 'rubocop/cop/spellcheck/spelling'
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-spellcheck
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniel (dB.) Doubrovkine
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: commonmarker
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: kotoshu
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.8
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: lint_roller
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.72'
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 1.72.1
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '1.72'
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.72.1
75
+ description: RuboCop spelling checks for Ruby comments, Markdown, and plain text,
76
+ using offline dictionaries.
77
+ email:
78
+ - dblock@dblock.org
79
+ executables: []
80
+ extensions: []
81
+ extra_rdoc_files: []
82
+ files:
83
+ - CHANGELOG.md
84
+ - LICENSE.txt
85
+ - README.md
86
+ - config/default.yml
87
+ - data/en/LICENSE
88
+ - data/en/README.md
89
+ - data/en/en.aff
90
+ - data/en/en.dic
91
+ - lib/rubocop-spellcheck.rb
92
+ - lib/rubocop/cop/spellcheck/spelling.rb
93
+ - lib/rubocop/spellcheck/checker.rb
94
+ - lib/rubocop/spellcheck/front_matter.rb
95
+ - lib/rubocop/spellcheck/markdown.rb
96
+ - lib/rubocop/spellcheck/plugin.rb
97
+ - lib/rubocop/spellcheck/prose_source.rb
98
+ - lib/rubocop/spellcheck/version.rb
99
+ homepage: https://github.com/dblock/rubocop-spellcheck
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ source_code_uri: https://github.com/dblock/rubocop-spellcheck
105
+ changelog_uri: https://github.com/dblock/rubocop-spellcheck/blob/master/CHANGELOG.md
106
+ bug_tracker_uri: https://github.com/dblock/rubocop-spellcheck/issues
107
+ rubygems_mfa_required: 'true'
108
+ default_lint_roller_plugin: RuboCop::Spellcheck::Plugin
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3.2'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.5.16
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Spellcheck Ruby comments, Markdown, and plain text with RuboCop.
128
+ test_files: []