-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #380 from crystal-ameba/add-documentation-admoniti…
…on-rule Add `Lint/DocumentationAdmonition` rule
- Loading branch information
Showing
8 changed files
with
224 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
Lint/DocumentationAdmonition: | ||
Timezone: UTC | ||
Admonitions: [FIXME, BUG] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,9 @@ jobs: | |
runs-on: ${{ matrix.os }} | ||
|
||
steps: | ||
- name: Set timezone to UTC | ||
uses: szenius/[email protected] | ||
|
||
- name: Install Crystal | ||
uses: crystal-lang/install-crystal@v1 | ||
with: | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
require "../../../spec_helper" | ||
|
||
module Ameba::Rule::Lint | ||
subject = DocumentationAdmonition.new | ||
|
||
describe DocumentationAdmonition do | ||
it "passes for comments with admonition mid-word/sentence" do | ||
subject.admonitions.each do |admonition| | ||
expect_no_issues subject, <<-CRYSTAL | ||
# Mentioning #{admonition} mid-sentence | ||
# x#{admonition}x | ||
# x#{admonition} | ||
# #{admonition}x | ||
CRYSTAL | ||
end | ||
end | ||
|
||
it "fails for comments with admonition" do | ||
subject.admonitions.each do |admonition| | ||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition}: Single-line comment | ||
# ^{} error: Found a #{admonition} admonition in a comment | ||
CRYSTAL | ||
|
||
expect_issue subject, <<-CRYSTAL | ||
# Text before ... | ||
# #{admonition}(some context): Part of multi-line comment | ||
# ^{} error: Found a #{admonition} admonition in a comment | ||
# Text after ... | ||
CRYSTAL | ||
|
||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition} | ||
# ^{} error: Found a #{admonition} admonition in a comment | ||
if rand > 0.5 | ||
end | ||
CRYSTAL | ||
end | ||
end | ||
|
||
context "with date" do | ||
it "passes for admonitions with future date" do | ||
subject.admonitions.each do |admonition| | ||
future_date = (Time.utc + 21.days).to_s(format: "%F") | ||
expect_no_issues subject, <<-CRYSTAL | ||
# #{admonition}(#{future_date}): sth in the future | ||
CRYSTAL | ||
end | ||
end | ||
|
||
it "fails for admonitions with past date" do | ||
subject.admonitions.each do |admonition| | ||
past_date = (Time.utc - 21.days).to_s(format: "%F") | ||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition}(#{past_date}): sth in the past | ||
# ^{} error: Found a #{admonition} admonition in a comment (21 days past) | ||
CRYSTAL | ||
end | ||
end | ||
|
||
it "fails for admonitions with yesterday's date" do | ||
subject.admonitions.each do |admonition| | ||
yesterday_date = (Time.utc - 1.day).to_s(format: "%F") | ||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition}(#{yesterday_date}): sth in the past | ||
# ^{} error: Found a #{admonition} admonition in a comment (1 day past) | ||
CRYSTAL | ||
end | ||
end | ||
|
||
it "fails for admonitions with today's date" do | ||
subject.admonitions.each do |admonition| | ||
today_date = Time.utc.to_s(format: "%F") | ||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition}(#{today_date}): sth in the past | ||
# ^{} error: Found a #{admonition} admonition in a comment (today is the day!) | ||
CRYSTAL | ||
end | ||
end | ||
|
||
it "fails for admonitions with invalid date" do | ||
subject.admonitions.each do |admonition| | ||
expect_issue subject, <<-CRYSTAL | ||
# #{admonition}(0000-00-00): sth wrong | ||
# ^{} error: #{admonition} admonition error: Invalid time: "0000-00-00" | ||
CRYSTAL | ||
end | ||
end | ||
end | ||
|
||
context "properties" do | ||
describe "#admonitions" do | ||
it "lets setting custom admonitions" do | ||
rule = DocumentationAdmonition.new | ||
rule.admonitions = %w[FOO BAR] | ||
|
||
rule.admonitions.each do |admonition| | ||
expect_issue rule, <<-CRYSTAL | ||
# #{admonition} | ||
# ^{} error: Found a #{admonition} admonition in a comment | ||
CRYSTAL | ||
end | ||
|
||
subject.admonitions.each do |admonition| | ||
expect_no_issues rule, <<-CRYSTAL | ||
# #{admonition} | ||
CRYSTAL | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
module Ameba::Rule::Lint | ||
# A rule that reports documentation admonitions. | ||
# | ||
# Optionally, these can fail at an appropriate time. | ||
# | ||
# ``` | ||
# def get_user(id) | ||
# # TODO(2024-04-24) Fix this hack when the database migration is complete | ||
# if id < 1_000_000 | ||
# v1_api_call(id) | ||
# else | ||
# v2_api_call(id) | ||
# end | ||
# end | ||
# ``` | ||
# | ||
# `TODO` comments are used to remind yourself of source code related things. | ||
# | ||
# The premise here is that `TODO` should be dealt with in the near future | ||
# and are therefore reported by Ameba. | ||
# | ||
# `FIXME` comments are used to indicate places where source code needs fixing. | ||
# | ||
# The premise here is that `FIXME` should indeed be fixed as soon as possible | ||
# and are therefore reported by Ameba. | ||
# | ||
# YAML configuration example: | ||
# | ||
# ``` | ||
# Lint/DocumentationAdmonition: | ||
# Enabled: true | ||
# Admonitions: [TODO, FIXME, BUG] | ||
# Timezone: UTC | ||
# ``` | ||
class DocumentationAdmonition < Base | ||
properties do | ||
description "Reports documentation admonitions" | ||
admonitions %w[TODO FIXME BUG] | ||
timezone "UTC" | ||
end | ||
|
||
MSG = "Found a %s admonition in a comment" | ||
MSG_LATE = "Found a %s admonition in a comment (%s)" | ||
MSG_ERR = "%s admonition error: %s" | ||
|
||
@[YAML::Field(ignore: true)] | ||
private getter location : Time::Location { | ||
Time::Location.load(self.timezone) | ||
} | ||
|
||
def test(source) | ||
Tokenizer.new(source).run do |token| | ||
next unless token.type.comment? | ||
next unless doc = token.value.to_s | ||
|
||
pattern = | ||
/^#\s*(?<admonition>#{Regex.union(admonitions)})(?:\((?<context>.+?)\))?(?:\W+|$)/m | ||
|
||
matches = doc.scan(pattern) | ||
matches.each do |match| | ||
admonition = match["admonition"] | ||
begin | ||
case expr = match["context"]?.presence | ||
when /\A\d{4}-\d{2}-\d{2}\Z/ # date | ||
# ameba:disable Lint/NotNil | ||
date = Time.parse(expr.not_nil!, "%F", location) | ||
issue_for_date source, token, admonition, date | ||
when /\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?\Z/ # date + time (no tz) | ||
# ameba:disable Lint/NotNil | ||
date = Time.parse(expr.not_nil!, "%F #{$1?.presence ? "%T" : "%R"}", location) | ||
issue_for_date source, token, admonition, date | ||
else | ||
issue_for token, MSG % admonition | ||
end | ||
rescue ex | ||
issue_for token, MSG_ERR % {admonition, "#{ex}: #{expr.inspect}"} | ||
end | ||
end | ||
end | ||
end | ||
|
||
private def issue_for_date(source, node, admonition, date) | ||
diff = Time.utc - date.to_utc | ||
|
||
return if diff.negative? | ||
|
||
past = case diff | ||
when 0.seconds..1.day then "today is the day!" | ||
when 1.day..2.days then "1 day past" | ||
else "#{diff.total_days.to_i} days past" | ||
end | ||
|
||
issue_for node, MSG_LATE % {admonition, past} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters