cuke_linter 1.0.0 → 1.0.1

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