Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
enkessler committed May 11, 2019
2 parents c4ef0b6 + 91c8b38 commit 422ff5e
Show file tree
Hide file tree
Showing 24 changed files with 477 additions and 167 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Expand Up @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Nothing yet...

## [0.4.0] - 2019-05-11

### Added
- A base linter class has been added that can be used to create custom linters more easily by providing common boilerplate code that every linter would need.

### Changed
- Linters now return only a single problem instead of returning a collection of problems.

## [0.3.1] - 2019-04-13

### Added
Expand All @@ -33,7 +41,8 @@ Nothing yet...
- Custom linters, formatters, and command line usability


[Unreleased]: https://github.com/enkessler/cuke_linter/compare/v0.3.1...HEAD
[Unreleased]: https://github.com/enkessler/cuke_linter/compare/v0.4.0...HEAD
[0.4.0]: https://github.com/enkessler/cuke_linter/compare/v0.3.1...v0.4.0
[0.3.1]: https://github.com/enkessler/cuke_linter/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/enkessler/cuke_linter/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/enkessler/cuke_linter/compare/v0.1.0...v0.2.0
Expand Down
19 changes: 8 additions & 11 deletions README.md
Expand Up @@ -56,20 +56,17 @@ CukeLinter.lint

The linting will happen against a tree of `CukeModeler` models that is generated based on the current directory. You can generate your own model tree and use that instead, if desired.

Custom linters can be any object that responds to `#lint` and returns a collection of detected issues in the format of
Custom linters can be any object that responds to `#lint` and returns a detected issue (or `nil`) in the format of

```
[
{ problem: 'some linting issue',
location: 'path/to/file:line_number' },
{ problem: 'some linting issue',
location: 'path/to/file:line_number' },
# etc.
]
{ problem: 'some linting issue',
location: 'path/to/file:line_number' }
```

Note that a linter will receive, in turn, *every model* in the model tree in order for it to have the chance to detect problems with it. Checking the model's class before attempting to lint it is recommended.

**In order to simplify the process of creating custom linters a base class is provided (see [documentation](#documentation)).**

Custom formatters can be any object that responds to `#format` and takes input data in the following format:

```
Expand Down Expand Up @@ -98,12 +95,12 @@ class MyCustomLinter
end
def lint(model)
return [] unless model.is_a?(CukeModeler::Scenario)
return nil unless model.is_a?(CukeModeler::Scenario)
if model.name.empty?
[{ problem: 'Scenario has no name', location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }]
{ problem: 'Scenario has no name', location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }
else
[]
nil
end
end
Expand Down
4 changes: 2 additions & 2 deletions Rakefile
Expand Up @@ -50,11 +50,11 @@ namespace 'cuke_linter' do
Rake::Task['cuke_linter:test_everything'].invoke
Rake::Task['cuke_linter:check_documentation'].invoke
rescue => e
puts Rainbow("Something isn't right!").red
puts Rainbow("-----------------------\nSomething isn't right!").red
raise e
end

puts Rainbow('All is well. :)').green
puts Rainbow("-----------------------\nAll is well. :)").green
end

end
Expand Down
10 changes: 7 additions & 3 deletions lib/cuke_linter.rb
Expand Up @@ -2,6 +2,7 @@

require "cuke_linter/version"
require 'cuke_linter/formatters/pretty_formatter'
require 'cuke_linter/linters/linter'
require 'cuke_linter/linters/example_without_name_linter'
require 'cuke_linter/linters/feature_without_scenarios_linter'
require 'cuke_linter/linters/outline_with_single_example_row_linter'
Expand Down Expand Up @@ -73,9 +74,12 @@ def self.lint(model_tree: CukeModeler::Directory.new(Dir.pwd), linters: self.reg
# TODO: have linters lint only certain types of models
# linting_data.concat(linter.lint(model)) if relevant_model?(linter, model)

linted_data = linter.lint(model)
linted_data.each { |data_point| data_point[:linter] = linter.name }
linting_data.concat(linted_data)
result = linter.lint(model)

if result
result[:linter] = linter.name
linting_data << result
end
end
end

Expand Down
22 changes: 9 additions & 13 deletions lib/cuke_linter/linters/example_without_name_linter.rb
Expand Up @@ -2,22 +2,18 @@ module CukeLinter

# A linter that detects unnamed example groups

class ExampleWithoutNameLinter
class ExampleWithoutNameLinter < Linter

# Returns the name of the linter
def name
'ExampleWithoutNameLinter'
end
# The rule used to determine if a model has a problem
def rule(model)
return false unless model.is_a?(CukeModeler::Example)

# Lints the given model and returns linting data about said model
def lint(model)
return [] unless model.is_a?(CukeModeler::Example)
model.name.nil? || model.name.empty?
end

if model.name.nil? || model.name.empty?
[{ problem: 'Example has no name', location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }]
else
[]
end
# The message used to describe the problem that has been found
def message
'Example has no name'
end

end
Expand Down
22 changes: 9 additions & 13 deletions lib/cuke_linter/linters/feature_without_scenarios_linter.rb
Expand Up @@ -2,22 +2,18 @@ module CukeLinter

# A linter that detects empty features

class FeatureWithoutScenariosLinter
class FeatureWithoutScenariosLinter < Linter

# Returns the name of the linter
def name
'FeatureWithoutScenariosLinter'
end
# The rule used to determine if a model has a problem
def rule(model)
return false unless model.is_a?(CukeModeler::Feature)

# Lints the given model and returns linting data about said model
def lint(model)
return [] unless model.is_a?(CukeModeler::Feature)
model.tests.nil? || model.tests.empty?
end

if model.tests.nil? || model.tests.empty?
[{ problem: 'Feature has no scenarios', location: "#{model.parent_model.path}:#{model.source_line}" }]
else
[]
end
# The message used to describe the problem that has been found
def message
'Feature has no scenarios'
end

end
Expand Down
35 changes: 35 additions & 0 deletions lib/cuke_linter/linters/linter.rb
@@ -0,0 +1,35 @@
module CukeLinter

# A generic linter that can be used to make arbitrary linting rules

class Linter

# Creates a new linter object
def initialize(name: nil, message: nil, rule: nil)
@name = name || self.class.name.split('::').last
@message = message || "#{self.name} problem detected"
@rule = rule
end

# Returns the name of the linter
def name
@name
end

# Lints the given model and returns linting data about said model
def lint(model)
raise 'No linting rule provided!' unless @rule || respond_to?(:rule)

problem_found = respond_to?(:rule) ? rule(model) : @rule.call(model)

if problem_found
problem_message = respond_to?(:message) ? message : @message

{ problem: problem_message, location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }
else
nil
end
end

end
end
26 changes: 11 additions & 15 deletions lib/cuke_linter/linters/outline_with_single_example_row_linter.rb
Expand Up @@ -2,25 +2,21 @@ module CukeLinter

# A linter that detects outlines that don't have multiple example rows

class OutlineWithSingleExampleRowLinter
class OutlineWithSingleExampleRowLinter < Linter

# Returns the name of the linter
def name
'OutlineWithSingleExampleRowLinter'
end

# Lints the given model and returns linting data about said model
def lint(model)
return [] unless model.is_a?(CukeModeler::Outline)
return [] if model.examples.nil?
# The rule used to determine if a model has a problem
def rule(model)
return false unless model.is_a?(CukeModeler::Outline)
return false if model.examples.nil?

examples_rows = model.examples.collect(&:argument_rows).flatten

if examples_rows.count == 1
[{ problem: 'Outline has only one example row', location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }]
else
[]
end
examples_rows.count == 1
end

# The message used to describe the problem that has been found
def message
'Outline has only one example row'
end

end
Expand Down
28 changes: 12 additions & 16 deletions lib/cuke_linter/linters/test_with_too_many_steps_linter.rb
Expand Up @@ -2,30 +2,26 @@ module CukeLinter

# A linter that detects scenarios and outlines that have too many steps

class TestWithTooManyStepsLinter

# Returns the name of the linter
def name
'TestWithTooManyStepsLinter'
end
class TestWithTooManyStepsLinter < Linter

# Changes the linting settings on the linter using the provided configuration
def configure(options)
@step_threshold = options['StepThreshold'] if options['StepThreshold']
end

# Lints the given model and returns linting data about said model
def lint(model)
return [] unless model.is_a?(CukeModeler::Scenario) || model.is_a?(CukeModeler::Outline)
# The rule used to determine if a model has a problem
def rule(model)
return false unless model.is_a?(CukeModeler::Scenario) || model.is_a?(CukeModeler::Outline)

step_count = model.steps.nil? ? 0 : model.steps.count
step_threshold = @step_threshold || 10
@linted_step_count = model.steps.nil? ? 0 : model.steps.count
@linted_step_threshold = @step_threshold || 10

@linted_step_count > @linted_step_threshold
end

if step_count > step_threshold
[{ problem: "Test has too many steps. #{step_count} steps found (max #{step_threshold})", location: "#{model.get_ancestor(:feature_file).path}:#{model.source_line}" }]
else
[]
end
# The message used to describe the problem that has been found
def message
"Test has too many steps. #{@linted_step_count} steps found (max #{@linted_step_threshold})"
end

end
Expand Down
2 changes: 1 addition & 1 deletion lib/cuke_linter/version.rb
@@ -1,4 +1,4 @@
module CukeLinter
# The release version of this gem
VERSION = "0.3.1"
VERSION = "0.4.0"
end
54 changes: 54 additions & 0 deletions testing/cucumber/features/linters/custom_linters.feature
@@ -0,0 +1,54 @@
Feature: Custom linters

In addition to the linters provided by CukeSlicer, custom linters can be used. A linter is essentially any object that provides a few needed methods. In order to simplify the creation of custom linters, a base linter class is available that provides these needed methods.


Scenario: Creating a custom linter object
Given the following custom linter object:
"""
custom_name = 'MyCustomLinter'
custom_message = 'My custom message'
custom_rule = lambda do |model|
# Your logic here, return true for a problem and false for not problem
true
end
@linter = CukeLinter::Linter.new(name: custom_name,
message: custom_message,
rule: custom_rule)
"""
And a model to lint
When the model is linted
Then an error is reported
| linter | problem | location |
| MyCustomLinter | My custom message | <path_to_file>:<model_line_number> |

Scenario: Creating a custom linter class
Given the following custom linter class:
"""
class MyCustomLinter < CukeLinter::Linter
def name
'MyCustomLinter'
end
def message
'My custom message'
end
def rule(model)
# Your logic here, return true for a problem and false for not problem
true
end
end
"""
And the following code is used:
"""
@linter = MyCustomLinter.new
"""
And a model to lint
When the model is linted
Then an error is reported
| linter | problem | location |
| MyCustomLinter | My custom message | <path_to_file>:<model_line_number> |
2 changes: 1 addition & 1 deletion testing/cucumber/step_definitions/action_steps.rb
Expand Up @@ -8,7 +8,7 @@
@results = CukeLinter.const_get("#{linter_name.capitalize}Formatter").new.format(@linter_data)
end

When(/^(?:the feature|it) is linted$/) do
When(/^(?:the feature|the model|it) is linted$/) do
options = { model_tree: @model,
formatters: [[CukeLinter::FormatterFactory.generate_fake_formatter, "#{CukeLinter::FileHelper::create_directory}/junk_output_file.txt"]] }
options[:linters] = [@linter] if @linter
Expand Down
27 changes: 27 additions & 0 deletions testing/cucumber/step_definitions/setup_steps.rb
Expand Up @@ -66,3 +66,30 @@
Given(/^no linters are currently registered$/) do
CukeLinter.clear_registered_linters
end

Given(/^the following custom linter object:$/) do |code|
code.sub!('<path_to>', @root_test_directory)
code.sub!('<code_to_generate_a_new_linter_instance>', 'CukeLinter::LinterFactory.generate_fake_linter')

if @working_directory
Dir.chdir(@working_directory) do
eval(code)
end
else
eval(code)
end
end

And(/^a model to lint$/) do
# Any old model should be fine
@model = CukeModeler::Feature.new

fake_file_model = CukeModeler::FeatureFile.new
fake_file_model.path = 'path_to_file'

@model.parent_model = fake_file_model
end

Given(/^the following custom linter class:$/) do |code|
eval(code)
end
2 changes: 1 addition & 1 deletion testing/cucumber/step_definitions/verification_steps.rb
Expand Up @@ -10,7 +10,7 @@
table.hashes.each do |error_record|
expect(@results).to include({ linter: error_record['linter'],
problem: error_record['problem'],
location: error_record['location'].sub('<path_to_file>', @model.get_ancestor(:feature_file).path) })
location: error_record['location'].sub('<path_to_file>', @model.get_ancestor(:feature_file).path).sub('<model_line_number>', @model.source_line.to_s) })
end
end

Expand Down

0 comments on commit 422ff5e

Please sign in to comment.