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.
@@ -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
- categorized_problems = Hash.new { |linters, linter_name| linters[linter_name] = Hash.new { |problems, problem| problems[problem] = [] } }
8
+ format_data(categorize_problems(data), data.count)
9
+ end
10
+
10
11
 
11
- data.each do |lint_item|
12
- categorized_problems[lint_item[:linter]][lint_item[:problem]] << lint_item[:location]
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
- formatted_data = ''
16
-
17
- categorized_problems.each_pair do |linter, problems|
18
- formatted_data << linter + "\n"
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
- sorted_locations.each do |location|
42
- formatted_data << " #{location}\n"
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
- total_problems = data.count
48
- formatted_data << "\n" unless total_problems.zero?
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
- formatted_data << "#{total_problems} issues found"
75
+ def later_file(file_name_1, file_name_2)
76
+ (file_name_1 > file_name_2)
77
+ end
51
78
 
52
- formatted_data
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.collect(&:keyword).any? { |keyword| when_keywords.include?(keyword) || then_keywords.include?(keyword) }
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 model.is_a?(CukeModeler::Feature) || model.is_a?(CukeModeler::Outline)
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
- @common_tag = tag_name_sets.reduce(:&).first
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
- case class_name
31
- when 'Feature'
32
- "All tests in #{class_name} have tag '#{@common_tag}'. Move tag to #{class_name} level."
33
- when 'Outline'
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 model.is_a?(CukeModeler::Feature) ||
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
- relevant_tags = model.all_tags
24
- else
25
- relevant_tags = model.tags || []
26
- end
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 model.is_a?(CukeModeler::Feature) ||
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
- if @tag_inheritance
25
- @linted_tag_count = model.all_tags.count
26
- else
27
- @linted_tag_count = model.tags.nil? ? 0 : model.tags.count
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
@@ -13,7 +13,7 @@ module CukeLinter
13
13
 
14
14
  # The message used to describe the problem that has been found
15
15
  def message
16
- "Feature has no description"
16
+ 'Feature has no description'
17
17
  end
18
18
 
19
19
  end
@@ -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
- if problem_found
26
- problem_message = respond_to?(:message) ? message : @message
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
- if model.is_a?(CukeModeler::FeatureFile)
29
- { problem: problem_message, location: "#{model.path}" }
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
- nil
38
+ { problem: problem_message, location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }
35
39
  end
36
40
  end
37
41