From 57b1095c5fadb9e535d77962f8facd555515c56f Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Wed, 15 Nov 2017 20:49:09 +0200 Subject: [PATCH] Document entities --- spec/ameba/ast/traverse_spec.cr | 2 +- src/ameba.cr | 30 ++++++++++ src/ameba/ast/traverse.cr | 25 +++++++- src/ameba/ast/util.cr | 6 ++ src/ameba/config.cr | 86 ++++++++++++++++++++++++--- src/ameba/formatter/base_formatter.cr | 14 ++++- src/ameba/formatter/dot_formatter.cr | 5 ++ src/ameba/rule/base.cr | 50 ++++++++++++++++ src/ameba/runner.cr | 53 +++++++++++++++++ src/ameba/source.cr | 65 ++++++++++++++++++-- src/ameba/tokenizer.cr | 25 ++++++++ src/ameba/version.cr | 3 - 12 files changed, 346 insertions(+), 18 deletions(-) delete mode 100644 src/ameba/version.cr diff --git a/spec/ameba/ast/traverse_spec.cr b/spec/ameba/ast/traverse_spec.cr index 5d32ed6e..7dc31b19 100644 --- a/spec/ameba/ast/traverse_spec.cr +++ b/spec/ameba/ast/traverse_spec.cr @@ -5,7 +5,7 @@ module Ameba::AST source = Source.new "" describe "Traverse" do - {% for name in NODE_VISITORS %} + {% for name in NODES %} describe "{{name}}" do it "allow to visit {{name}} node" do visitor = Visitor.new rule, source diff --git a/src/ameba.cr b/src/ameba.cr index 91e728e6..d91c953e 100644 --- a/src/ameba.cr +++ b/src/ameba.cr @@ -3,9 +3,39 @@ require "./ameba/ast/*" require "./ameba/rule/*" require "./ameba/formatter/*" +# Ameba's entry module. +# +# To run the linter with default parameters: +# +# ``` +# Ameba.run +# ``` +# +# To configure and run it: +# +# ``` +# config = Ameba::Config.load +# config.formatter = formatter +# config.files = file_paths +# +# Ameba.run config +# ``` +# module Ameba extend self + VERSION = "0.2.0" + + # Initializes `Ameba::Runner` and runs it. + # Can be configured via `config` parameter. + # + # Examples: + # + # ``` + # Ameba.run + # Ameba.run config + # ``` + # def run(config = Config.load) Runner.new(config).run end diff --git a/src/ameba/ast/traverse.cr b/src/ameba/ast/traverse.cr index 41c113f2..ed1a3e8a 100644 --- a/src/ameba/ast/traverse.cr +++ b/src/ameba/ast/traverse.cr @@ -1,7 +1,9 @@ require "compiler/crystal/syntax/*" +# A module that helps to traverse Crystal AST using `Crystal::Visitor`. module Ameba::AST - NODE_VISITORS = [ + # List of nodes to be visited by Ameba's rules. + NODES = [ Alias, Assign, Call, @@ -20,19 +22,38 @@ module Ameba::AST Var, ] + # An AST Visitor used by rules. + # + # ``` + # visitor = Ameba::AST::Visitor.new(rule, source) + # ``` + # class Visitor < Crystal::Visitor + # A corresponding rule that uses this visitor. @rule : Rule::Base + + # A source that needs to be traversed. @source : Source + # Creates instance of this visitor. + # + # ``` + # visitor = Ameba::AST::Visitor.new(rule, source) + # ``` + # def initialize(@rule, @source) @source.ast.accept self end + # A main visit method that accepts `Crystal::ASTNode`. + # Returns true meaning all child nodes will be traversed. def visit(node : Crystal::ASTNode) true end - {% for name in NODE_VISITORS %} + {% for name in NODES %} + # A visit callback for `Crystal::{{name}}` node. + # Returns true meaning that child nodes will be traversed as well. def visit(node : Crystal::{{name}}) @rule.test @source, node true diff --git a/src/ameba/ast/util.cr b/src/ameba/ast/util.cr index 9a885b95..7aa22089 100644 --- a/src/ameba/ast/util.cr +++ b/src/ameba/ast/util.cr @@ -1,12 +1,18 @@ +# Utility module for Ameba's rules. module Ameba::AST::Util + # Returns true if current `node` is a literal, false - otherwise. def literal?(node) node.try &.class.name.ends_with? "Literal" end + # Returns true if current `node` is a string literal, false - otherwise. def string_literal?(node) node.is_a? Crystal::StringLiteral end + # Returns a source code for the current node. + # This method uses `node.location` and `node.end_location` + # to determine and cut a piece of source of the node. def node_source(node, code_lines) loc, end_loc = node.location, node.end_location diff --git a/src/ameba/config.cr b/src/ameba/config.cr index 44a2f207..cbd2179a 100644 --- a/src/ameba/config.cr +++ b/src/ameba/config.cr @@ -1,29 +1,73 @@ require "yaml" +# A configuration entry for `Ameba::Runner`. +# +# Config can be loaded from configuration YAML file and adjusted. +# +# ``` +# config = Config.load +# config.formatter = my_formatter +# ``` +# +# By default config loads `.ameba.yml` file in a current directory. +# class Ameba::Config setter formatter : Formatter::BaseFormatter? setter files : Array(String)? - def initialize(@config : YAML::Any) + # Creates a new instance of `Ameba::Config` based on YAML parameters. + # + # `Config.load` uses this constructor to instantiate new config by YAML file. + protected def initialize(@config : YAML::Any) end - def self.load(path = ".ameba.yml") - content = (path && File.exists? path) ? File.read path : "{}" + # Loads YAML configuration file by `path`. + # + # ``` + # config = Ameba::Config.load + # ``` + # + def self.load(path = nil) + path ||= ".ameba.yml" + content = File.exists?(path) ? File.read path : "{}" Config.new YAML.parse(content) end - def self.load(path : Nil) - self.load - end - + # Returns a list of paths (with wildcards) to files. + # Represents a list of sources to be inspected. + # If files are not set, it will return default list of files. + # + # ``` + # config = Ameba::Config.load + # config.files = ["**/*.cr"] + # config.files + # ``` + # def files @files ||= default_files end + # Returns a formatter to be used while inspecting files. + # If formatter is not set, it will return default formatter. + # + # ``` + # config = Ameba::Config.load + # config.formatter = custom_formatter + # config.formatter + # ``` + # def formatter @formatter ||= default_formatter end + # Returns a subconfig of a home full loaded configuration. + # This is used to get a corresponding to a specific rule config. + # + # ``` + # config = Ameba::Config.load + # config.subconfig "LineLength" + # ``` + # def subconfig(name) @config[name]? end @@ -36,9 +80,23 @@ class Ameba::Config Formatter::DotFormatter.new end + # An entity that represents a corresponding configuration for a specific Rule. module Rule + # Represents a configuration of a specific Rule. getter config : YAML::Any? + # A macro that defines a dsl to define configurable properties. + # + # ``` + # class Configurable + # include Ameba::Config::Rule + # + # prop enabled? = false + # prop max_length = 80 + # prop wildcard = "*" + # end + # ``` + # macro prop(assign) # Rule configuration property. def {{assign.target}} @@ -54,6 +112,20 @@ class Ameba::Config end end + # Creates an instance of a Rule configuration. + # + # ``` + # class Configurable + # include Ameba::Config::Rule + # + # prop enabled? = false + # prop max_length = 80 + # prop wildcard = "*" + # end + # + # Configurable.new config + # ``` + # def initialize(config = nil) @config = config.try &.subconfig(name) end diff --git a/src/ameba/formatter/base_formatter.cr b/src/ameba/formatter/base_formatter.cr index 764c610b..de4d8c6a 100644 --- a/src/ameba/formatter/base_formatter.cr +++ b/src/ameba/formatter/base_formatter.cr @@ -1,17 +1,29 @@ +# A module that utilizes Ameba's formatters. module Ameba::Formatter + # A base formatter for all formatters. It uses `output` IO + # to report results and also implements stub methods for + # callbacks in `Ameba::Runner#run` method. class BaseFormatter - # allow other IOs + # TODO: allow other IOs getter output : IO::FileDescriptor | IO::Memory def initialize(@output = STDOUT) end + # Callback that indicates when inspecting is started. + # A list of sources to inspect is passed as an argument. def started(sources); end + # Callback that indicates when source inspection is finished. + # A corresponding source is passed as an argument. def source_finished(source : Source); end + # Callback that indicates when source inspection is finished. + # A corresponding source is passed as an argument. def source_started(source : Source); end + # Callback that indicates when inspection is finished. + # A list of inspected sources is passed as an argument. def finished(sources); end end end diff --git a/src/ameba/formatter/dot_formatter.cr b/src/ameba/formatter/dot_formatter.cr index 62de69a9..a2323659 100644 --- a/src/ameba/formatter/dot_formatter.cr +++ b/src/ameba/formatter/dot_formatter.cr @@ -1,19 +1,24 @@ module Ameba::Formatter + # A formatter that shows a progress of inspection in a terminal using dots. + # It is similar to Crystal's dot formatter for specs. class DotFormatter < BaseFormatter @started_at : Time? + # Reports a message when inspection is started. def started(sources) @started_at = Time.now # Time.monotonic output << started_message(sources.size) end + # Reports a result of the inspection of a corresponding source. def source_finished(source : Source) sym = source.valid? ? ".".colorize(:green) : "F".colorize(:red) output << sym output.flush end + # Reports a message when inspection is finished. def finished(sources) output << "\n\n" failed_sources = sources.reject { |s| s.valid? } diff --git a/src/ameba/rule/base.cr b/src/ameba/rule/base.cr index b1ac6e8d..b1055b62 100644 --- a/src/ameba/rule/base.cr +++ b/src/ameba/rule/base.cr @@ -1,19 +1,63 @@ module Ameba::Rule + # Represents a base of all rules. In other words, all rules + # inherits from this struct: + # + # ``` + # struct MyRule < Ameba::Rule::Base + # def test(source) + # if invalid?(source) + # source.error self, location, "Something wrong." + # end + # end + # + # private def invalid?(source) + # # ... + # end + # end + # ``` + # + # Enforces rules to implement an abstract `#test` method which + # is designed to test the source passed in. If source has issues + # that are tested by this rule, it should add an error. + # abstract struct Base include Config::Rule + # This method is designed to test the source passed in. If source has issues + # that are tested by this rule, it should add an error. abstract def test(source : Source) + # Enabled property indicates whether this rule enabled or not. + # Only enabled rules will be included into the inspection. prop enabled? = true def test(source : Source, node : Crystal::ASTNode) # can't be abstract end + # A convenient addition to `#test` method that does the same + # but returns a passed in `source` as an addition. + # + # ``` + # source = MyRule.new.catch(source) + # source.valid? + # ``` + # def catch(source : Source) source.tap { |s| test s } end + # Returns a name of this rule, which is basically a class name. + # + # ``` + # struct MyRule < Ameba::Rule::Base + # def test(source) + # end + # end + # + # MyRule.new.name # => "MyRule" + # ``` + # def name self.class.name.gsub("Ameba::Rule::", "") end @@ -23,6 +67,12 @@ module Ameba::Rule end end + # Returns a list of all available rules. + # + # ``` + # Ameba::Rule.rules # => [LineLength, ConstantNames, ....] + # ``` + # def self.rules Base.subclasses end diff --git a/src/ameba/runner.cr b/src/ameba/runner.cr index 5de2e5f1..57f60173 100644 --- a/src/ameba/runner.cr +++ b/src/ameba/runner.cr @@ -1,19 +1,63 @@ module Ameba + # Represents a runner for inspecting sources files. + # Holds a list of rules to do inspection based on, + # list of sources to run inspection on and a formatter + # to prepare a report. + # + # ``` + # config = Ameba::Config.load + # runner = Ameba::Runner.new config + # runner.run.success? # => true or false + # ``` + # class Runner + # A list of rules to do inspection based on. @rules : Array(Rule::Base) + + # A list of sources to run inspection on. @sources : Array(Source) + + # A formatter to prepare report. @formatter : Formatter::BaseFormatter + # Instantiates a runner using a `config`. + # + # ``` + # config = Ameba::Config.load + # config.files = files + # config.formatter = formatter + # + # Ameba::Runner.new config + # ``` + # def initialize(config : Config) @rules = load_rules(config) @sources = load_sources(config) @formatter = config.formatter end + # Instantiates a runner using a list of sources and a formatter. + # + # ``` + # runner = Ameba::Runner.new sources, formatter + # ``` + # def initialize(@sources, @formatter) @rules = load_rules nil end + # Performs the inspection. Iterates through all sources and test it using + # list of rules. If a specific rule fails on a specific source, it adds + # an error to that source. + # + # This action also notifies formatter when inspection is started/finished, + # and when a specific source started/finished to be inspected. + # + # ``` + # runner = Ameba::Runner.new config + # runner.run # => returns runner again + # ``` + # def run @formatter.started @sources @sources.each do |source| @@ -28,6 +72,15 @@ module Ameba @formatter.finished @sources end + # Indicates whether the last inspection successful or not. + # It returns true if no issues in sources found, false otherwise. + # + # ``` + # runner = Ameba::Runner.new config + # runner.run + # runner.success? # => true or false + # ``` + # def success? @sources.all? &.valid? end diff --git a/src/ameba/source.cr b/src/ameba/source.cr index 8a1a6293..0c3c950d 100644 --- a/src/ameba/source.cr +++ b/src/ameba/source.cr @@ -11,27 +11,76 @@ module Ameba location : Crystal::Location?, message : String - getter lines : Array(String)? - getter errors = [] of Error + # Path to the source file. getter path : String? - getter code : String - getter ast : Crystal::ASTNode? + # Crystal code (content of a source file). + getter code : String + + # List of errors reported. + getter errors = [] of Error + + @lines : Array(String)? + @ast : Crystal::ASTNode? + + # Creates a new source by `code` and `path`. + # + # For example: + # + # ``` + # path = "./src/source.cr" + # Ameba::Source.new File.read(path), path + # ``` + # def initialize(@code : String, @path = nil) end + # Add new error to the list of errors. + # + # ``` + # source.error rule, location, "Line too long" + # ``` + # def error(rule : Rule::Base, location, message : String) errors << Error.new rule, location, message end + # Indicates whether source is valid or not. + # Returns true if the list or errors empty, false otherwise. + # + # ``` + # source = Ameba::Source.new code, path + # source.valid? # => true + # source.error rule, location, message + # source.valid? # => false + # ``` + # def valid? errors.empty? end + # Returns lines of code splitted by new line character. + # Since `code` is immutable and can't be changed, this + # method caches lines in an instance variable, so calling + # it second time will not perform a split, but will return + # lines instantly. + # + # ``` + # source = Ameba::Source.new "a = 1\nb = 2", path + # source.lines # => ["a = 1", "b = 2"] + # ``` + # def lines @lines ||= @code.split("\n") end + # Returns AST nodes constructed by `Crystal::Parser`. + # + # ``` + # source = Ameba::Source.new code, path + # source.ast + # ``` + # def ast @ast ||= Crystal::Parser.new(code) @@ -39,6 +88,14 @@ module Ameba .parse end + # Returns a new instance of the `Crystal::Location` in current + # source based on the line number `l` and column number `c`. + # + # ``` + # s = Ameba::Source.new code, path + # s.location(3, 76) + # ``` + # def location(l, c) Crystal::Location.new path, l, c end diff --git a/src/ameba/tokenizer.cr b/src/ameba/tokenizer.cr index c1c06bb4..deefb111 100644 --- a/src/ameba/tokenizer.cr +++ b/src/ameba/tokenizer.cr @@ -1,7 +1,24 @@ require "compiler/crystal/syntax/*" module Ameba + # Represents Crystal syntax tokenizer based on `Crystal::Lexer`. + # + # ``` + # source = Ameba::Source.new code, path + # tokenizer = Ameba::Tokenizer.new(source) + # tokenizer.run do |token| + # puts token + # end + # ``` + # class Tokenizer + # Instantiates Tokenizer using a `source`. + # + # ``` + # source = Ameba::Source.new code, path + # Ameba::Tokenizer.new(source) + # ``` + # def initialize(source) @lexer = Crystal::Lexer.new source.code @lexer.count_whitespace = true @@ -10,6 +27,14 @@ module Ameba @lexer.filename = source.path end + # Runs the tokenizer and yields each token as a block argument. + # + # ``` + # Ameba::Tokenizer.new(source).run do |token| + # puts token + # end + # ``` + # def run(&block : Crystal::Token -> _) run_normal_state @lexer, &block true diff --git a/src/ameba/version.cr b/src/ameba/version.cr deleted file mode 100644 index a7d997da..00000000 --- a/src/ameba/version.cr +++ /dev/null @@ -1,3 +0,0 @@ -module Ameba - VERSION = "0.2.0" -end