From 822a69b81cefb603037ecafc185d3da52b765887 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Mon, 13 Nov 2017 23:20:22 +0200 Subject: [PATCH] Base YAML config loader & Ameba runner --- bench/check_sources.cr | 9 +++-- src/ameba.cr | 23 +----------- src/ameba/config.cr | 69 +++++++++++++++++++++++++++++++++++ src/ameba/rule/base.cr | 4 ++ src/ameba/rule/line_length.cr | 13 ++++++- src/ameba/runner.cr | 49 +++++++++++++++++++++++++ src/cli.cr | 23 +++++++++--- 7 files changed, 158 insertions(+), 32 deletions(-) create mode 100644 src/ameba/config.cr create mode 100644 src/ameba/runner.cr diff --git a/bench/check_sources.cr b/bench/check_sources.cr index 9e309de7..966e84e1 100644 --- a/bench/check_sources.cr +++ b/bench/check_sources.cr @@ -1,7 +1,7 @@ require "../src/ameba" require "benchmark" -private def get_sources(n) +private def get_files(n) Dir["src/**/*.cr"].first(n) end @@ -16,10 +16,11 @@ Benchmark.ips do |x| 30, 40, ].each do |n| - sources = get_sources(n) - formatter = Ameba::Formatter::BaseFormatter.new + config = Ameba::Config.load + config.formatter = Ameba::Formatter::BaseFormatter.new + config.files = get_files(n) s = n == 1 ? "" : "s" - x.report("#{n} source#{s}") { Ameba.run sources, formatter } + x.report("#{n} source#{s}") { Ameba.run config } end end diff --git a/src/ameba.cr b/src/ameba.cr index b1228038..91e728e6 100644 --- a/src/ameba.cr +++ b/src/ameba.cr @@ -6,26 +6,7 @@ require "./ameba/formatter/*" module Ameba extend self - def run(formatter = Formatter::BaseFormatter.new) - run Dir["**/*.cr"].reject(&.starts_with? "lib/"), formatter - end - - def run(files, formatter : Formatter::BaseFormatter) - sources = files.map { |path| Source.new(File.read(path), path) } - - formatter.started sources - sources.each do |source| - formatter.source_started source - catch(source) - formatter.source_finished source - end - formatter.finished sources - sources - end - - def catch(source : Source) - Rule.rules.each do |rule| - rule.new.test(source) - end + def run(config = Config.load) + Runner.new(config).run end end diff --git a/src/ameba/config.cr b/src/ameba/config.cr new file mode 100644 index 00000000..5be157bb --- /dev/null +++ b/src/ameba/config.cr @@ -0,0 +1,69 @@ +require "yaml" + +class Ameba::Config + setter formatter : Formatter::BaseFormatter? + setter files : Array(String)? + + def initialize(@config : YAML::Any) + end + + def self.load(path = ".ameba.yml") + content = (path && File.exists? path) ? File.read path : "{}" + Config.new YAML.parse(content) + end + + def files + @files ||= default_files + end + + def formatter + @formatter ||= default_formatter + end + + def subconfig(name) + @config[name]? + end + + private def default_files + Dir["**/*.cr"].reject(&.starts_with? "lib/") + end + + private def default_formatter + Formatter::DotFormatter.new + end + + module Rule + getter config : YAML::Any? + + macro prop(assign) + # Rule configuration property. + def {{assign.target}} + {% prop_name = assign.target.id.camelcase.gsub(/\?/, "") %} + + {% if assign.value.is_a? NumberLiteral %} + int_prop "{{prop_name}}", {{assign.value}} + {% elsif assign.value.is_a? BoolLiteral %} + bool_prop "{{prop_name}}", {{assign.value}} + {% elsif assign.value.is_a? StringLiteral %} + str_prop "{{prop_name}}", {{assign.value}} + {% end %} + end + end + + def initialize(config = nil) + @config = config.try &.subconfig(name) + end + + protected def int_prop(name, default : Number) + str_prop(name, default).to_i + end + + protected def bool_prop(name, default : Bool) + str_prop(name, default.to_s) == "true" + end + + protected def str_prop(name, default) + config.try &.[name]?.try &.as_s || default + end + end +end diff --git a/src/ameba/rule/base.cr b/src/ameba/rule/base.cr index e64d1c8e..b1ac6e8d 100644 --- a/src/ameba/rule/base.cr +++ b/src/ameba/rule/base.cr @@ -1,7 +1,11 @@ module Ameba::Rule abstract struct Base + include Config::Rule + abstract def test(source : Source) + prop enabled? = true + def test(source : Source, node : Crystal::ASTNode) # can't be abstract end diff --git a/src/ameba/rule/line_length.cr b/src/ameba/rule/line_length.cr index 143421c5..6b49f30c 100644 --- a/src/ameba/rule/line_length.cr +++ b/src/ameba/rule/line_length.cr @@ -1,10 +1,19 @@ module Ameba::Rule - # A rule that disallows lines longer than 80 symbols. + # A rule that disallows lines longer than `max_length` number of symbols. # + # YAML configuration example: + # + # ``` + # LineLength: + # Enabled: true + # MaxLength: 100 + # ``` struct LineLength < Base + prop max_length = 80 + def test(source) source.lines.each_with_index do |line, index| - next unless line.size > 80 + next unless line.size > max_length source.error self, source.location(index + 1, line.size), "Line too long" diff --git a/src/ameba/runner.cr b/src/ameba/runner.cr new file mode 100644 index 00000000..b71a8b69 --- /dev/null +++ b/src/ameba/runner.cr @@ -0,0 +1,49 @@ +module Ameba + class Runner + @rules : Array(Rule::Base) + @sources : Array(Source) + @formatter : Formatter::BaseFormatter + + def initialize(config : Config) + @rules = load_rules(config) + @sources = load_sources(config) + @formatter = config.formatter + end + + def run + @formatter.started @sources + @sources.each do |source| + @formatter.source_started source + + @rules.each &.test(source) + + @formatter.source_finished source + end + self + ensure + @formatter.finished @sources + end + + def success? + @sources.all? &.valid? + end + + def test_source(source) + @formatter.source_started source + @rules.each &.test(source) + ensure + @formatter.source_finished source + end + + private def load_sources(config) + config.files + .map { |wildcard| Dir[wildcard] } + .flatten + .map { |path| Source.new File.read(path), path } + end + + private def load_rules(config) + Rule.rules.map { |r| r.new config }.select &.enabled? + end + end +end diff --git a/src/cli.cr b/src/cli.cr index 5e22f889..544c8581 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -1,10 +1,10 @@ require "option_parser" require "./ameba" -formatter = Ameba::Formatter::DotFormatter +files, formatter, config_path = nil, nil, nil OptionParser.parse(ARGV) do |parser| - parser.banner = "Usage: ameba [options]" + parser.banner = "Usage: ameba [options] [file1 file2 ...]" parser.on("-v", "--version", "Print version") do puts Ameba::VERSION @@ -17,10 +17,23 @@ OptionParser.parse(ARGV) do |parser| end parser.on("-s", "--silent", "Disable output") do - formatter = Ameba::Formatter::BaseFormatter + formatter = Ameba::Formatter::BaseFormatter.new + end + + # parser.on("-f FORMATTER", "--format FORMATTER", "Specify formatter") do |f| + # end + + parser.on("-c PATH", "--config PATH", "Specify configuration file") do |f| + config_path = f + end + + parser.unknown_args do |f| + files = f if f.any? end end -files = Dir["**/*.cr"] +config = Ameba::Config.load config_path +config.formatter = formatter +config.files = files -exit(1) unless Ameba.run(files, formatter.new).all? &.valid? +exit(1) unless Ameba.run(config).success?