cuke_linter 1.0.0 → 1.0.1
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/CHANGELOG.md +7 -1
- data/README.md +8 -4
- data/cuke_linter.gemspec +23 -19
- data/exe/cuke_linter +3 -1
- data/lib/cuke_linter.rb +119 -182
- data/lib/cuke_linter/configuration.rb +45 -0
- data/lib/cuke_linter/default_linters.rb +32 -0
- data/lib/cuke_linter/formatters/pretty_formatter.rb +63 -35
- data/lib/cuke_linter/gherkin.rb +10 -0
- data/lib/cuke_linter/linter_registration.rb +32 -0
- data/lib/cuke_linter/linters/background_does_more_than_setup_linter.rb +1 -1
- data/lib/cuke_linter/linters/element_with_common_tags_linter.rb +23 -14
- data/lib/cuke_linter/linters/element_with_duplicate_tags_linter.rb +18 -13
- data/lib/cuke_linter/linters/element_with_too_many_tags_linter.rb +17 -11
- data/lib/cuke_linter/linters/feature_with_too_many_different_tags_linter.rb +1 -3
- data/lib/cuke_linter/linters/feature_without_description_linter.rb +1 -1
- data/lib/cuke_linter/linters/linter.rb +17 -13
- data/lib/cuke_linter/linters/step_with_too_many_characters_linter.rb +2 -2
- data/lib/cuke_linter/linters/test_should_use_background_linter.rb +23 -15
- data/lib/cuke_linter/linters/test_with_bad_name_linter.rb +4 -4
- data/lib/cuke_linter/linters/test_with_setup_step_as_final_step_linter.rb +1 -1
- data/lib/cuke_linter/version.rb +1 -1
- metadata +85 -19
@@ -0,0 +1,45 @@
|
|
1
|
+
module CukeLinter
|
2
|
+
|
3
|
+
# Mix-in module containing methods related to configuring linters
|
4
|
+
module Configuration
|
5
|
+
|
6
|
+
# Configures linters based on the given options
|
7
|
+
def load_configuration(config_file_path: nil, config: nil)
|
8
|
+
# TODO: define what happens if both a configuration file and a configuration are
|
9
|
+
# provided. Merge them or have direct config take precedence? Both?
|
10
|
+
|
11
|
+
unless config || config_file_path
|
12
|
+
config_file_path = "#{Dir.pwd}/.cuke_linter"
|
13
|
+
message = 'No configuration or configuration file given and no .cuke_linter file found'
|
14
|
+
raise message unless File.exist?(config_file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
config ||= YAML.load_file(config_file_path)
|
18
|
+
configure_linters(config, registered_linters)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
|
25
|
+
def configure_linters(configuration, linters)
|
26
|
+
common_config = configuration['AllLinters'] || {}
|
27
|
+
to_delete = []
|
28
|
+
|
29
|
+
linters.each_pair do |name, linter|
|
30
|
+
linter_config = configuration[name] || {}
|
31
|
+
final_config = common_config.merge(linter_config)
|
32
|
+
|
33
|
+
disabled = (final_config.key?('Enabled') && !final_config['Enabled'])
|
34
|
+
|
35
|
+
# Just save it for afterwards because modifying a collection while iterating through it is not a good idea
|
36
|
+
to_delete << name if disabled
|
37
|
+
|
38
|
+
linter.configure(final_config) if linter.respond_to?(:configure)
|
39
|
+
end
|
40
|
+
|
41
|
+
to_delete.each { |linter_name| unregister_linter(linter_name) }
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CukeLinter
|
2
|
+
|
3
|
+
# Long names inherently result in long lines
|
4
|
+
# rubocop:disable Metrics/LineLength
|
5
|
+
@original_linters = { 'BackgroundDoesMoreThanSetupLinter' => BackgroundDoesMoreThanSetupLinter.new,
|
6
|
+
'ElementWithCommonTagsLinter' => ElementWithCommonTagsLinter.new,
|
7
|
+
'ElementWithDuplicateTagsLinter' => ElementWithDuplicateTagsLinter.new,
|
8
|
+
'ElementWithTooManyTagsLinter' => ElementWithTooManyTagsLinter.new,
|
9
|
+
'ExampleWithoutNameLinter' => ExampleWithoutNameLinter.new,
|
10
|
+
'FeatureFileWithInvalidNameLinter' => FeatureFileWithInvalidNameLinter.new,
|
11
|
+
'FeatureFileWithMismatchedNameLinter' => FeatureFileWithMismatchedNameLinter.new,
|
12
|
+
'FeatureWithTooManyDifferentTagsLinter' => FeatureWithTooManyDifferentTagsLinter.new,
|
13
|
+
'FeatureWithoutDescriptionLinter' => FeatureWithoutDescriptionLinter.new,
|
14
|
+
'FeatureWithoutNameLinter' => FeatureWithoutNameLinter.new,
|
15
|
+
'FeatureWithoutScenariosLinter' => FeatureWithoutScenariosLinter.new,
|
16
|
+
'OutlineWithSingleExampleRowLinter' => OutlineWithSingleExampleRowLinter.new,
|
17
|
+
'SingleTestBackgroundLinter' => SingleTestBackgroundLinter.new,
|
18
|
+
'StepWithEndPeriodLinter' => StepWithEndPeriodLinter.new,
|
19
|
+
'StepWithTooManyCharactersLinter' => StepWithTooManyCharactersLinter.new,
|
20
|
+
'TestShouldUseBackgroundLinter' => TestShouldUseBackgroundLinter.new,
|
21
|
+
'TestWithActionStepAsFinalStepLinter' => TestWithActionStepAsFinalStepLinter.new,
|
22
|
+
'TestWithBadNameLinter' => TestWithBadNameLinter.new,
|
23
|
+
'TestWithNoActionStepLinter' => TestWithNoActionStepLinter.new,
|
24
|
+
'TestWithNoNameLinter' => TestWithNoNameLinter.new,
|
25
|
+
'TestWithNoVerificationStepLinter' => TestWithNoVerificationStepLinter.new,
|
26
|
+
'TestWithSetupStepAfterActionStepLinter' => TestWithSetupStepAfterActionStepLinter.new,
|
27
|
+
'TestWithSetupStepAfterVerificationStepLinter' => TestWithSetupStepAfterVerificationStepLinter.new,
|
28
|
+
'TestWithSetupStepAsFinalStepLinter' => TestWithSetupStepAsFinalStepLinter.new,
|
29
|
+
'TestWithTooManyStepsLinter' => TestWithTooManyStepsLinter.new }
|
30
|
+
# rubocop:enable Metrics/LineLength
|
31
|
+
|
32
|
+
end
|
@@ -1,55 +1,83 @@
|
|
1
1
|
module CukeLinter
|
2
2
|
|
3
3
|
# Formats linting data into organized, user readable text
|
4
|
-
|
5
4
|
class PrettyFormatter
|
6
5
|
|
7
6
|
# Formats the given linting data
|
8
7
|
def format(data)
|
9
|
-
|
8
|
+
format_data(categorize_problems(data), data.count)
|
9
|
+
end
|
10
|
+
|
10
11
|
|
11
|
-
|
12
|
-
|
12
|
+
private
|
13
|
+
|
14
|
+
|
15
|
+
def categorize_problems(data)
|
16
|
+
{}.tap do |categorized_problems|
|
17
|
+
data.each do |lint_item|
|
18
|
+
categorized_problems[lint_item[:linter]] ||= {}
|
19
|
+
categorized_problems[lint_item[:linter]][lint_item[:problem]] ||= []
|
20
|
+
categorized_problems[lint_item[:linter]][lint_item[:problem]] << lint_item[:location]
|
21
|
+
end
|
13
22
|
end
|
23
|
+
end
|
14
24
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
problems.each_pair do |problem, locations|
|
21
|
-
formatted_data << " #{problem}" + "\n"
|
22
|
-
|
23
|
-
sorted_locations = locations.sort do |a, b|
|
24
|
-
file_name_1 = a.match(/(.*?)(?::\d+)?$/)[1]
|
25
|
-
line_number_1 = a.match(/:(\d+)$/) ? a.match(/:(\d+)$/)[1].to_i : 0
|
26
|
-
file_name_2 = b.match(/(.*?)(?::\d+)?$/)[1]
|
27
|
-
line_number_2 = b.match(/:(\d+)$/) ? b.match(/:(\d+)$/)[1].to_i : 0
|
28
|
-
|
29
|
-
case
|
30
|
-
when (file_name_1 < file_name_2) ||
|
31
|
-
(file_name_1 == file_name_2) && (line_number_1 < line_number_2)
|
32
|
-
-1
|
33
|
-
when (file_name_1 > file_name_2) ||
|
34
|
-
(file_name_1 == file_name_2) && (line_number_1 > line_number_2)
|
35
|
-
1
|
36
|
-
else
|
37
|
-
0
|
38
|
-
end
|
39
|
-
end
|
25
|
+
def format_data(problem_data, problem_count)
|
26
|
+
''.tap do |formatted_data|
|
27
|
+
problem_data.each_pair do |linter, problems|
|
28
|
+
formatted_data << "#{linter}\n"
|
40
29
|
|
41
|
-
|
42
|
-
formatted_data << "
|
30
|
+
problems.each_pair do |problem, locations|
|
31
|
+
formatted_data << " #{problem}\n"
|
32
|
+
|
33
|
+
sort_locations(locations).each do |location|
|
34
|
+
formatted_data << " #{location}\n"
|
35
|
+
end
|
43
36
|
end
|
44
37
|
end
|
38
|
+
|
39
|
+
formatted_data << "\n" unless problem_count.zero?
|
40
|
+
formatted_data << "#{problem_count} issues found"
|
45
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def sort_locations(locations)
|
45
|
+
locations.sort do |a, b|
|
46
|
+
file_name_1 = a.match(/(.*?)(?::\d+)?$/)[1]
|
47
|
+
line_number_1 = a =~ /:\d+$/ ? a.match(/:(\d+)$/)[1].to_i : 0
|
48
|
+
file_name_2 = b.match(/(.*?)(?::\d+)?$/)[1]
|
49
|
+
line_number_2 = b =~ /:\d+$/ ? b.match(/:(\d+)$/)[1].to_i : 0
|
50
|
+
|
51
|
+
compare_locations(file_name_1, file_name_2, line_number_1, line_number_2)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def compare_locations(file_name_1, file_name_2, line_number_1, line_number_2)
|
56
|
+
if earlier_file(file_name_1, file_name_2) ||
|
57
|
+
same_file_earlier_line(file_name_1, file_name_2, line_number_1, line_number_2)
|
58
|
+
-1
|
59
|
+
elsif later_file(file_name_1, file_name_2) ||
|
60
|
+
same_file_later_line(file_name_1, file_name_2, line_number_1, line_number_2)
|
61
|
+
1
|
62
|
+
else
|
63
|
+
0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def earlier_file(file_name_1, file_name_2)
|
68
|
+
(file_name_1 < file_name_2)
|
69
|
+
end
|
46
70
|
|
47
|
-
|
48
|
-
|
71
|
+
def same_file_earlier_line(file_name_1, file_name_2, line_number_1, line_number_2)
|
72
|
+
(file_name_1 == file_name_2) && (line_number_1 < line_number_2)
|
73
|
+
end
|
49
74
|
|
50
|
-
|
75
|
+
def later_file(file_name_1, file_name_2)
|
76
|
+
(file_name_1 > file_name_2)
|
77
|
+
end
|
51
78
|
|
52
|
-
|
79
|
+
def same_file_later_line(file_name_1, file_name_2, line_number_1, line_number_2)
|
80
|
+
(file_name_1 == file_name_2) && (line_number_1 > line_number_2)
|
53
81
|
end
|
54
82
|
|
55
83
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module CukeLinter
|
2
|
+
|
3
|
+
# The default keyword that is considered a 'Given' keyword
|
4
|
+
DEFAULT_GIVEN_KEYWORD = 'Given'.freeze
|
5
|
+
# The default keyword that is considered a 'When' keyword
|
6
|
+
DEFAULT_WHEN_KEYWORD = 'When'.freeze
|
7
|
+
# The default keyword that is considered a 'Then' keyword
|
8
|
+
DEFAULT_THEN_KEYWORD = 'Then'.freeze
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CukeLinter
|
2
|
+
|
3
|
+
# Mix-in module containing methods related to registering linters
|
4
|
+
module LinterRegistration
|
5
|
+
|
6
|
+
# Returns the registered linters to their default state
|
7
|
+
def reset_linters
|
8
|
+
@registered_linters = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
# Registers for linting use the given linter object, tracked by the given name
|
12
|
+
def register_linter(linter:, name:)
|
13
|
+
registered_linters[name] = linter
|
14
|
+
end
|
15
|
+
|
16
|
+
# Unregisters the linter object tracked by the given name so that it is not used for linting
|
17
|
+
def unregister_linter(name)
|
18
|
+
registered_linters.delete(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Lists the names of the currently registered linting objects
|
22
|
+
def registered_linters
|
23
|
+
@registered_linters ||= Marshal.load(Marshal.dump(@original_linters))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Unregisters all currently registered linting objects
|
27
|
+
def clear_registered_linters
|
28
|
+
registered_linters.clear
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -14,7 +14,7 @@ module CukeLinter
|
|
14
14
|
def rule(model)
|
15
15
|
return false unless model.is_a?(CukeModeler::Background)
|
16
16
|
|
17
|
-
model.steps.
|
17
|
+
model.steps.map(&:keyword).any? { |keyword| when_keywords.include?(keyword) || then_keywords.include?(keyword) }
|
18
18
|
end
|
19
19
|
|
20
20
|
# The message used to describe the problem that has been found
|
@@ -6,36 +6,45 @@ module CukeLinter
|
|
6
6
|
|
7
7
|
# The rule used to determine if a model has a problem
|
8
8
|
def rule(model)
|
9
|
-
return false unless
|
9
|
+
return false unless relevant_model?(model)
|
10
10
|
|
11
11
|
@linted_model_class = model.class
|
12
|
-
|
13
|
-
child_accessor_method = model.is_a?(CukeModeler::Feature) ? :tests : :examples
|
14
|
-
child_models = model.send(child_accessor_method) || []
|
12
|
+
child_models = model.send(child_accessor_method(model)) || []
|
15
13
|
|
16
14
|
tag_sets = child_models.collect { |child_model| child_model.tags || [] }
|
17
15
|
tag_name_sets = tag_sets.collect { |tags| tags.map(&:name) }
|
18
16
|
|
19
17
|
return false if tag_name_sets.count < 2
|
20
18
|
|
21
|
-
|
22
|
-
|
23
|
-
!@common_tag.nil?
|
19
|
+
!find_common_tag(tag_name_sets).nil?
|
24
20
|
end
|
25
21
|
|
26
22
|
# The message used to describe the problem that has been found
|
27
23
|
def message
|
28
24
|
class_name = @linted_model_class.name.split('::').last
|
29
25
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
"All Examples in #{class_name} have tag '#{@common_tag}'. Move tag to #{class_name} level."
|
35
|
-
else
|
36
|
-
raise("Linted an unexpected model type '#{class_name}'!")
|
26
|
+
if class_name == 'Feature'
|
27
|
+
"All tests in Feature have tag '#{@common_tag}'. Move tag to #{class_name} level."
|
28
|
+
else
|
29
|
+
"All Examples in Outline have tag '#{@common_tag}'. Move tag to #{class_name} level."
|
37
30
|
end
|
38
31
|
end
|
39
32
|
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
|
37
|
+
def relevant_model?(model)
|
38
|
+
model.is_a?(CukeModeler::Feature) || model.is_a?(CukeModeler::Outline)
|
39
|
+
end
|
40
|
+
|
41
|
+
def child_accessor_method(model)
|
42
|
+
model.is_a?(CukeModeler::Feature) ? :tests : :examples
|
43
|
+
end
|
44
|
+
|
45
|
+
def find_common_tag(tag_name_sets)
|
46
|
+
@common_tag = tag_name_sets.reduce(:&).first
|
47
|
+
end
|
48
|
+
|
40
49
|
end
|
41
50
|
end
|
@@ -11,22 +11,16 @@ module CukeLinter
|
|
11
11
|
|
12
12
|
# The rule used to determine if a model has a problem
|
13
13
|
def rule(model)
|
14
|
-
return false unless
|
15
|
-
model.is_a?(CukeModeler::Scenario) ||
|
16
|
-
model.is_a?(CukeModeler::Outline) ||
|
17
|
-
model.is_a?(CukeModeler::Example)
|
18
|
-
|
14
|
+
return false unless relevant_model?(model)
|
19
15
|
|
20
16
|
@linted_model_class = model.class
|
21
17
|
|
22
|
-
if @tag_inheritance
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
tag_names = relevant_tags.map(&:name)
|
18
|
+
relevant_tags = if @tag_inheritance
|
19
|
+
model.all_tags
|
20
|
+
else
|
21
|
+
model.tags || []
|
22
|
+
end
|
23
|
+
tag_names = relevant_tags.map(&:name)
|
30
24
|
|
31
25
|
@duplicate_tag = tag_names.find { |tag| tag_names.count(tag) > 1 }
|
32
26
|
|
@@ -40,5 +34,16 @@ module CukeLinter
|
|
40
34
|
"#{class_name} has duplicate tag '#{@duplicate_tag}'."
|
41
35
|
end
|
42
36
|
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
|
41
|
+
def relevant_model?(model)
|
42
|
+
model.is_a?(CukeModeler::Feature) ||
|
43
|
+
model.is_a?(CukeModeler::Scenario) ||
|
44
|
+
model.is_a?(CukeModeler::Outline) ||
|
45
|
+
model.is_a?(CukeModeler::Example)
|
46
|
+
end
|
47
|
+
|
43
48
|
end
|
44
49
|
end
|
@@ -12,20 +12,15 @@ module CukeLinter
|
|
12
12
|
|
13
13
|
# The rule used to determine if a model has a problem
|
14
14
|
def rule(model)
|
15
|
-
return false unless
|
16
|
-
model.is_a?(CukeModeler::Scenario) ||
|
17
|
-
model.is_a?(CukeModeler::Outline) ||
|
18
|
-
model.is_a?(CukeModeler::Example)
|
19
|
-
|
15
|
+
return false unless relevant_model?(model)
|
20
16
|
|
21
17
|
@linted_model_class = model.class
|
22
18
|
@linted_tag_threshold = @tag_threshold || 5
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
19
|
+
@linted_tag_count = if @tag_inheritance
|
20
|
+
model.all_tags.count
|
21
|
+
else
|
22
|
+
model.tags.nil? ? 0 : model.tags.count
|
23
|
+
end
|
29
24
|
|
30
25
|
@linted_tag_count > @linted_tag_threshold
|
31
26
|
end
|
@@ -37,5 +32,16 @@ module CukeLinter
|
|
37
32
|
"#{class_name} has too many tags. #{@linted_tag_count} tags found (max #{@linted_tag_threshold})."
|
38
33
|
end
|
39
34
|
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
|
39
|
+
def relevant_model?(model)
|
40
|
+
model.is_a?(CukeModeler::Feature) ||
|
41
|
+
model.is_a?(CukeModeler::Scenario) ||
|
42
|
+
model.is_a?(CukeModeler::Outline) ||
|
43
|
+
model.is_a?(CukeModeler::Example)
|
44
|
+
end
|
45
|
+
|
40
46
|
end
|
41
47
|
end
|
@@ -16,9 +16,7 @@ module CukeLinter
|
|
16
16
|
tags = model.tags
|
17
17
|
|
18
18
|
model.each_descendant do |descendant_model|
|
19
|
-
if descendant_model.respond_to?(:tags)
|
20
|
-
tags.concat(descendant_model.tags)
|
21
|
-
end
|
19
|
+
tags.concat(descendant_model.tags) if descendant_model.respond_to?(:tags)
|
22
20
|
end
|
23
21
|
|
24
22
|
tags = tags.collect(&:name).uniq
|
@@ -4,6 +4,9 @@ module CukeLinter
|
|
4
4
|
|
5
5
|
class Linter
|
6
6
|
|
7
|
+
# Returns the name of the linter
|
8
|
+
attr_reader :name
|
9
|
+
|
7
10
|
# Creates a new linter object
|
8
11
|
def initialize(name: nil, message: nil, rule: nil)
|
9
12
|
@name = name || self.class.name.split('::').last
|
@@ -11,27 +14,28 @@ module CukeLinter
|
|
11
14
|
@rule = rule
|
12
15
|
end
|
13
16
|
|
14
|
-
# Returns the name of the linter
|
15
|
-
def name
|
16
|
-
@name
|
17
|
-
end
|
18
|
-
|
19
17
|
# Lints the given model and returns linting data about said model
|
20
18
|
def lint(model)
|
21
19
|
raise 'No linting rule provided!' unless @rule || respond_to?(:rule)
|
22
20
|
|
23
21
|
problem_found = respond_to?(:rule) ? rule(model) : @rule.call(model)
|
24
22
|
|
25
|
-
|
26
|
-
|
23
|
+
return nil unless problem_found
|
24
|
+
|
25
|
+
build_problem(model)
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
|
32
|
+
def build_problem(model)
|
33
|
+
problem_message = respond_to?(:message) ? message : @message
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
else
|
31
|
-
{ problem: problem_message, location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }
|
32
|
-
end
|
35
|
+
if model.is_a?(CukeModeler::FeatureFile)
|
36
|
+
{ problem: problem_message, location: model.path }
|
33
37
|
else
|
34
|
-
|
38
|
+
{ problem: problem_message, location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }
|
35
39
|
end
|
36
40
|
end
|
37
41
|
|