lib/gemfilelint.rb in gemfilelint-0.1.0 vs lib/gemfilelint.rb in gemfilelint-0.2.0
- old
+ new
@@ -1,135 +1,192 @@
# frozen_string_literal: true
+require 'delegate'
require 'logger'
require 'bundler'
require 'bundler/similarity_detector'
require 'gemfilelint/version'
module Gemfilelint
+ class SpellChecker
+ attr_reader :detector, :haystack
+
+ def initialize(haystack)
+ @detector = Bundler::SimilarityDetector.new(haystack)
+ @haystack = haystack
+ end
+
+ def correct(needle)
+ return [] if haystack.include?(needle)
+
+ detector.similar_words(needle)
+ end
+ end
+
module Offenses
- class Dependency < Struct.new(:name, :suggestions)
+ class Dependency < Struct.new(:path, :name, :suggestions)
def to_s
<<~ERR
Gem \"#{name}\" is possibly misspelled, suggestions:
#{suggestions.map { |suggestion| " * #{suggestion}" }.join("\n")}"
ERR
end
end
- class Remote < Struct.new(:name, :suggestions)
+ class InvalidGemfile < Struct.new(:path)
def to_s
+ "Gemfile at \"#{path}\" is invalid."
+ end
+ end
+
+ class Remote < Struct.new(:path, :name, :suggestions)
+ def to_s
<<~ERR
Source \"#{name}\" is possibly misspelled, suggestions:
#{suggestions.map { |suggestion| " * #{suggestion}" }.join("\n")}
ERR
end
end
end
- class Linter
- class SpellChecker
- attr_reader :detector, :haystack
+ module Parser
+ class Valid < Struct.new(:path, :dsl)
+ def each_offense
+ dependencies.each do |dependency|
+ yield dependency_offense_for(dependency)
+ end
- def initialize(haystack)
- @detector = Bundler::SimilarityDetector.new(haystack)
- @haystack = haystack
+ remotes.each do |remote|
+ yield remote_offense_for(remote)
+ end
end
- def correct(needle)
- return [] if haystack.include?(needle)
+ private
- detector.similar_words(needle)
+ def dependencies
+ dsl.dependencies.map(&:name)
end
+
+ def dependency_offense_for(name)
+ corrections = Gemfilelint.dependencies.correct(name)
+ return if corrections.empty?
+
+ Offenses::Dependency.new(path, name, corrections.first(5))
+ end
+
+ # Lol wut, there has got to be a better way to do this
+ def remotes
+ dsl
+ .instance_variable_get(:@sources)
+ .instance_variable_get(:@rubygems_aggregate)
+ .remotes
+ .map(&:to_s)
+ end
+
+ def remote_offense_for(uri)
+ corrections = Gemfilelint.remotes.correct(uri)
+ return if corrections.empty?
+
+ Offenses::Remote.new(path, uri, corrections)
+ end
end
+ class Invalid < Struct.new(:path)
+ def each_offense
+ yield Offenses::InvalidGemfile.new(path)
+ end
+ end
+
+ def self.for(path)
+ Valid.new(path, Bundler::Dsl.new.tap { |dsl| dsl.eval_gemfile(path) })
+ rescue Bundler::Dsl::DSLError
+ Invalid.new(path)
+ end
+ end
+
+ class Linter
module ANSIColor
- CODES = { green: 32, magenta: 35 }.freeze
+ CODES = { green: 32, magenta: 35, cyan: 36 }.freeze
refine String do
def colorize(code)
"\033[#{CODES[code]}m#{self}\033[0m"
end
end
end
using ANSIColor
- attr_reader :dependency_checker, :remote_checker, :logger
+ attr_reader :logger
- def initialize
- common_gems = File.read(File.expand_path('gems.txt', __dir__)).split("\n")
-
- @dependency_checker = SpellChecker.new(common_gems)
- @remote_checker = SpellChecker.new(['https://rubygems.org/'])
+ def initialize(logger: nil)
+ @logger = logger || make_logger
end
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
- def lint(path, logger: nil)
- logger ||= make_logger
+ def lint(*paths)
+ logger.info("Inspecting gemfiles at #{paths.join(', ')}\n")
- logger.info("Inspecting gemfile at #{path}\n")
offenses = []
- each_offense_for(path) do |offense|
+ each_offense_for(paths) do |offense|
if offense
offenses << offense
logger.info('W'.colorize(:magenta))
else
logger.info('.'.colorize(:green))
end
end
logger.info("\n")
- return 0 if offenses.empty?
- prefix = 'W'.colorize(:magenta)
- messages = offenses.map { |offense| "#{prefix}: #{offense}\n" }
- logger.info("\nOffenses:\n\n#{messages.join}\n")
-
- 1
+ if offenses.empty?
+ true
+ else
+ messages = offenses.map { |offense| offense_to_message(offense) }
+ logger.info("\nOffenses:\n\n#{messages.join("\n")}\n")
+ false
+ end
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
private
- def make_logger
- Logger.new(STDOUT).tap do |logger|
- logger.level = :info
- logger.formatter = ->(*, message) { message }
+ def each_offense_for(paths)
+ paths.each do |path|
+ Parser.for(path).each_offense do |offense|
+ yield offense
+ end
end
end
- def each_offense_for(path)
- dsl = Bundler::Dsl.new
- dsl.eval_gemfile(path)
-
- # Lol wut, there has got to be a better way to do this
- source_list = dsl.instance_variable_get(:@sources)
- rubygems = source_list.instance_variable_get(:@rubygems_aggregate)
-
- dsl.dependencies.each do |dependency|
- yield dependency_offense_for(dependency.name)
+ def make_logger
+ Logger.new(STDOUT).tap do |creating|
+ creating.level = :info
+ creating.formatter = ->(*, message) { message }
end
+ end
- rubygems.remotes.each do |remote|
- yield remote_offense_for(remote.to_s)
- end
+ def offense_to_message(offense)
+ "#{offense.path.colorize(:cyan)}: #{'W'.colorize(:magenta)}: #{offense}"
end
+ end
- def dependency_offense_for(name)
- corrections = dependency_checker.correct(name)
- Offenses::Dependency.new(name, corrections.first(5)) if corrections.any?
+ class << self
+ def dependencies
+ @dependencies ||=
+ SpellChecker.new(
+ File.read(File.expand_path('gems.txt', __dir__)).split("\n")
+ )
end
- def remote_offense_for(uri)
- corrections = remote_checker.correct(uri)
- Offenses::Remote.new(uri, corrections) if corrections.any?
+ def remotes
+ @remotes ||= SpellChecker.new(['https://rubygems.org/'])
end
- end
- def self.lint(path, logger: nil)
- Linter.new.lint(path, logger: logger)
+ def lint(*paths, logger: nil)
+ Linter.new(logger: logger).lint(*paths)
+ end
end
end