cuke_linter 1.3.0 → 1.4.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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +217 -211
  3. data/LICENSE.txt +21 -21
  4. data/README.md +160 -160
  5. data/cuke_linter.gemspec +59 -58
  6. data/exe/cuke_linter +112 -112
  7. data/lib/cuke_linter/configuration.rb +45 -45
  8. data/lib/cuke_linter/default_linters.rb +32 -32
  9. data/lib/cuke_linter/formatters/pretty_formatter.rb +84 -84
  10. data/lib/cuke_linter/gherkin.rb +10 -10
  11. data/lib/cuke_linter/linter_registration.rb +32 -32
  12. data/lib/cuke_linter/linters/background_does_more_than_setup_linter.rb +35 -35
  13. data/lib/cuke_linter/linters/element_with_common_tags_linter.rb +49 -49
  14. data/lib/cuke_linter/linters/element_with_duplicate_tags_linter.rb +48 -48
  15. data/lib/cuke_linter/linters/element_with_too_many_tags_linter.rb +46 -46
  16. data/lib/cuke_linter/linters/example_without_name_linter.rb +19 -19
  17. data/lib/cuke_linter/linters/feature_file_with_invalid_name_linter.rb +20 -20
  18. data/lib/cuke_linter/linters/feature_file_with_mismatched_name_linter.rb +25 -25
  19. data/lib/cuke_linter/linters/feature_with_too_many_different_tags_linter.rb +35 -35
  20. data/lib/cuke_linter/linters/feature_without_description_linter.rb +19 -19
  21. data/lib/cuke_linter/linters/feature_without_name_linter.rb +19 -19
  22. data/lib/cuke_linter/linters/feature_without_scenarios_linter.rb +23 -23
  23. data/lib/cuke_linter/linters/linter.rb +42 -42
  24. data/lib/cuke_linter/linters/outline_with_single_example_row_linter.rb +22 -22
  25. data/lib/cuke_linter/linters/single_test_background_linter.rb +19 -19
  26. data/lib/cuke_linter/linters/step_with_end_period_linter.rb +19 -19
  27. data/lib/cuke_linter/linters/step_with_too_many_characters_linter.rb +38 -38
  28. data/lib/cuke_linter/linters/test_name_with_too_many_characters_linter.rb +38 -38
  29. data/lib/cuke_linter/linters/test_should_use_background_linter.rb +80 -80
  30. data/lib/cuke_linter/linters/test_with_action_step_as_final_step_linter.rb +33 -33
  31. data/lib/cuke_linter/linters/test_with_bad_name_linter.rb +23 -23
  32. data/lib/cuke_linter/linters/test_with_no_action_step_linter.rb +33 -33
  33. data/lib/cuke_linter/linters/test_with_no_name_linter.rb +19 -19
  34. data/lib/cuke_linter/linters/test_with_no_verification_step_linter.rb +33 -33
  35. data/lib/cuke_linter/linters/test_with_setup_step_after_action_step_linter.rb +46 -46
  36. data/lib/cuke_linter/linters/test_with_setup_step_after_verification_step_linter.rb +46 -46
  37. data/lib/cuke_linter/linters/test_with_setup_step_as_final_step_linter.rb +33 -33
  38. data/lib/cuke_linter/linters/test_with_too_many_steps_linter.rb +27 -27
  39. data/lib/cuke_linter/version.rb +4 -4
  40. data/lib/cuke_linter.rb +196 -196
  41. data/testing/cucumber/features/command_line.feature +202 -202
  42. data/testing/cucumber/features/configuration/configuring_linters.feature +58 -58
  43. data/testing/cucumber/features/configuration/locally_scoping_linters.feature +55 -55
  44. data/testing/cucumber/features/configuration/using_configurations.feature +41 -41
  45. data/testing/cucumber/features/custom_linters.feature +56 -56
  46. data/testing/cucumber/features/default_linters.feature +57 -57
  47. data/testing/cucumber/features/formatters/pretty_formatter.feature +26 -26
  48. data/testing/cucumber/features/linters/background_does_more_than_setup.feature +84 -84
  49. data/testing/cucumber/features/linters/element_with_common_tags.feature +28 -28
  50. data/testing/cucumber/features/linters/element_with_duplicate_tags.feature +71 -71
  51. data/testing/cucumber/features/linters/element_with_too_many_tags.feature +70 -70
  52. data/testing/cucumber/features/linters/example_without_name.feature +34 -34
  53. data/testing/cucumber/features/linters/feature_file_with_invalid_name.feature +20 -20
  54. data/testing/cucumber/features/linters/feature_file_with_mismatched_name.feature +32 -32
  55. data/testing/cucumber/features/linters/feature_with_too_many_different_tags.feature +56 -56
  56. data/testing/cucumber/features/linters/feature_without_description.feature +17 -17
  57. data/testing/cucumber/features/linters/feature_without_name.feature +18 -18
  58. data/testing/cucumber/features/linters/feature_without_scenarios.feature +39 -39
  59. data/testing/cucumber/features/linters/outline_with_single_example_row.feature +23 -23
  60. data/testing/cucumber/features/linters/single_test_background.feature +24 -24
  61. data/testing/cucumber/features/linters/step_too_long.feature +43 -43
  62. data/testing/cucumber/features/linters/step_with_end_period.feature +21 -21
  63. data/testing/cucumber/features/linters/test_name_too_long.feature +41 -41
  64. data/testing/cucumber/features/linters/test_should_use_background.feature +29 -29
  65. data/testing/cucumber/features/linters/test_with_action_as_final_step.feature +50 -50
  66. data/testing/cucumber/features/linters/test_with_bad_name.feature +29 -29
  67. data/testing/cucumber/features/linters/test_with_no_action_step.feature +56 -56
  68. data/testing/cucumber/features/linters/test_with_no_name.feature +23 -23
  69. data/testing/cucumber/features/linters/test_with_no_verification_step.feature +58 -58
  70. data/testing/cucumber/features/linters/test_with_setup_step_after_action_step.feature +57 -57
  71. data/testing/cucumber/features/linters/test_with_setup_step_after_verification_step.feature +57 -57
  72. data/testing/cucumber/features/linters/test_with_setup_step_as_final_step.feature +50 -50
  73. data/testing/cucumber/features/linters/test_with_too_many_steps.feature +61 -61
  74. metadata +21 -25
  75. data/testing/cucumber/features/linters/rule_without_name.feature +0 -18
data/lib/cuke_linter.rb CHANGED
@@ -1,196 +1,196 @@
1
- require 'yaml'
2
- require 'cuke_modeler'
3
-
4
- require 'cuke_linter/version'
5
- require 'cuke_linter/formatters/pretty_formatter'
6
- require 'cuke_linter/linters/linter'
7
- require 'cuke_linter/linters/background_does_more_than_setup_linter'
8
- require 'cuke_linter/linters/element_with_common_tags_linter'
9
- require 'cuke_linter/linters/element_with_duplicate_tags_linter'
10
- require 'cuke_linter/linters/element_with_too_many_tags_linter'
11
- require 'cuke_linter/linters/example_without_name_linter'
12
- require 'cuke_linter/linters/feature_file_with_invalid_name_linter'
13
- require 'cuke_linter/linters/feature_file_with_mismatched_name_linter'
14
- require 'cuke_linter/linters/feature_with_too_many_different_tags_linter'
15
- require 'cuke_linter/linters/feature_without_name_linter'
16
- require 'cuke_linter/linters/feature_without_description_linter'
17
- require 'cuke_linter/linters/feature_without_scenarios_linter'
18
- require 'cuke_linter/linters/outline_with_single_example_row_linter'
19
- require 'cuke_linter/linters/single_test_background_linter'
20
- require 'cuke_linter/linters/step_with_end_period_linter'
21
- require 'cuke_linter/linters/step_with_too_many_characters_linter'
22
- require 'cuke_linter/linters/test_name_with_too_many_characters_linter'
23
- require 'cuke_linter/linters/test_should_use_background_linter'
24
- require 'cuke_linter/linters/test_with_action_step_as_final_step_linter'
25
- require 'cuke_linter/linters/test_with_bad_name_linter'
26
- require 'cuke_linter/linters/test_with_no_action_step_linter'
27
- require 'cuke_linter/linters/test_with_no_name_linter'
28
- require 'cuke_linter/linters/test_with_no_verification_step_linter'
29
- require 'cuke_linter/linters/test_with_setup_step_after_action_step_linter'
30
- require 'cuke_linter/linters/test_with_setup_step_after_verification_step_linter'
31
- require 'cuke_linter/linters/test_with_setup_step_as_final_step_linter'
32
- require 'cuke_linter/linters/test_with_too_many_steps_linter'
33
- require 'cuke_linter/configuration'
34
- require 'cuke_linter/default_linters'
35
- require 'cuke_linter/gherkin'
36
- require 'cuke_linter/linter_registration'
37
-
38
-
39
- # The top level namespace used by this gem
40
- module CukeLinter
41
-
42
- extend CukeLinter::Configuration
43
- extend CukeLinter::LinterRegistration
44
-
45
- class << self
46
-
47
- # Lints the given model trees and file paths using the given linting objects and formatting
48
- # the results with the given formatters and their respective output locations
49
- def lint(file_paths: [], model_trees: [], linters: registered_linters.values, formatters: [[CukeLinter::PrettyFormatter.new]]) # rubocop:disable Metrics/LineLength
50
- # TODO: Test this?
51
- # Because directive memoization is based on a model's `#object_id` and Ruby reuses object IDs over the
52
- # life of a program as objects are garbage collected, it is not safe to remember the IDs forever. However,
53
- # models shouldn't get GC'd in the middle of the linting process and so the start of the linting process is
54
- # a good time to reset things
55
- @directives_for_feature_file = {}
56
-
57
- model_trees = [CukeModeler::Directory.new(Dir.pwd)] if model_trees.empty? && file_paths.empty?
58
- file_path_models = collect_file_path_models(file_paths)
59
- model_sets = model_trees + file_path_models
60
-
61
- linting_data = lint_models(model_sets, linters)
62
- format_data(formatters, linting_data)
63
-
64
- linting_data
65
- end
66
-
67
-
68
- private
69
-
70
-
71
- def collect_file_path_models(file_paths)
72
- file_paths.collect do |file_path|
73
- # TODO: raise exception unless path exists?
74
- if File.directory?(file_path)
75
- CukeModeler::Directory.new(file_path)
76
- elsif File.file?(file_path) && File.extname(file_path) == '.feature'
77
- CukeModeler::FeatureFile.new(file_path)
78
- end
79
- end.compact # Compacting in order to get rid of any `nil` values left over from non-feature files
80
- end
81
-
82
- def lint_models(model_sets, linters)
83
- [].tap do |linting_data|
84
- model_sets.each do |model_tree|
85
- model_tree.each_model do |model|
86
- applicable_linters = relevant_linters_for_model(linters, model)
87
- applicable_linters.each do |linter|
88
- # TODO: have linters lint only certain types of models?
89
- # linting_data.concat(linter.lint(model)) if relevant_model?(linter, model)
90
-
91
- result = linter.lint(model)
92
-
93
- if result
94
- result[:linter] = linter.name
95
- linting_data << result
96
- end
97
- end
98
- end
99
- end
100
- end
101
- end
102
-
103
- def relevant_linters_for_model(base_linters, model)
104
- feature_file_model = model.get_ancestor(:feature_file)
105
-
106
- # Linter directives are not applicable for directory and feature file models. Every other
107
- # model type should have a feature file ancestor from which to grab linter directive comments.
108
- return base_linters if feature_file_model.nil?
109
-
110
- linter_modifications_for_model = {}
111
-
112
- linter_directives_for_feature_file(feature_file_model).each do |directive|
113
- # Assuming that the directives are in the same order that they appear in the file
114
- break if directive[:source_line] > model.source_line
115
-
116
- linter_modifications_for_model[directive[:linter_class]] = directive[:enabled_status]
117
- end
118
-
119
- disabled_linter_classes = linter_modifications_for_model.reject { |_name, status| status }.keys
120
- enabled_linter_classes = linter_modifications_for_model.select { |_name, status| status }.keys
121
-
122
- determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
123
- end
124
-
125
- def determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
126
- final_linters = base_linters.reject { |linter| disabled_linter_classes.include?(linter.class) }
127
-
128
- enabled_linter_classes.each do |clazz|
129
- final_linters << dynamic_linters[clazz] unless final_linters.map(&:class).include?(clazz)
130
- end
131
-
132
- final_linters
133
- end
134
-
135
- def linter_directives_for_feature_file(feature_file_model)
136
- # IMPORTANT ASSUMPTION: Models never change during the life of a linting, so data only has to be gathered once
137
- existing_directives = @directives_for_feature_file[feature_file_model.object_id]
138
-
139
- return existing_directives if existing_directives
140
-
141
- directives = gather_directives_in_feature(feature_file_model)
142
-
143
- # Make sure that the directives are in the same order as they appear in the source file
144
- directives = directives.sort_by { |a| a[:source_line] }
145
-
146
- @directives_for_feature_file[feature_file_model.object_id] = directives
147
- end
148
-
149
- def gather_directives_in_feature(feature_file_model)
150
- [].tap do |directives|
151
- feature_file_model.comments.each do |comment|
152
- pieces = comment.text.match(/#\s*cuke_linter:(disable|enable)\s+(.*)/)
153
- next unless pieces # Skipping non-directive file comments
154
-
155
- linter_classes = pieces[2].tr(',', ' ').split(' ')
156
- linter_classes.each do |clazz|
157
- directives << { linter_class: Kernel.const_get(clazz),
158
- enabled_status: pieces[1] != 'disable',
159
- source_line: comment.source_line }
160
- end
161
- end
162
- end
163
- end
164
-
165
- def dynamic_linters
166
- # No need to keep making new ones over and over...
167
- @dynamic_linters ||= Hash.new { |hash, key| hash[key] = key.new }
168
- end
169
-
170
- def format_data(formatters, linting_data)
171
- formatters.each do |formatter_output_pair|
172
- formatter = formatter_output_pair[0]
173
- location = formatter_output_pair[1]
174
-
175
- formatted_data = formatter.format(linting_data)
176
-
177
- if location
178
- File.write(location, formatted_data)
179
- else
180
- puts formatted_data
181
- end
182
- end
183
- end
184
-
185
- # Not linting unused code
186
- # rubocop:disable Metrics/LineLength
187
- # def self.relevant_model?(linter, model)
188
- # model_classes = linter.class.target_model_types.map { |type| CukeModeler.const_get(type.to_s.capitalize.chop) }
189
- # model_classes.any? { |clazz| model.is_a?(clazz) }
190
- # end
191
- #
192
- # private_class_method(:relevant_model?)
193
- # rubocop:enable Metrics/LineLength
194
-
195
- end
196
- end
1
+ require 'yaml'
2
+ require 'cuke_modeler'
3
+
4
+ require 'cuke_linter/version'
5
+ require 'cuke_linter/formatters/pretty_formatter'
6
+ require 'cuke_linter/linters/linter'
7
+ require 'cuke_linter/linters/background_does_more_than_setup_linter'
8
+ require 'cuke_linter/linters/element_with_common_tags_linter'
9
+ require 'cuke_linter/linters/element_with_duplicate_tags_linter'
10
+ require 'cuke_linter/linters/element_with_too_many_tags_linter'
11
+ require 'cuke_linter/linters/example_without_name_linter'
12
+ require 'cuke_linter/linters/feature_file_with_invalid_name_linter'
13
+ require 'cuke_linter/linters/feature_file_with_mismatched_name_linter'
14
+ require 'cuke_linter/linters/feature_with_too_many_different_tags_linter'
15
+ require 'cuke_linter/linters/feature_without_name_linter'
16
+ require 'cuke_linter/linters/feature_without_description_linter'
17
+ require 'cuke_linter/linters/feature_without_scenarios_linter'
18
+ require 'cuke_linter/linters/outline_with_single_example_row_linter'
19
+ require 'cuke_linter/linters/single_test_background_linter'
20
+ require 'cuke_linter/linters/step_with_end_period_linter'
21
+ require 'cuke_linter/linters/step_with_too_many_characters_linter'
22
+ require 'cuke_linter/linters/test_name_with_too_many_characters_linter'
23
+ require 'cuke_linter/linters/test_should_use_background_linter'
24
+ require 'cuke_linter/linters/test_with_action_step_as_final_step_linter'
25
+ require 'cuke_linter/linters/test_with_bad_name_linter'
26
+ require 'cuke_linter/linters/test_with_no_action_step_linter'
27
+ require 'cuke_linter/linters/test_with_no_name_linter'
28
+ require 'cuke_linter/linters/test_with_no_verification_step_linter'
29
+ require 'cuke_linter/linters/test_with_setup_step_after_action_step_linter'
30
+ require 'cuke_linter/linters/test_with_setup_step_after_verification_step_linter'
31
+ require 'cuke_linter/linters/test_with_setup_step_as_final_step_linter'
32
+ require 'cuke_linter/linters/test_with_too_many_steps_linter'
33
+ require 'cuke_linter/configuration'
34
+ require 'cuke_linter/default_linters'
35
+ require 'cuke_linter/gherkin'
36
+ require 'cuke_linter/linter_registration'
37
+
38
+
39
+ # The top level namespace used by this gem
40
+ module CukeLinter
41
+
42
+ extend CukeLinter::Configuration
43
+ extend CukeLinter::LinterRegistration
44
+
45
+ class << self
46
+
47
+ # Lints the given model trees and file paths using the given linting objects and formatting
48
+ # the results with the given formatters and their respective output locations
49
+ def lint(file_paths: [], model_trees: [], linters: registered_linters.values, formatters: [[CukeLinter::PrettyFormatter.new]]) # rubocop:disable Layout/LineLength
50
+ # TODO: Test this?
51
+ # Because directive memoization is based on a model's `#object_id` and Ruby reuses object IDs over the
52
+ # life of a program as objects are garbage collected, it is not safe to remember the IDs forever. However,
53
+ # models shouldn't get GC'd in the middle of the linting process and so the start of the linting process is
54
+ # a good time to reset things
55
+ @directives_for_feature_file = {}.compare_by_identity
56
+
57
+ model_trees = [CukeModeler::Directory.new(Dir.pwd)] if model_trees.empty? && file_paths.empty?
58
+ file_path_models = collect_file_path_models(file_paths)
59
+ model_sets = model_trees + file_path_models
60
+
61
+ linting_data = lint_models(model_sets, linters)
62
+ format_data(formatters, linting_data)
63
+
64
+ linting_data
65
+ end
66
+
67
+
68
+ private
69
+
70
+
71
+ def collect_file_path_models(file_paths)
72
+ file_paths.collect do |file_path|
73
+ # TODO: raise exception unless path exists?
74
+ if File.directory?(file_path)
75
+ CukeModeler::Directory.new(file_path)
76
+ elsif File.file?(file_path) && File.extname(file_path) == '.feature'
77
+ CukeModeler::FeatureFile.new(file_path)
78
+ end
79
+ end.compact # Compacting in order to get rid of any `nil` values left over from non-feature files
80
+ end
81
+
82
+ def lint_models(model_sets, linters)
83
+ [].tap do |linting_data|
84
+ model_sets.each do |model_tree|
85
+ model_tree.each_model do |model|
86
+ applicable_linters = relevant_linters_for_model(linters, model)
87
+ applicable_linters.each do |linter|
88
+ # TODO: have linters lint only certain types of models?
89
+ # linting_data.concat(linter.lint(model)) if relevant_model?(linter, model)
90
+
91
+ result = linter.lint(model)
92
+
93
+ if result
94
+ result[:linter] = linter.name
95
+ linting_data << result
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ def relevant_linters_for_model(base_linters, model) # rubocop:disable Metrics/AbcSize -- Maybe I'll revisit this later
104
+ feature_file_model = model.get_ancestor(:feature_file)
105
+
106
+ # Linter directives are not applicable for directory and feature file models. Every other
107
+ # model type should have a feature file ancestor from which to grab linter directive comments.
108
+ return base_linters if feature_file_model.nil?
109
+
110
+ linter_modifications_for_model = {}
111
+
112
+ linter_directives_for_feature_file(feature_file_model).each do |directive|
113
+ # Assuming that the directives are in the same order that they appear in the file
114
+ break if directive[:source_line] > model.source_line
115
+
116
+ linter_modifications_for_model[directive[:linter_class]] = directive[:enabled_status]
117
+ end
118
+
119
+ disabled_linter_classes = linter_modifications_for_model.reject { |_name, status| status }.keys
120
+ enabled_linter_classes = linter_modifications_for_model.select { |_name, status| status }.keys
121
+
122
+ determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
123
+ end
124
+
125
+ def determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
126
+ final_linters = base_linters.reject { |linter| disabled_linter_classes.include?(linter.class) }
127
+
128
+ enabled_linter_classes.each do |clazz|
129
+ final_linters << dynamic_linters[clazz] unless final_linters.map(&:class).include?(clazz)
130
+ end
131
+
132
+ final_linters
133
+ end
134
+
135
+ def linter_directives_for_feature_file(feature_file_model)
136
+ # IMPORTANT ASSUMPTION: Models never change during the life of a linting, so data only has to be gathered once
137
+ existing_directives = @directives_for_feature_file[feature_file_model]
138
+
139
+ return existing_directives if existing_directives
140
+
141
+ directives = gather_directives_in_feature(feature_file_model)
142
+
143
+ # Make sure that the directives are in the same order as they appear in the source file
144
+ directives = directives.sort_by { |a| a[:source_line] }
145
+
146
+ @directives_for_feature_file[feature_file_model] = directives
147
+ end
148
+
149
+ def gather_directives_in_feature(feature_file_model)
150
+ [].tap do |directives|
151
+ feature_file_model.comments.each do |comment|
152
+ pieces = comment.text.match(/#\s*cuke_linter:(disable|enable)\s+(.*)/)
153
+ next unless pieces # Skipping non-directive file comments
154
+
155
+ linter_classes = pieces[2].tr(',', ' ').split
156
+ linter_classes.each do |clazz|
157
+ directives << { linter_class: Kernel.const_get(clazz),
158
+ enabled_status: pieces[1] != 'disable',
159
+ source_line: comment.source_line }
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ def dynamic_linters
166
+ # No need to keep making new ones over and over...
167
+ @dynamic_linters ||= Hash.new { |hash, key| hash[key] = key.new }
168
+ end
169
+
170
+ def format_data(formatters, linting_data)
171
+ formatters.each do |formatter_output_pair|
172
+ formatter = formatter_output_pair[0]
173
+ location = formatter_output_pair[1]
174
+
175
+ formatted_data = formatter.format(linting_data)
176
+
177
+ if location
178
+ File.write(location, formatted_data)
179
+ else
180
+ puts formatted_data
181
+ end
182
+ end
183
+ end
184
+
185
+ # Not linting unused code
186
+ # rubocop:disable Layout/LineLength
187
+ # def self.relevant_model?(linter, model)
188
+ # model_classes = linter.class.target_model_types.map { |type| CukeModeler.const_get(type.to_s.capitalize.chop) }
189
+ # model_classes.any? { |clazz| model.is_a?(clazz) }
190
+ # end
191
+ #
192
+ # private_class_method(:relevant_model?)
193
+ # rubocop:enable Layout/LineLength
194
+
195
+ end
196
+ end