Skip to content

Commit

Permalink
Merge pull request #381 from crystal-ameba/add-typos-rule
Browse files Browse the repository at this point in the history
Add `Lint/Typos` rule
  • Loading branch information
Sija authored Nov 8, 2023
2 parents 9f6615b + 891cad2 commit 0c67457
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .ameba.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Lint/DocumentationAdmonition:
Timezone: UTC
Admonitions: [FIXME, BUG]

Lint/Typos:
Excluded:
- spec/ameba/rule/lint/typos_spec.cr
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
- name: Install dependencies
run: shards install

- name: Install typos-cli
if: matrix.os == 'macos-latest'
run: brew install typos-cli

- name: Run specs
run: crystal spec

Expand Down
32 changes: 32 additions & 0 deletions spec/ameba/rule/lint/typos_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "../../../spec_helper"

private def check_typos_bin!
unless Ameba::Rule::Lint::Typos::BIN_PATH
pending! "`typos` executable is not available"
end
end

module Ameba::Rule::Lint
subject = Typos.new
.tap(&.fail_on_error = true)

describe Typos do
it "reports typos" do
check_typos_bin!

source = expect_issue subject, <<-CRYSTAL
# method with no arugments
# ^^^^^^^^^ error: Typo found: arugments -> arguments
def tpos
# ^^^^ error: Typo found: tpos -> typos
end
CRYSTAL

expect_correction source, <<-CRYSTAL
# method with no arguments
def typos
end
CRYSTAL
end
end
end
96 changes: 96 additions & 0 deletions src/ameba/rule/lint/typos.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module Ameba::Rule::Lint
# A rule that reports typos found in source files.
#
# NOTE: Needs [typos](https://github.com/crate-ci/typos) CLI tool.
# NOTE: See the chapter on [false positives](https://github.com/crate-ci/typos#false-positives).
#
# YAML configuration example:
#
# ```
# Lint/Typos:
# Enabled: true
# BinPath: ~
# FailOnError: false
# ```
class Typos < Base
properties do
description "Reports typos found in source files"
bin_path nil.as(String?)
fail_on_error false
end

MSG = "Typo found: %s -> %s"

BIN_PATH = Process.find_executable("typos")

def bin_path : String?
@bin_path || BIN_PATH
end

def test(source : Source)
typos = typos_from(source)
typos.try &.each do |typo|
corrections = typo.corrections
message = MSG % {
typo.typo, corrections.join(" | "),
}
if corrections.size == 1
issue_for typo.location, typo.end_location, message do |corrector|
corrector.replace(typo.location, typo.end_location, corrections.first)
end
else
issue_for typo.location, typo.end_location, message
end
end
rescue ex
raise ex if fail_on_error?
end

private record Typo,
path : String,
typo : String,
corrections : Array(String),
location : {Int32, Int32},
end_location : {Int32, Int32} do
def self.parse(str) : self?
issue = JSON.parse(str)

return unless issue["type"] == "typo"

typo = issue["typo"].as_s
corrections = issue["corrections"].as_a.map(&.as_s)

return if typo.empty? || corrections.empty?

path = issue["path"].as_s
line_no = issue["line_num"].as_i
col_no = issue["byte_offset"].as_i + 1
end_col_no = col_no + typo.size - 1

new(path, typo, corrections,
{line_no, col_no}, {line_no, end_col_no})
end
end

protected def typos_from(source : Source) : Array(Typo)?
unless bin_path = self.bin_path
if fail_on_error?
raise RuntimeError.new "Could not find `typos` executable"
end
return
end
status = Process.run(bin_path, args: %w[--format json -],
input: IO::Memory.new(source.code),
output: output = IO::Memory.new,
)
return if status.success?

([] of Typo).tap do |typos|
# NOTE: `--format json` is actually JSON Lines (`jsonl`)
output.to_s.each_line do |line|
Typo.parse(line).try { |typo| typos << typo }
end
end
end
end
end

0 comments on commit 0c67457

Please sign in to comment.