195 lines
5.5 KiB
Crystal
195 lines
5.5 KiB
Crystal
require "./annotated_source"
|
|
require "./util"
|
|
|
|
# Mixin for `expect_issue` and `expect_no_issues`
|
|
#
|
|
# This mixin makes it easier to specify strict issue expectations
|
|
# in a declarative and visual fashion. Just type out the code that
|
|
# should generate an issue, annotate code by writing '^'s
|
|
# underneath each character that should be highlighted, and follow
|
|
# the carets with a string (separated by a space) that is the
|
|
# message of the issue. You can include multiple issues in
|
|
# one code snippet.
|
|
#
|
|
# Usage:
|
|
#
|
|
# expect_issue subject, <<-CRYSTAL
|
|
# a do
|
|
# b
|
|
# end.c
|
|
# # ^^^ error: Avoid chaining a method call on a do...end block.
|
|
# CRYSTAL
|
|
#
|
|
# Equivalent assertion without `expect_issue`:
|
|
#
|
|
# source = Source.new <<-CRYSTAL, "source.cr"
|
|
# a do
|
|
# b
|
|
# end.c
|
|
# CRYSTAL
|
|
# subject.catch(source).should_not be_valid
|
|
# source.issues.size.should be(1)
|
|
#
|
|
# issue = source.issues.first
|
|
# issue.location.to_s.should eq "source.cr:3:1"
|
|
# issue.end_location.to_s.should eq "source.cr:3:5"
|
|
# issue.message.should eq(
|
|
# "Avoid chaining a method call on a do...end block."
|
|
# )
|
|
#
|
|
# Autocorrection can be tested using `expect_correction` after
|
|
# `expect_issue`.
|
|
#
|
|
# source = expect_issue subject, <<-CRYSTAL
|
|
# x % 2 == 0
|
|
# # ^^^^^^^^ error: Replace with `Int#even?`.
|
|
# CRYSTAL
|
|
#
|
|
# expect_correction source, <<-CRYSTAL
|
|
# x.even?
|
|
# CRYSTAL
|
|
#
|
|
# If you do not want to specify an issue then use the
|
|
# companion method `expect_no_issues`. This method is a much
|
|
# simpler assertion since it just inspects the code and checks
|
|
# that there were no issues. The `expect_issue` method has
|
|
# to do more work by parsing out lines that contain carets.
|
|
#
|
|
# If the code produces an issue that could not be auto-corrected, you can
|
|
# use `expect_no_corrections` after `expect_issue`.
|
|
#
|
|
# source = expect_issue subject, <<-CRYSTAL
|
|
# a do
|
|
# b
|
|
# end.c
|
|
# # ^^^ error: Avoid chaining a method call on a do...end block.
|
|
# CRYSTAL
|
|
#
|
|
# expect_no_corrections source
|
|
#
|
|
# If your code has variables of different lengths, you can use `%{foo}`,
|
|
# `^{foo}`, and `_{foo}` to format your template; you can also abbreviate
|
|
# issue messages with `[...]`:
|
|
#
|
|
# %w[raise fail].each do |keyword|
|
|
# expect_issue subject, <<-CRYSTAL, keyword: keyword
|
|
# %{keyword} Exception.new(msg)
|
|
# # ^{keyword}^^^^^^^^^^^^^^^^^ error: Redundant `Exception.new` [...]
|
|
# CRYSTAL
|
|
#
|
|
# %w[has_one has_many].each do |type|
|
|
# expect_issue subject, <<-CRYSTAL, type: type
|
|
# class Book
|
|
# %{type} :chapter, foreign_key: "book_id"
|
|
# _{type} # ^^^^^^^^^^^^^^^^^^^^^^ error: Specifying the default [...]
|
|
# end
|
|
# CRYSTAL
|
|
# end
|
|
#
|
|
# If you need to specify an issue on a blank line, use the empty `^{}` marker:
|
|
#
|
|
# expect_issue subject, <<-CRYSTAL
|
|
#
|
|
# # ^{} error: Missing frozen string literal comment.
|
|
# puts 1
|
|
# CRYSTAL
|
|
module Ameba::Spec::ExpectIssue
|
|
include Spec::Util
|
|
|
|
def expect_issue(rules : Rule::Base | Enumerable(Rule::Base),
|
|
annotated_code : String,
|
|
path = "",
|
|
*,
|
|
file = __FILE__,
|
|
line = __LINE__,
|
|
**replacements)
|
|
annotated_code = format_issue(annotated_code, **replacements)
|
|
expected_annotations = AnnotatedSource.parse(annotated_code)
|
|
lines = expected_annotations.lines
|
|
code = lines.join('\n')
|
|
|
|
if code == annotated_code
|
|
raise "Use `expect_no_issues` to assert that no issues are found"
|
|
end
|
|
|
|
source, actual_annotations = actual_annotations(rules, code, path, lines)
|
|
unless actual_annotations == expected_annotations
|
|
fail <<-MSG, file, line
|
|
Expected:
|
|
|
|
#{expected_annotations}
|
|
|
|
Got:
|
|
|
|
#{actual_annotations}
|
|
MSG
|
|
end
|
|
|
|
source
|
|
end
|
|
|
|
def expect_correction(source, correction, *, file = __FILE__, line = __LINE__)
|
|
raise "Use `expect_no_corrections` if the code will not change" unless source.correct?
|
|
return if correction == source.code
|
|
|
|
fail <<-MSG, file, line
|
|
Expected correction:
|
|
|
|
#{correction}
|
|
|
|
Got:
|
|
|
|
#{source.code}
|
|
MSG
|
|
end
|
|
|
|
def expect_no_corrections(source, *, file = __FILE__, line = __LINE__)
|
|
return unless source.correct?
|
|
|
|
fail <<-MSG, file, line
|
|
Expected no corrections, but got:
|
|
|
|
#{source.code}
|
|
MSG
|
|
end
|
|
|
|
def expect_no_issues(rules : Rule::Base | Enumerable(Rule::Base),
|
|
code : String,
|
|
path = "",
|
|
*,
|
|
file = __FILE__,
|
|
line = __LINE__)
|
|
lines = code.split('\n') # must preserve trailing newline
|
|
|
|
_, actual_annotations = actual_annotations(rules, code, path, lines)
|
|
return if actual_annotations.to_s == code
|
|
|
|
fail <<-MSG, file, line
|
|
Expected no issues, but got:
|
|
|
|
#{actual_annotations}
|
|
MSG
|
|
end
|
|
|
|
private def actual_annotations(rules, code, path, lines)
|
|
source = Source.new(code, path, normalize: false)
|
|
if rules.is_a?(Enumerable)
|
|
rules.each(&.catch(source))
|
|
else
|
|
rules.catch(source)
|
|
end
|
|
{source, AnnotatedSource.new(lines, source.issues)}
|
|
end
|
|
|
|
private def format_issue(code, **replacements)
|
|
replacements.each do |keyword, value|
|
|
value = value.to_s
|
|
code = code
|
|
.gsub("%{#{keyword}}", value)
|
|
.gsub("^{#{keyword}}", "^" * value.size)
|
|
.gsub("_{#{keyword}}", " " * value.size)
|
|
end
|
|
code
|
|
end
|
|
end
|