mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Merge pull request #374 from crystal-ameba/lint-documentation-rule
Add `Lint/Documentation` rule
This commit is contained in:
commit
aceb054aa0
12 changed files with 321 additions and 47 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -36,4 +36,4 @@ jobs:
|
||||||
run: shards build -Dpreview_mt
|
run: shards build -Dpreview_mt
|
||||||
|
|
||||||
- name: Run ameba linter
|
- name: Run ameba linter
|
||||||
run: bin/ameba --all
|
run: bin/ameba
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@ build:
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: build
|
lint: build
|
||||||
./bin/ameba --all
|
./bin/ameba
|
||||||
|
|
||||||
.PHONY: spec
|
.PHONY: spec
|
||||||
spec:
|
spec:
|
||||||
|
|
|
@ -99,15 +99,6 @@ $ ameba --explain crystal/command/format.cr:26:83 # same thing
|
||||||
|
|
||||||
### Run in parallel
|
### Run in parallel
|
||||||
|
|
||||||
Starting from 0.31.0 Crystal [supports parallelism](https://crystal-lang.org/2019/09/06/parallelism-in-crystal.html).
|
|
||||||
It allows to run linting in parallel too.
|
|
||||||
In order to take advantage of this feature you need to build ameba with preview_mt support:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ crystal build src/cli.cr -Dpreview_mt -o bin/ameba
|
|
||||||
$ make install
|
|
||||||
```
|
|
||||||
|
|
||||||
Some quick benchmark results measured while running Ameba on Crystal repo:
|
Some quick benchmark results measured while running Ameba on Crystal repo:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
@ -2,6 +2,17 @@ require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
describe ScopeVisitor do
|
describe ScopeVisitor do
|
||||||
|
{% for type in %w[class module enum].map(&.id) %}
|
||||||
|
it "creates a scope for the {{ type }} def" do
|
||||||
|
rule = ScopeRule.new
|
||||||
|
ScopeVisitor.new rule, Source.new <<-CRYSTAL
|
||||||
|
{{ type }} Foo
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
rule.scopes.size.should eq 1
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
it "creates a scope for the def" do
|
it "creates a scope for the def" do
|
||||||
rule = ScopeRule.new
|
rule = ScopeRule.new
|
||||||
ScopeVisitor.new rule, Source.new <<-CRYSTAL
|
ScopeVisitor.new rule, Source.new <<-CRYSTAL
|
||||||
|
@ -54,5 +65,33 @@ module Ameba::AST
|
||||||
outer_block.outer_scope.should be_nil
|
outer_block.outer_scope.should be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "#visibility" do
|
||||||
|
it "is being properly set" do
|
||||||
|
rule = ScopeRule.new
|
||||||
|
ScopeVisitor.new rule, Source.new <<-CRYSTAL
|
||||||
|
private class Foo
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
rule.scopes.size.should eq 1
|
||||||
|
rule.scopes.first.visibility.should eq Crystal::Visibility::Private
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is being inherited from the outer scope(s)" do
|
||||||
|
rule = ScopeRule.new
|
||||||
|
ScopeVisitor.new rule, Source.new <<-CRYSTAL
|
||||||
|
private class Foo
|
||||||
|
class Bar
|
||||||
|
def baz
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
rule.scopes.size.should eq 3
|
||||||
|
rule.scopes.each &.visibility.should eq Crystal::Visibility::Private
|
||||||
|
rule.scopes.last.node.visibility.should eq Crystal::Visibility::Private
|
||||||
|
rule.scopes[0...-1].each &.node.visibility.should eq Crystal::Visibility::Public
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
151
spec/ameba/rule/lint/documentation_spec.cr
Normal file
151
spec/ameba/rule/lint/documentation_spec.cr
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
require "../../../spec_helper"
|
||||||
|
|
||||||
|
module Ameba::Rule::Lint
|
||||||
|
subject = Documentation.new
|
||||||
|
.tap(&.ignore_classes = false)
|
||||||
|
.tap(&.ignore_modules = false)
|
||||||
|
.tap(&.ignore_enums = false)
|
||||||
|
.tap(&.ignore_defs = false)
|
||||||
|
.tap(&.ignore_macros = false)
|
||||||
|
|
||||||
|
describe Documentation do
|
||||||
|
it "passes for undocumented private types" do
|
||||||
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
|
private class Foo
|
||||||
|
def foo
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private module Bar
|
||||||
|
def bar
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private enum Baz
|
||||||
|
end
|
||||||
|
|
||||||
|
private def bat
|
||||||
|
end
|
||||||
|
|
||||||
|
private macro bag
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
|
||||||
|
it "passes for documented public types" do
|
||||||
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
|
# Foo
|
||||||
|
class Foo
|
||||||
|
# foo
|
||||||
|
def foo
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bar
|
||||||
|
module Bar
|
||||||
|
# bar
|
||||||
|
def bar
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Baz
|
||||||
|
enum Baz
|
||||||
|
end
|
||||||
|
|
||||||
|
# bat
|
||||||
|
def bat
|
||||||
|
end
|
||||||
|
|
||||||
|
# bag
|
||||||
|
macro bag
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
|
||||||
|
it "fails if there is an undocumented public type" do
|
||||||
|
expect_issue subject, <<-CRYSTAL
|
||||||
|
class Foo
|
||||||
|
# ^^^^^^^^^ error: Missing documentation
|
||||||
|
end
|
||||||
|
|
||||||
|
module Bar
|
||||||
|
# ^^^^^^^^^^ error: Missing documentation
|
||||||
|
end
|
||||||
|
|
||||||
|
enum Baz
|
||||||
|
# ^^^^^^^^ error: Missing documentation
|
||||||
|
end
|
||||||
|
|
||||||
|
def bat
|
||||||
|
# ^^^^^^^ error: Missing documentation
|
||||||
|
end
|
||||||
|
|
||||||
|
macro bag
|
||||||
|
# ^^^^^^^^^ error: Missing documentation
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
|
||||||
|
context "properties" do
|
||||||
|
describe "#ignore_classes" do
|
||||||
|
it "lets the rule to ignore method definitions if true" do
|
||||||
|
rule = Documentation.new
|
||||||
|
rule.ignore_classes = true
|
||||||
|
|
||||||
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
|
class Foo
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ignore_modules" do
|
||||||
|
it "lets the rule to ignore method definitions if true" do
|
||||||
|
rule = Documentation.new
|
||||||
|
rule.ignore_modules = true
|
||||||
|
|
||||||
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
|
module Bar
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ignore_enums" do
|
||||||
|
it "lets the rule to ignore method definitions if true" do
|
||||||
|
rule = Documentation.new
|
||||||
|
rule.ignore_enums = true
|
||||||
|
|
||||||
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
|
enum Baz
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ignore_defs" do
|
||||||
|
it "lets the rule to ignore method definitions if true" do
|
||||||
|
rule = Documentation.new
|
||||||
|
rule.ignore_defs = true
|
||||||
|
|
||||||
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
|
def bat
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#ignore_macros" do
|
||||||
|
it "lets the rule to ignore macros if true" do
|
||||||
|
rule = Documentation.new
|
||||||
|
rule.ignore_macros = true
|
||||||
|
|
||||||
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
|
macro bag
|
||||||
|
end
|
||||||
|
CRYSTAL
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -43,6 +43,9 @@ module Ameba
|
||||||
description "Internal rule to test scopes"
|
description "Internal rule to test scopes"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::VisibilityModifier, scope : AST::Scope)
|
||||||
|
end
|
||||||
|
|
||||||
def test(source, node : Crystal::ASTNode, scope : AST::Scope)
|
def test(source, node : Crystal::ASTNode, scope : AST::Scope)
|
||||||
@scopes << scope
|
@scopes << scope
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,9 @@ module Ameba::AST
|
||||||
# Whether the scope yields.
|
# Whether the scope yields.
|
||||||
setter yields = false
|
setter yields = false
|
||||||
|
|
||||||
|
# Scope visibility level
|
||||||
|
setter visibility : Crystal::Visibility?
|
||||||
|
|
||||||
# Link to local variables
|
# Link to local variables
|
||||||
getter variables = [] of Variable
|
getter variables = [] of Variable
|
||||||
|
|
||||||
|
@ -121,10 +124,7 @@ module Ameba::AST
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
def spawn_block?
|
def spawn_block?
|
||||||
return false unless node.is_a?(Crystal::Block)
|
node.as?(Crystal::Block).try(&.call).try(&.name) == "spawn"
|
||||||
|
|
||||||
call = node.as(Crystal::Block).call
|
|
||||||
call.try(&.name) == "spawn"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns `true` if current scope sits inside a macro.
|
# Returns `true` if current scope sits inside a macro.
|
||||||
|
@ -167,9 +167,12 @@ module Ameba::AST
|
||||||
# Returns `true` if current scope (or any of inner scopes) yields,
|
# Returns `true` if current scope (or any of inner scopes) yields,
|
||||||
# `false` otherwise.
|
# `false` otherwise.
|
||||||
def yields?(check_inner_scopes = true)
|
def yields?(check_inner_scopes = true)
|
||||||
return true if @yields
|
@yields || (check_inner_scopes && inner_scopes.any?(&.yields?))
|
||||||
return inner_scopes.any?(&.yields?) if check_inner_scopes
|
end
|
||||||
false
|
|
||||||
|
# Returns visibility of the current scope (could be inherited from the outer scope).
|
||||||
|
def visibility
|
||||||
|
@visibility || outer_scope.try(&.visibility)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns `true` if current scope is a def, `false` otherwise.
|
# Returns `true` if current scope is a def, `false` otherwise.
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
require "./base_visitor"
|
require "./base_visitor"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
|
# An AST Visitor that traverses the source and allows all nodes
|
||||||
|
# to be inspected by rules.
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
|
||||||
|
# ```
|
||||||
|
class NodeVisitor < BaseVisitor
|
||||||
# List of nodes to be visited by Ameba's rules.
|
# List of nodes to be visited by Ameba's rules.
|
||||||
NODES = {
|
NODES = {
|
||||||
Alias,
|
Alias,
|
||||||
|
@ -29,13 +36,6 @@ module Ameba::AST
|
||||||
Until,
|
Until,
|
||||||
}
|
}
|
||||||
|
|
||||||
# An AST Visitor that traverses the source and allows all nodes
|
|
||||||
# to be inspected by rules.
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
|
|
||||||
# ```
|
|
||||||
class NodeVisitor < BaseVisitor
|
|
||||||
@skip : Array(Crystal::ASTNode.class)?
|
@skip : Array(Crystal::ASTNode.class)?
|
||||||
|
|
||||||
def initialize(@rule, @source, skip = nil)
|
def initialize(@rule, @source, skip = nil)
|
||||||
|
@ -43,6 +43,11 @@ module Ameba::AST
|
||||||
super @rule, @source
|
super @rule, @source
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visit(node : Crystal::VisibilityModifier)
|
||||||
|
node.exp.visibility = node.modifier
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
{% for name in NODES %}
|
{% for name in NODES %}
|
||||||
# A visit callback for `Crystal::{{ name }}` node.
|
# A visit callback for `Crystal::{{ name }}` node.
|
||||||
#
|
#
|
||||||
|
|
|
@ -25,6 +25,7 @@ module Ameba::AST
|
||||||
@scope_queue = [] of Scope
|
@scope_queue = [] of Scope
|
||||||
@current_scope : Scope
|
@current_scope : Scope
|
||||||
@current_assign : Crystal::ASTNode?
|
@current_assign : Crystal::ASTNode?
|
||||||
|
@visibility_modifier : Crystal::Visibility?
|
||||||
@skip : Array(Crystal::ASTNode.class)?
|
@skip : Array(Crystal::ASTNode.class)?
|
||||||
|
|
||||||
def initialize(@rule, @source, skip = nil)
|
def initialize(@rule, @source, skip = nil)
|
||||||
|
@ -36,12 +37,18 @@ module Ameba::AST
|
||||||
|
|
||||||
private def on_scope_enter(node)
|
private def on_scope_enter(node)
|
||||||
return if skip?(node)
|
return if skip?(node)
|
||||||
@current_scope = Scope.new(node, @current_scope)
|
|
||||||
|
scope = Scope.new(node, @current_scope)
|
||||||
|
scope.visibility = @visibility_modifier
|
||||||
|
|
||||||
|
@current_scope = scope
|
||||||
end
|
end
|
||||||
|
|
||||||
private def on_scope_end(node)
|
private def on_scope_end(node)
|
||||||
@scope_queue << @current_scope
|
@scope_queue << @current_scope
|
||||||
|
|
||||||
|
@visibility_modifier = nil
|
||||||
|
|
||||||
# go up if this is not a top level scope
|
# go up if this is not a top level scope
|
||||||
return unless outer_scope = @current_scope.outer_scope
|
return unless outer_scope = @current_scope.outer_scope
|
||||||
@current_scope = outer_scope
|
@current_scope = outer_scope
|
||||||
|
@ -64,6 +71,12 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def visit(node : Crystal::VisibilityModifier)
|
||||||
|
@visibility_modifier = node.exp.visibility = node.modifier
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def visit(node : Crystal::Yield)
|
def visit(node : Crystal::Yield)
|
||||||
@current_scope.yields = true
|
@current_scope.yields = true
|
||||||
|
@ -109,6 +122,7 @@ module Ameba::AST
|
||||||
@current_assign = node.value unless node.value.nil?
|
@current_assign = node.value unless node.value.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
def end_visit(node : Crystal::TypeDeclaration)
|
def end_visit(node : Crystal::TypeDeclaration)
|
||||||
return unless (var = node.var).is_a?(Crystal::Var)
|
return unless (var = node.var).is_a?(Crystal::Var)
|
||||||
|
|
||||||
|
|
|
@ -240,6 +240,7 @@ class Ameba::Config
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
module RuleConfig
|
module RuleConfig
|
||||||
|
# Define rule properties
|
||||||
macro properties(&block)
|
macro properties(&block)
|
||||||
{% definitions = [] of NamedTuple %}
|
{% definitions = [] of NamedTuple %}
|
||||||
{% if block.body.is_a? Assign %}
|
{% if block.body.is_a? Assign %}
|
||||||
|
|
|
@ -112,6 +112,7 @@ module Ameba::Rule
|
||||||
name.hash
|
name.hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Adds an issue to the *source*
|
||||||
macro issue_for(*args, **kwargs, &block)
|
macro issue_for(*args, **kwargs, &block)
|
||||||
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }}
|
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }}
|
||||||
end
|
end
|
||||||
|
|
66
src/ameba/rule/lint/documentation.cr
Normal file
66
src/ameba/rule/lint/documentation.cr
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
module Ameba::Rule::Lint
|
||||||
|
# A rule that enforces documentation for public types:
|
||||||
|
# modules, classes, enums, methods and macros.
|
||||||
|
#
|
||||||
|
# YAML configuration example:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# Lint/Documentation:
|
||||||
|
# Enabled: true
|
||||||
|
# ```
|
||||||
|
class Documentation < Base
|
||||||
|
properties do
|
||||||
|
enabled false
|
||||||
|
description "Enforces public types to be documented"
|
||||||
|
|
||||||
|
ignore_classes false
|
||||||
|
ignore_modules true
|
||||||
|
ignore_enums false
|
||||||
|
ignore_defs true
|
||||||
|
ignore_macros false
|
||||||
|
end
|
||||||
|
|
||||||
|
MSG = "Missing documentation"
|
||||||
|
|
||||||
|
MACRO_HOOK_NAMES = %w[
|
||||||
|
inherited
|
||||||
|
included extended
|
||||||
|
method_missing method_added
|
||||||
|
finished
|
||||||
|
]
|
||||||
|
|
||||||
|
def test(source)
|
||||||
|
AST::ScopeVisitor.new self, source
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ClassDef, scope : AST::Scope)
|
||||||
|
ignore_classes? || check_missing_doc(source, node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::ModuleDef, scope : AST::Scope)
|
||||||
|
ignore_modules? || check_missing_doc(source, node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::EnumDef, scope : AST::Scope)
|
||||||
|
ignore_enums? || check_missing_doc(source, node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::Def, scope : AST::Scope)
|
||||||
|
ignore_defs? || check_missing_doc(source, node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test(source, node : Crystal::Macro, scope : AST::Scope)
|
||||||
|
node.name.in?(MACRO_HOOK_NAMES) ||
|
||||||
|
ignore_macros? || check_missing_doc(source, node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def check_missing_doc(source, node, scope)
|
||||||
|
visibility = scope.visibility
|
||||||
|
|
||||||
|
return if visibility && !visibility.public?
|
||||||
|
return if node.doc.presence
|
||||||
|
|
||||||
|
issue_for(node, MSG)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue