chutney 2.2.1 → 3.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +16 -0
- data/.rubocop.yml +10 -3
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +7 -1
- data/Rakefile +8 -8
- data/chutney.gemspec +13 -6
- data/config/{chutney.yml → chutney_defaults.yml} +2 -0
- data/config/cucumber.yml +1 -0
- data/docs/index.md +1 -1
- data/docs/usage/rules.md +34 -64
- data/exe/chutney +23 -3
- data/lib/chutney.rb +18 -14
- data/lib/chutney/configuration.rb +9 -2
- data/lib/chutney/formatter.rb +6 -5
- data/lib/chutney/formatter/json_formatter.rb +2 -0
- data/lib/chutney/formatter/pie_formatter.rb +10 -13
- data/lib/chutney/formatter/rainbow_formatter.rb +13 -13
- data/lib/chutney/issue.rb +2 -0
- data/lib/chutney/linter.rb +92 -86
- data/lib/chutney/linter/avoid_full_stop.rb +4 -4
- data/lib/chutney/linter/avoid_outline_for_single_example.rb +7 -5
- data/lib/chutney/linter/avoid_scripting.rb +8 -6
- data/lib/chutney/linter/avoid_typographers_quotes.rb +16 -14
- data/lib/chutney/linter/background_does_more_than_setup.rb +8 -7
- data/lib/chutney/linter/background_requires_multiple_scenarios.rb +7 -4
- data/lib/chutney/linter/bad_scenario_name.rb +6 -4
- data/lib/chutney/linter/empty_feature_file.rb +2 -0
- data/lib/chutney/linter/file_name_differs_feature_name.rb +7 -5
- data/lib/chutney/linter/givens_after_background.rb +7 -8
- data/lib/chutney/linter/invalid_file_name.rb +3 -1
- data/lib/chutney/linter/invalid_step_flow.rb +9 -9
- data/lib/chutney/linter/missing_example_name.rb +9 -9
- data/lib/chutney/linter/missing_feature_description.rb +5 -4
- data/lib/chutney/linter/missing_feature_name.rb +5 -4
- data/lib/chutney/linter/missing_scenario_name.rb +4 -6
- data/lib/chutney/linter/missing_test_action.rb +4 -2
- data/lib/chutney/linter/missing_verification.rb +4 -2
- data/lib/chutney/linter/required_tags_starts_with.rb +7 -6
- data/lib/chutney/linter/same_tag_different_case.rb +37 -0
- data/lib/chutney/linter/same_tag_for_all_scenarios.rb +20 -19
- data/lib/chutney/linter/scenario_names_match.rb +6 -6
- data/lib/chutney/linter/tag_used_multiple_times.rb +3 -1
- data/lib/chutney/linter/too_clumsy.rb +4 -2
- data/lib/chutney/linter/too_long_step.rb +6 -4
- data/lib/chutney/linter/too_many_different_tags.rb +10 -8
- data/lib/chutney/linter/too_many_steps.rb +6 -4
- data/lib/chutney/linter/too_many_tags.rb +5 -3
- data/lib/chutney/linter/unique_scenario_names.rb +5 -5
- data/lib/chutney/linter/unknown_variable.rb +15 -15
- data/lib/chutney/linter/unused_variable.rb +15 -16
- data/lib/chutney/linter/use_background.rb +20 -19
- data/lib/chutney/linter/use_outline.rb +15 -14
- data/lib/chutney/version.rb +3 -1
- data/lib/config/locales/en.yml +3 -0
- data/spec/chutney_spec.rb +11 -9
- data/spec/spec_helper.rb +2 -0
- metadata +21 -16
@@ -1,5 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'delegate'
|
2
|
-
|
4
|
+
|
5
|
+
module Chutney
|
3
6
|
# gherkin_lint configuration object
|
4
7
|
class Configuration < SimpleDelegator
|
5
8
|
def initialize(path)
|
@@ -18,7 +21,11 @@ module Chutney
|
|
18
21
|
end
|
19
22
|
|
20
23
|
def load_user_configuration
|
21
|
-
|
24
|
+
config_files = ['chutney.yml', '.chutney.yml'].map do |fname|
|
25
|
+
Dir.glob(File.join(Dir.pwd, '**', fname))
|
26
|
+
end.flatten
|
27
|
+
|
28
|
+
config_file = config_files.first
|
22
29
|
merge_config(config_file) if !config_file.nil? && File.exist?(config_file)
|
23
30
|
end
|
24
31
|
|
data/lib/chutney/formatter.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chutney
|
2
4
|
# base class for all formatters
|
3
5
|
class Formatter
|
4
6
|
attr_accessor :results
|
5
|
-
|
7
|
+
|
6
8
|
def initialize
|
7
9
|
@results = {}
|
8
|
-
end
|
9
|
-
|
10
|
+
end
|
11
|
+
|
10
12
|
def files
|
11
13
|
results.map { |k, _v| k }
|
12
14
|
end
|
13
|
-
|
15
|
+
|
14
16
|
def files_with_issues
|
15
17
|
results.filter { |_k, v| v.any? { |r| r[:issues].count.positive? } }
|
16
18
|
end
|
17
|
-
|
18
19
|
end
|
19
20
|
end
|
@@ -1,13 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pastel'
|
2
4
|
require 'tty/pie'
|
3
5
|
|
4
6
|
module Chutney
|
5
7
|
# format results as pie charts
|
6
8
|
class PieFormatter < Formatter
|
7
|
-
def initialize
|
8
|
-
super
|
9
|
-
end
|
10
|
-
|
11
9
|
def format
|
12
10
|
data = top_offences.map do |offence|
|
13
11
|
{
|
@@ -19,14 +17,14 @@ module Chutney
|
|
19
17
|
end
|
20
18
|
print_report(data)
|
21
19
|
end
|
22
|
-
|
20
|
+
|
23
21
|
def print_report(data)
|
24
22
|
return if data.empty?
|
25
23
|
|
26
24
|
print TTY::Pie.new(data: data, radius: 8, legend: { format: '%<label>s %<name>s %<value>i' })
|
27
25
|
puts
|
28
26
|
end
|
29
|
-
|
27
|
+
|
30
28
|
def top_offences
|
31
29
|
offence = Hash.new(0)
|
32
30
|
files_with_issues.each do |_file, linter|
|
@@ -36,9 +34,9 @@ module Chutney
|
|
36
34
|
end
|
37
35
|
offence.reject { |_k, v| v.zero? }.sort_by { |_linter, count| -count }
|
38
36
|
end
|
39
|
-
|
37
|
+
|
40
38
|
def char_loop
|
41
|
-
@char_looper ||= Fiber.new do
|
39
|
+
@char_looper ||= Fiber.new do
|
42
40
|
chars = %w[• x + @ * / -]
|
43
41
|
current = 0
|
44
42
|
loop do
|
@@ -49,10 +47,10 @@ module Chutney
|
|
49
47
|
end
|
50
48
|
@char_looper.resume
|
51
49
|
end
|
52
|
-
|
50
|
+
|
53
51
|
def colour_loop
|
54
|
-
@colour_looper ||= Fiber.new do
|
55
|
-
colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
|
52
|
+
@colour_looper ||= Fiber.new do
|
53
|
+
colours = %i[bright_cyan bright_magenta bright_yellow bright_green]
|
56
54
|
current = 0
|
57
55
|
loop do
|
58
56
|
current = 0 if current >= colours.count
|
@@ -62,7 +60,7 @@ module Chutney
|
|
62
60
|
end
|
63
61
|
@colour_looper.resume
|
64
62
|
end
|
65
|
-
|
63
|
+
|
66
64
|
def put_summary
|
67
65
|
pastel = Pastel.new
|
68
66
|
print "#{files.count} features inspected, "
|
@@ -72,6 +70,5 @@ module Chutney
|
|
72
70
|
puts pastel.red("#{files_with_issues.count} taste nasty")
|
73
71
|
end
|
74
72
|
end
|
75
|
-
|
76
73
|
end
|
77
74
|
end
|
@@ -1,39 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'pastel'
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# pretty formatter
|
5
7
|
class RainbowFormatter < Formatter
|
6
|
-
|
7
8
|
def initialize
|
8
9
|
super
|
9
|
-
|
10
|
+
|
10
11
|
@pastel = Pastel.new
|
11
12
|
end
|
12
|
-
|
13
|
-
def format
|
13
|
+
|
14
|
+
def format
|
14
15
|
files_with_issues.each do |file, linter|
|
15
16
|
put_file(file)
|
16
17
|
linter.filter { |l| !l[:issues].empty? }.each do |linter_with_issues|
|
17
|
-
|
18
18
|
put_linter(linter_with_issues)
|
19
|
-
linter_with_issues[:issues].each { |i| put_issue(i) }
|
19
|
+
linter_with_issues[:issues].each { |i| put_issue(file, i) }
|
20
20
|
end
|
21
21
|
end
|
22
22
|
put_summary
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
def put_file(file)
|
26
26
|
puts @pastel.cyan(file.to_s)
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def put_linter(linter)
|
30
30
|
puts @pastel.red(" #{linter[:linter]}")
|
31
31
|
end
|
32
|
-
|
33
|
-
def put_issue(issue)
|
34
|
-
puts " #{
|
32
|
+
|
33
|
+
def put_issue(file, issue)
|
34
|
+
puts " #{issue[:message]}"
|
35
|
+
puts " #{@pastel.dim file.to_s}:#{@pastel.dim(issue.dig(:location, :line))}"
|
35
36
|
end
|
36
|
-
|
37
|
+
|
37
38
|
def put_summary
|
38
39
|
print "#{files.count} features inspected, "
|
39
40
|
if files_with_issues.count.zero?
|
@@ -42,6 +43,5 @@ module Chutney
|
|
42
43
|
puts @pastel.red("#{files_with_issues.count} taste nasty")
|
43
44
|
end
|
44
45
|
end
|
45
|
-
|
46
46
|
end
|
47
47
|
end
|
data/lib/chutney/issue.rb
CHANGED
data/lib/chutney/linter.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# gherkin utilities
|
2
4
|
|
3
5
|
module Chutney
|
4
6
|
# base class for all linters
|
5
7
|
class Linter
|
6
8
|
attr_accessor :issues
|
7
|
-
attr_reader :filename
|
8
|
-
|
9
|
-
|
9
|
+
attr_reader :filename, :configuration
|
10
|
+
|
10
11
|
Lint = Struct.new(:message, :gherkin_type, :location, :feature, :scenario, :step, keyword_init: true)
|
11
12
|
|
12
13
|
def self.descendants
|
@@ -18,77 +19,81 @@ module Chutney
|
|
18
19
|
@filename = filename
|
19
20
|
@issues = []
|
20
21
|
@configuration = configuration
|
21
|
-
language = @content.dig(:feature, :language) || 'en'
|
22
|
-
@dialect = Gherkin::Dialect.for(language)
|
23
22
|
end
|
24
23
|
|
25
24
|
def lint
|
26
25
|
raise 'not implemented'
|
27
26
|
end
|
28
|
-
|
27
|
+
|
29
28
|
def and_word?(word)
|
30
|
-
|
29
|
+
dialect_word(:and).include?(word)
|
31
30
|
end
|
32
|
-
|
31
|
+
|
33
32
|
def background_word?(word)
|
34
|
-
|
33
|
+
dialect_word(:background).include?(word)
|
35
34
|
end
|
36
|
-
|
35
|
+
|
37
36
|
def but_word?(word)
|
38
|
-
|
37
|
+
dialect_word(:but).include?(word)
|
39
38
|
end
|
40
|
-
|
39
|
+
|
41
40
|
def examples_word?(word)
|
42
|
-
|
41
|
+
dialect_word(:examples).include?(word)
|
43
42
|
end
|
44
|
-
|
43
|
+
|
45
44
|
def feature_word?(word)
|
46
|
-
|
45
|
+
dialect_word(:feature).include?(word)
|
47
46
|
end
|
48
|
-
|
47
|
+
|
49
48
|
def given_word?(word)
|
50
|
-
|
49
|
+
dialect_word(:given).include?(word)
|
51
50
|
end
|
52
|
-
|
51
|
+
|
53
52
|
def scenario_outline_word?(word)
|
54
|
-
|
53
|
+
dialect_word(:scenarioOutline).include?(word)
|
55
54
|
end
|
56
|
-
|
55
|
+
|
57
56
|
def then_word?(word)
|
58
|
-
|
57
|
+
dialect_word(:then).include?(word)
|
59
58
|
end
|
60
|
-
|
59
|
+
|
61
60
|
def when_word?(word)
|
62
|
-
|
61
|
+
dialect_word(:when).include?(word)
|
62
|
+
end
|
63
|
+
|
64
|
+
def dialect_word(word)
|
65
|
+
CukeModeler::Parsing.dialects[dialect][word.to_s].map(&:strip)
|
66
|
+
end
|
67
|
+
|
68
|
+
def dialect
|
69
|
+
@content.feature&.parsing_data&.dig(:language) || 'en'
|
63
70
|
end
|
64
|
-
|
65
|
-
def tags_for(element)
|
66
|
-
return [] unless element.include? :tags
|
67
71
|
|
68
|
-
|
72
|
+
def tags_for(element)
|
73
|
+
element.tags.map { |tag| tag.name[1..] }
|
69
74
|
end
|
70
|
-
|
71
|
-
def add_issue(message, feature = nil, scenario = nil,
|
75
|
+
|
76
|
+
def add_issue(message, feature = nil, scenario = nil, item = nil)
|
72
77
|
issues << Lint.new(
|
73
78
|
message: message,
|
74
|
-
gherkin_type: type(feature, scenario,
|
75
|
-
location: location(feature, scenario,
|
76
|
-
feature: feature
|
77
|
-
scenario: scenario
|
78
|
-
step:
|
79
|
+
gherkin_type: type(feature, scenario, item),
|
80
|
+
location: location(feature, scenario, item),
|
81
|
+
feature: feature&.name,
|
82
|
+
scenario: scenario&.name,
|
83
|
+
step: item&.parsing_data&.dig(:name)
|
79
84
|
).to_h
|
80
85
|
end
|
81
|
-
|
86
|
+
|
82
87
|
def location(feature, scenario, step)
|
83
88
|
if step
|
84
|
-
step[:location]
|
89
|
+
step.parsing_data[:location]
|
85
90
|
elsif scenario
|
86
|
-
scenario
|
87
|
-
else
|
88
|
-
feature ? feature[:location] : 0
|
91
|
+
scenario.parsing_data.dig(:scenario, :location) || scenario.parsing_data.dig(:background, :location)
|
92
|
+
else
|
93
|
+
feature ? feature.parsing_data[:location] : { line: 0, column: 0 }
|
89
94
|
end
|
90
95
|
end
|
91
|
-
|
96
|
+
|
92
97
|
def type(_feature, scenario, step)
|
93
98
|
if step
|
94
99
|
:step
|
@@ -99,101 +104,102 @@ module Chutney
|
|
99
104
|
|
100
105
|
def feature
|
101
106
|
if block_given?
|
102
|
-
yield(@content
|
107
|
+
yield(@content.feature) if @content.feature
|
103
108
|
else
|
104
|
-
@content
|
109
|
+
@content.feature
|
105
110
|
end
|
106
111
|
end
|
107
|
-
|
108
|
-
def elements
|
112
|
+
|
113
|
+
def elements
|
109
114
|
return [] unless feature
|
110
|
-
|
111
|
-
if block_given?
|
112
|
-
feature
|
115
|
+
|
116
|
+
if block_given?
|
117
|
+
feature.children.each do |child|
|
113
118
|
next if off_switch?(child)
|
114
|
-
|
119
|
+
|
115
120
|
yield(feature, child)
|
116
121
|
end
|
117
122
|
else
|
118
|
-
feature
|
123
|
+
feature.children
|
119
124
|
end
|
120
125
|
end
|
121
126
|
|
122
127
|
def off_switch?(element = feature)
|
123
|
-
off_switch = element
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
128
|
+
off_switch = element.tags
|
129
|
+
.map(&:name)
|
130
|
+
.then { |tags| tags || [] }
|
131
|
+
.filter { |tag_name| tag_name == "@disable#{linter_name}" }
|
132
|
+
.count
|
133
|
+
.positive?
|
129
134
|
off_switch ||= off_switch?(feature) unless element == feature
|
130
135
|
off_switch
|
131
136
|
end
|
132
|
-
|
137
|
+
|
133
138
|
def background
|
139
|
+
return unless feature&.background
|
140
|
+
return if off_switch?(feature)
|
141
|
+
|
134
142
|
if block_given?
|
135
|
-
|
136
|
-
next unless child[:type] == :Background
|
137
|
-
|
138
|
-
yield(feature, child)
|
139
|
-
end
|
143
|
+
yield(feature, feature.background)
|
140
144
|
else
|
141
|
-
|
145
|
+
feature.background
|
142
146
|
end
|
143
147
|
end
|
144
|
-
|
148
|
+
|
145
149
|
def scenarios
|
146
150
|
if block_given?
|
147
|
-
|
148
|
-
next
|
149
|
-
|
150
|
-
yield(feature,
|
151
|
+
feature&.tests&.each do |test|
|
152
|
+
next if off_switch?(test)
|
153
|
+
|
154
|
+
yield(feature, test)
|
151
155
|
end
|
156
|
+
|
152
157
|
else
|
153
|
-
|
158
|
+
feature&.tests
|
154
159
|
end
|
155
160
|
end
|
156
|
-
|
161
|
+
|
157
162
|
def filled_scenarios
|
158
163
|
if block_given?
|
159
164
|
scenarios do |feature, scenario|
|
160
|
-
next
|
161
|
-
|
162
|
-
|
165
|
+
next if scenario.steps.empty?
|
166
|
+
|
163
167
|
yield(feature, scenario)
|
164
168
|
end
|
165
169
|
else
|
166
|
-
scenarios.filter { |s| !s
|
170
|
+
scenarios ? scenarios.filter { |s| !s.steps.empty? } : []
|
167
171
|
end
|
168
172
|
end
|
169
|
-
|
173
|
+
|
170
174
|
def steps
|
171
|
-
|
172
|
-
next
|
173
|
-
|
174
|
-
|
175
|
+
feature&.tests&.each do |test|
|
176
|
+
next if off_switch?(test)
|
177
|
+
|
178
|
+
test.steps.each do |step|
|
179
|
+
yield(feature, test, step)
|
180
|
+
end
|
175
181
|
end
|
176
182
|
end
|
177
183
|
|
178
184
|
def self.linter_name
|
179
185
|
name.split('::').last
|
180
186
|
end
|
181
|
-
|
187
|
+
|
182
188
|
def linter_name
|
183
189
|
self.class.linter_name
|
184
190
|
end
|
185
191
|
|
186
192
|
def render_step(step)
|
187
|
-
value = "#{step
|
188
|
-
value += render_step_argument
|
193
|
+
value = "#{step.keyword} #{step.text}"
|
194
|
+
value += render_step_argument(step.block) if step.block
|
189
195
|
value
|
190
196
|
end
|
191
|
-
|
197
|
+
|
192
198
|
def render_step_argument(argument)
|
193
|
-
return "\n#{argument
|
194
|
-
|
195
|
-
result = argument
|
196
|
-
"|#{row
|
199
|
+
return "\n#{argument.content}" if argument.is_a?(CukeModeler::DocString)
|
200
|
+
|
201
|
+
result = argument.rows.map do |row|
|
202
|
+
"|#{row.cells.map(&:value).join '|'}|"
|
197
203
|
end.join "\n"
|
198
204
|
"\n#{result}"
|
199
205
|
end
|