module CukeLinter

The top level namespace used by this gem

Constants

DEFAULT_GIVEN_KEYWORD

The default keyword that is considered a 'Given' keyword

DEFAULT_THEN_KEYWORD

The default keyword that is considered a 'Then' keyword

DEFAULT_WHEN_KEYWORD

The default keyword that is considered a 'When' keyword

VERSION

The release version of this gem

Public Class Methods

lint(file_paths: [], model_trees: [], linters: registered_linters.values, formatters: [[CukeLinter::PrettyFormatter.new]]) click to toggle source

Lints the given model trees and file paths using the given linting objects and formatting the results with the given formatters and their respective output locations

# File lib/cuke_linter.rb, line 48
def lint(file_paths: [], model_trees: [], linters: registered_linters.values, formatters: [[CukeLinter::PrettyFormatter.new]]) # rubocop:disable Metrics/LineLength
  # TODO: Test this?
  # Because directive memoization is based on a model's `#object_id` and Ruby reuses object IDs over the
  # life of a program as objects are garbage collected, it is not safe to remember the IDs forever. However,
  # models shouldn't get GC'd in the middle of the linting process and so the start of the linting process is
  # a good time to reset things
  @directives_for_feature_file = {}

  model_trees                  = [CukeModeler::Directory.new(Dir.pwd)] if model_trees.empty? && file_paths.empty?
  file_path_models             = collect_file_path_models(file_paths)
  model_sets                   = model_trees + file_path_models

  linting_data = lint_models(model_sets, linters)
  format_data(formatters, linting_data)

  linting_data
end

Private Class Methods

collect_file_path_models(file_paths) click to toggle source
# File lib/cuke_linter.rb, line 70
def collect_file_path_models(file_paths)
  file_paths.collect do |file_path|
    # TODO: raise exception unless path exists?
    if File.directory?(file_path)
      CukeModeler::Directory.new(file_path)
    elsif File.file?(file_path) && File.extname(file_path) == '.feature'
      CukeModeler::FeatureFile.new(file_path)
    end
  end.compact # Compacting in order to get rid of any `nil` values left over from non-feature files
end
determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes) click to toggle source
# File lib/cuke_linter.rb, line 124
def determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
  final_linters = base_linters.reject { |linter| disabled_linter_classes.include?(linter.class) }

  enabled_linter_classes.each do |clazz|
    final_linters << dynamic_linters[clazz] unless final_linters.map(&:class).include?(clazz)
  end

  final_linters
end
dynamic_linters() click to toggle source
# File lib/cuke_linter.rb, line 164
def dynamic_linters
  # No need to keep making new ones over and over...
  @dynamic_linters ||= Hash.new { |hash, key| hash[key] = key.new }
  # return @dynamic_linters if @dynamic_linters
  #
  # @dynamic_linters = {}
end
format_data(formatters, linting_data) click to toggle source
# File lib/cuke_linter.rb, line 172
def format_data(formatters, linting_data)
  formatters.each do |formatter_output_pair|
    formatter = formatter_output_pair[0]
    location  = formatter_output_pair[1]

    formatted_data = formatter.format(linting_data)

    if location
      File.write(location, formatted_data)
    else
      puts formatted_data
    end
  end
end
gather_directives_in_feature(feature_file_model) click to toggle source
# File lib/cuke_linter.rb, line 148
def gather_directives_in_feature(feature_file_model)
  [].tap do |directives|
    feature_file_model.comments.each do |comment|
      pieces = comment.text.match(/#\s*cuke_linter:(disable|enable)\s+(.*)/)
      next unless pieces # Skipping non-directive file comments

      linter_classes = pieces[2].tr(',', ' ').split(' ')
      linter_classes.each do |clazz|
        directives << { linter_class:   Kernel.const_get(clazz),
                        enabled_status: pieces[1] != 'disable',
                        source_line:    comment.source_line }
      end
    end
  end
end
lint_models(model_sets, linters) click to toggle source
# File lib/cuke_linter.rb, line 81
def lint_models(model_sets, linters)
  [].tap do |linting_data|
    model_sets.each do |model_tree|
      model_tree.each_model do |model|
        applicable_linters = relevant_linters_for_model(linters, model)
        applicable_linters.each do |linter|
          # TODO: have linters lint only certain types of models?
          #         linting_data.concat(linter.lint(model)) if relevant_model?(linter, model)

          result = linter.lint(model)

          if result
            result[:linter] = linter.name
            linting_data << result
          end
        end
      end
    end
  end
end
linter_directives_for_feature_file(feature_file_model) click to toggle source
# File lib/cuke_linter.rb, line 134
def linter_directives_for_feature_file(feature_file_model)
  # IMPORTANT ASSUMPTION: Models never change during the life of a linting, so data only has to be gathered once
  existing_directives = @directives_for_feature_file[feature_file_model.object_id]

  return existing_directives if existing_directives

  directives = gather_directives_in_feature(feature_file_model)

  # Make sure that the directives are in the same order as they appear in the source file
  directives = directives.sort_by { |a| a[:source_line] }

  @directives_for_feature_file[feature_file_model.object_id] = directives
end
relevant_linters_for_model(base_linters, model) click to toggle source
# File lib/cuke_linter.rb, line 102
def relevant_linters_for_model(base_linters, model)
  feature_file_model = model.get_ancestor(:feature_file)

  # Linter directives are not applicable for directory and feature file models. Every other
  # model type should have a feature file ancestor from which to grab linter directive comments.
  return base_linters if feature_file_model.nil?

  linter_modifications_for_model = {}

  linter_directives_for_feature_file(feature_file_model).each do |directive|
    # Assuming that the directives are in the same order that they appear in the file
    break if directive[:source_line] > model.source_line

    linter_modifications_for_model[directive[:linter_class]] = directive[:enabled_status]
  end

  disabled_linter_classes = linter_modifications_for_model.reject { |_name, status| status }.keys
  enabled_linter_classes  = linter_modifications_for_model.select { |_name, status| status }.keys

  determine_final_linters(base_linters, disabled_linter_classes, enabled_linter_classes)
end