chutney 2.2.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|