Compare commits
149 Commits
29e29b8e1d
...
63be60ce96
Author | SHA1 | Date |
---|---|---|
Sijawusz Pur Rahnama | 63be60ce96 | |
Sijawusz Pur Rahnama | 17084f4a1d | |
Sijawusz Pur Rahnama | 590640b559 | |
Sijawusz Pur Rahnama | 3bea264948 | |
Sijawusz Pur Rahnama | 7f50ff90fd | |
Sijawusz Pur Rahnama | a79e711fae | |
Sijawusz Pur Rahnama | 28fafea19f | |
Sijawusz Pur Rahnama | f2677d68f6 | |
Vitalii Elenhaupt | b56d34715d | |
Johannes Müller | 1398c0ee8f | |
Sijawusz Pur Rahnama | 734bb2a7f1 | |
Sijawusz Pur Rahnama | 98d5bc720a | |
Sijawusz Pur Rahnama | d23ad7f0ab | |
Sijawusz Pur Rahnama | b6bd74e02f | |
Sijawusz Pur Rahnama | ce3f2b7e4b | |
Sijawusz Pur Rahnama | 444b07c179 | |
Sijawusz Pur Rahnama | e99a69765f | |
Sijawusz Pur Rahnama | 6d0b12c70f | |
Sijawusz Pur Rahnama | 65ab317a3b | |
Sijawusz Pur Rahnama | 452a7a867e | |
Sijawusz Pur Rahnama | 5a24f1eba5 | |
Sijawusz Pur Rahnama | aeffa6ad00 | |
Sijawusz Pur Rahnama | 4567293add | |
Sijawusz Pur Rahnama | a49faa33a9 | |
Sijawusz Pur Rahnama | 1dd531740c | |
Sijawusz Pur Rahnama | 1b661d633d | |
Sijawusz Pur Rahnama | 9745637cf9 | |
Sijawusz Pur Rahnama | 4ad151e5e0 | |
Sijawusz Pur Rahnama | c9bc01f88c | |
Sijawusz Pur Rahnama | 1feb5c279b | |
Sijawusz Pur Rahnama | 57898fd797 | |
Sijawusz Pur Rahnama | 46a42ee9e8 | |
Sijawusz Pur Rahnama | 61afa5bb2b | |
Sijawusz Pur Rahnama | 9bb6c9ac75 | |
Sijawusz Pur Rahnama | 954345d316 | |
Sijawusz Pur Rahnama | 55f3ec53b7 | |
Sijawusz Pur Rahnama | 26d9bc0bd0 | |
Sijawusz Pur Rahnama | 47088b10ca | |
Sijawusz Pur Rahnama | 9f9d5fae32 | |
Sijawusz Pur Rahnama | 5e70ae4f8c | |
Sijawusz Pur Rahnama | 82e0e53080 | |
Sijawusz Pur Rahnama | 1b8523def6 | |
Sijawusz Pur Rahnama | a88033c8ce | |
Sijawusz Pur Rahnama | 30e3816ed1 | |
Sijawusz Pur Rahnama | 5aac63ea74 | |
Sijawusz Pur Rahnama | 10b577d23a | |
Sijawusz Pur Rahnama | 06dc201344 | |
Sijawusz Pur Rahnama | d079f4bae6 | |
Sijawusz Pur Rahnama | 0461fff702 | |
Sijawusz Pur Rahnama | 22e2d1de00 | |
Sijawusz Pur Rahnama | 810a3440dd | |
Sijawusz Pur Rahnama | f3f1f3a2ab | |
Sijawusz Pur Rahnama | 547fec5a94 | |
Sijawusz Pur Rahnama | a8b8c35cc7 | |
Sijawusz Pur Rahnama | 11bf9ffcdc | |
Sijawusz Pur Rahnama | 52ccf23ef9 | |
Sijawusz Pur Rahnama | b3f11913ed | |
Sijawusz Pur Rahnama | 633ed7538e | |
Sijawusz Pur Rahnama | 15d241e138 | |
Sijawusz Pur Rahnama | 52a3e47a3b | |
Sijawusz Pur Rahnama | 3b87aa6490 | |
Sijawusz Pur Rahnama | 018adb54be | |
Sijawusz Pur Rahnama | be76b3682a | |
Sijawusz Pur Rahnama | 775650c882 | |
Sijawusz Pur Rahnama | 21a406e56d | |
Sijawusz Pur Rahnama | 0b225da9ba | |
Sijawusz Pur Rahnama | 0a2609c1b4 | |
Sijawusz Pur Rahnama | 06952fc7d3 | |
Sijawusz Pur Rahnama | f984d83b05 | |
Sijawusz Pur Rahnama | 98cc6fd612 | |
Sijawusz Pur Rahnama | 6caf24ad6d | |
Sijawusz Pur Rahnama | e62fffae80 | |
Sijawusz Pur Rahnama | 61ccb030bd | |
Sijawusz Pur Rahnama | 971bff6c27 | |
Sijawusz Pur Rahnama | bf4219532f | |
Sijawusz Pur Rahnama | a40f02f77f | |
Sijawusz Pur Rahnama | bee4472a26 | |
Sijawusz Pur Rahnama | 28014ada67 | |
Sijawusz Pur Rahnama | 1d76a7c71a | |
Sijawusz Pur Rahnama | 0abb73f0b6 | |
Sijawusz Pur Rahnama | fd44eeba08 | |
Sijawusz Pur Rahnama | cc23e7a7e7 | |
Sijawusz Pur Rahnama | 964d011d53 | |
Sijawusz Pur Rahnama | 3f1e925e07 | |
Sijawusz Pur Rahnama | e84cc05f0f | |
Sijawusz Pur Rahnama | 7ceb3ffad9 | |
Sijawusz Pur Rahnama | b9ce705a47 | |
Sijawusz Pur Rahnama | 881209d54e | |
Sijawusz Pur Rahnama | bcb72fb3c3 | |
Sijawusz Pur Rahnama | b25dc402c8 | |
Sijawusz Pur Rahnama | 8569355b5a | |
Sijawusz Pur Rahnama | 0c6745781e | |
Sijawusz Pur Rahnama | 891cad2610 | |
Sijawusz Pur Rahnama | 0140fd3573 | |
Sijawusz Pur Rahnama | 9f6615bdfd | |
Sijawusz Pur Rahnama | 1fccbfc8b8 | |
Sijawusz Pur Rahnama | c2b5e9449c | |
Sijawusz Pur Rahnama | d5ac394d19 | |
Sijawusz Pur Rahnama | bdbb79f1fa | |
Sijawusz Pur Rahnama | 1b342e8257 | |
Sijawusz Pur Rahnama | 23c61e04c0 | |
Sijawusz Pur Rahnama | ddb6e3c38f | |
Sijawusz Pur Rahnama | ef16ad6471 | |
Sijawusz Pur Rahnama | 1b57e2cad5 | |
Sijawusz Pur Rahnama | 3d3626accc | |
Sijawusz Pur Rahnama | bede3f97a1 | |
Sijawusz Pur Rahnama | 8ff621ba66 | |
Sijawusz Pur Rahnama | f1f21ac94d | |
Sijawusz Pur Rahnama | 1718945523 | |
Sijawusz Pur Rahnama | c9538220c6 | |
Vitalii Elenhaupt | 789e1b77e8 | |
Sijawusz Pur Rahnama | 7174e81a13 | |
Sijawusz Pur Rahnama | 29f84921b5 | |
Sijawusz Pur Rahnama | c7f3fe78aa | |
Sijawusz Pur Rahnama | 2d9db35ec4 | |
Sijawusz Pur Rahnama | dfda3d7677 | |
dependabot[bot] | 0829f70256 | |
dependabot[bot] | 53b311c5eb | |
dependabot[bot] | 867ddb4fbd | |
dependabot[bot] | 6724f9a0e0 | |
dependabot[bot] | 6389edc5fa | |
Vitalii Elenhaupt | 0ab39a025b | |
dependabot[bot] | 135ff87c7e | |
Vitalii Elenhaupt | 18d193bd08 | |
Stuart Frost | f96cb01015 | |
Stuart Frost | 1b85ba6f22 | |
Stuart Frost | eb60b25c4e | |
Stuart Frost | 7690074cab | |
Vitalii Elenhaupt | 7b8316f061 | |
Stuart Frost | b2069ea4ff | |
Stuart Frost | e85531df6c | |
Stuart Frost | 07aebfc84a | |
Vitalii Elenhaupt | 8ef588dc6d | |
Stuart Frost | 3b9c442e09 | |
Stuart Frost | 88e0437902 | |
Stuart Frost | 4741c9f4c4 | |
Stuart Frost | d9b2d69055 | |
Stuart Frost | 5f878fb40f | |
Stuart Frost | 01a943d0d6 | |
Sijawusz Pur Rahnama | 8c9d234d0b | |
Johannes Müller | efa9c9dba0 | |
Johannes Müller | 15ce5437d1 | |
Johannes Müller | eacb9308a7 | |
Sijawusz Pur Rahnama | a33f98624a | |
Sijawusz Pur Rahnama | 33c8273866 | |
Sijawusz Pur Rahnama | 327ed546b9 | |
Sijawusz Pur Rahnama | ddff8d226b | |
Sijawusz Pur Rahnama | 5cff76071a | |
Sijawusz Pur Rahnama | db59b23f9b |
|
@ -0,0 +1,7 @@
|
|||
Documentation/DocumentationAdmonition:
|
||||
Timezone: UTC
|
||||
Admonitions: [FIXME, BUG]
|
||||
|
||||
Lint/Typos:
|
||||
Excluded:
|
||||
- spec/ameba/rule/lint/typos_spec.cr
|
|
@ -24,18 +24,18 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login against a Docker registry except on PR
|
||||
# https://github.com/docker/login-action
|
||||
- name: Log into ${{ env.REGISTRY }} registry
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
# https://github.com/docker/metadata-action
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
@ -61,7 +61,7 @@ jobs:
|
|||
# https://github.com/docker/build-push-action
|
||||
- name: Build and push Docker image
|
||||
id: build-and-push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
|
|
@ -18,17 +18,24 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Set timezone to UTC
|
||||
uses: szenius/set-timezone@v1.2
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
- name: Download source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: shards install
|
||||
|
||||
- name: Install typos-cli
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: brew install typos-cli
|
||||
|
||||
- name: Run specs
|
||||
run: crystal spec
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
uses: crystal-lang/install-crystal@v1
|
||||
|
||||
- name: Download source
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: shards install
|
||||
|
|
80
Makefile
80
Makefile
|
@ -1,38 +1,92 @@
|
|||
.POSIX:
|
||||
all:
|
||||
|
||||
# Recipes
|
||||
|
||||
## Build ameba
|
||||
## $ make
|
||||
## Run tests
|
||||
## $ make test
|
||||
## Install ameba
|
||||
## $ sudo make install
|
||||
|
||||
-include Makefile.local # for optional local options
|
||||
|
||||
BUILD_TARGET ::= bin/ameba
|
||||
|
||||
DESTDIR ?= ## Install destination dir
|
||||
PREFIX ?= /usr/local## Install path prefix
|
||||
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
|
||||
|
||||
# The crystal command to use
|
||||
CRYSTAL_BIN ?= crystal
|
||||
# The shards command to use
|
||||
SHARDS_BIN ?= shards
|
||||
PREFIX ?= /usr/local
|
||||
# The install command to use
|
||||
INSTALL_BIN ?= /usr/bin/install
|
||||
|
||||
SHARD_BIN ?= ../../bin
|
||||
CRFLAGS ?= -Dpreview_mt
|
||||
|
||||
SRC_SOURCES ::= $(shell find src -name '*.cr' 2>/dev/null)
|
||||
DOC_SOURCE ::= src/**
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
build: ## Build the application binary
|
||||
build: $(BUILD_TARGET)
|
||||
|
||||
$(BUILD_TARGET): $(SRC_SOURCES)
|
||||
$(SHARDS_BIN) build $(CRFLAGS)
|
||||
|
||||
docs: ## Generate API docs
|
||||
docs: $(SRC_SOURCES)
|
||||
$(CRYSTAL_BIN) docs -o docs $(DOC_SOURCE)
|
||||
|
||||
.PHONY: lint
|
||||
lint: build
|
||||
./bin/ameba
|
||||
lint: ## Run ameba on ameba's code base
|
||||
lint: $(BUILD_TARGET)
|
||||
$(BUILD_TARGET)
|
||||
|
||||
.PHONY: spec
|
||||
spec: ## Run the spec suite
|
||||
spec:
|
||||
$(CRYSTAL_BIN) spec
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Remove application binary
|
||||
clean:
|
||||
rm -f ./bin/ameba ./bin/ameba.dwarf
|
||||
@rm -f "$(BUILD_TARGET)" "$(BUILD_TARGET).dwarf"
|
||||
|
||||
.PHONY: install
|
||||
install: build
|
||||
mkdir -p $(PREFIX)/bin
|
||||
cp ./bin/ameba $(PREFIX)/bin
|
||||
install: ## Install application binary into $DESTDIR
|
||||
install: $(BUILD_TARGET)
|
||||
$(INSTALL_BIN) -m 0755 "$(BUILD_TARGET)" "$(BINDIR)/ameba"
|
||||
|
||||
.PHONY: bin
|
||||
bin: build
|
||||
mkdir -p $(SHARD_BIN)
|
||||
cp ./bin/ameba $(SHARD_BIN)
|
||||
|
||||
.PHONY: run_file
|
||||
run_file:
|
||||
cp -n ./bin/ameba.cr $(SHARD_BIN) || true
|
||||
cp $(BUILD_TARGET) $(SHARD_BIN)
|
||||
|
||||
.PHONY: test
|
||||
test: ## Run the spec suite and linter
|
||||
test: spec lint
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show this help
|
||||
@echo
|
||||
@printf '\033[34mtargets:\033[0m\n'
|
||||
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
|
||||
sort |\
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
@printf '\033[34moptional variables:\033[0m\n'
|
||||
@grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\
|
||||
sort |\
|
||||
awk 'BEGIN {FS = " \\?=.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
|
||||
@echo
|
||||
@printf '\033[34mrecipes:\033[0m\n'
|
||||
@grep -hE '^##.*$$' $(MAKEFILE_LIST) |\
|
||||
awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf " \033[36m%s\033[0m\n", $$2}; /^## / {printf " %s\n", $$2}'
|
||||
|
|
11
README.md
11
README.md
|
@ -8,7 +8,7 @@
|
|||
</sup>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/crystal-ameba/ameba/actions?query=workflow%3ACI"><img src="https://github.com/crystal-ameba/ameba/workflows/CI/badge.svg"></a>
|
||||
<a href="https://github.com/crystal-ameba/ameba/actions/workflows/ci.yml"><img src="https://github.com/crystal-ameba/ameba/actions/workflows/ci.yml/badge.svg"></a>
|
||||
<a href="https://github.com/crystal-ameba/ameba/releases"><img src="https://img.shields.io/github/release/crystal-ameba/ameba.svg?maxAge=360"></a>
|
||||
<a href="https://github.com/crystal-ameba/ameba/blob/master/LICENSE"><img src="https://img.shields.io/github/license/crystal-ameba/ameba.svg"></a>
|
||||
</p>
|
||||
|
@ -118,7 +118,6 @@ Add this to your application's `shard.yml`:
|
|||
development_dependencies:
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.4.0
|
||||
```
|
||||
|
||||
Build `bin/ameba` binary within your project directory while running `shards install`.
|
||||
|
@ -165,7 +164,7 @@ Generate new file by running `ameba --gen-config`.
|
|||
**List of sources to run Ameba on can be configured globally via:**
|
||||
|
||||
- `Globs` section - an array of wildcards (or paths) to include to the
|
||||
inspection. Defaults to `%w(**/*.cr !lib)`, meaning it includes all project
|
||||
inspection. Defaults to `%w[**/*.cr !lib]`, meaning it includes all project
|
||||
files with `*.cr` extension except those which exist in `lib` folder.
|
||||
- `Excluded` section - an array of wildcards (or paths) to exclude from the
|
||||
source list defined by `Globs`. Defaults to an empty array.
|
||||
|
@ -186,8 +185,8 @@ Excluded:
|
|||
``` yaml
|
||||
Style/RedundantBegin:
|
||||
Excluded:
|
||||
- src/server/processor.cr
|
||||
- src/server/api.cr
|
||||
- src/server/processor.cr
|
||||
- src/server/api.cr
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
@ -240,4 +239,4 @@ time = Time.epoch(1483859302) # ameba:disable Style, Lint
|
|||
## Contributors
|
||||
|
||||
- [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer
|
||||
- [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - maintainer
|
||||
- [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - contributor, maintainer
|
||||
|
|
|
@ -15,7 +15,7 @@ Benchmark.ips do |x|
|
|||
20,
|
||||
30,
|
||||
40,
|
||||
].each do |n|
|
||||
].each do |n| # ameba:disable Naming/BlockParameterName
|
||||
config = Ameba::Config.load
|
||||
config.formatter = Ameba::Formatter::BaseFormatter.new
|
||||
config.globs = get_files(n)
|
||||
|
|
10
shard.yml
10
shard.yml
|
@ -1,20 +1,22 @@
|
|||
name: ameba
|
||||
version: 1.4.3
|
||||
version: 1.6.1
|
||||
|
||||
authors:
|
||||
- Vitalii Elenhaupt <velenhaupt@gmail.com>
|
||||
- Sijawusz Pur Rahnama <sija@sija.pl>
|
||||
|
||||
targets:
|
||||
ameba:
|
||||
main: src/cli.cr
|
||||
|
||||
scripts:
|
||||
# TODO: remove pre-compiled executable in future releases
|
||||
postinstall: make bin && make run_file
|
||||
postinstall: shards build -Dpreview_mt
|
||||
|
||||
# TODO: remove pre-compiled executable in future releases
|
||||
executables:
|
||||
- ameba
|
||||
- ameba.cr
|
||||
|
||||
crystal: "~> 1.7.0"
|
||||
crystal: ~> 1.10
|
||||
|
||||
license: MIT
|
||||
|
|
|
@ -298,6 +298,34 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
|
||||
context "Crystal::Call" do
|
||||
context "loop" do
|
||||
it "constructs a branch in block" do
|
||||
branch = branch_of_assign_in_def <<-CRYSTAL
|
||||
def method(a)
|
||||
loop do
|
||||
b = (a = 1)
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
branch.to_s.should eq "b = (a = 1)"
|
||||
end
|
||||
end
|
||||
|
||||
context "other" do
|
||||
it "skips constructing a branch in block" do
|
||||
branch = branch_of_assign_in_def <<-CRYSTAL
|
||||
def method(a)
|
||||
1.upto(10) do
|
||||
b = (a = 1)
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
branch.should be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#initialize" do
|
||||
it "creates new branch" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
|
@ -358,6 +386,30 @@ module Ameba::AST
|
|||
branch = Branch.new nodes.assign_nodes.first, branchable
|
||||
branch.in_loop?.should be_false
|
||||
end
|
||||
|
||||
context "Crystal::Call" do
|
||||
it "returns true if branch is in a loop" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
loop do
|
||||
a = 1
|
||||
end
|
||||
CRYSTAL
|
||||
branchable = Branchable.new nodes.call_nodes.first
|
||||
branch = Branch.new nodes.assign_nodes.first, branchable
|
||||
branch.in_loop?.should be_true
|
||||
end
|
||||
|
||||
it "returns false if branch is not in a loop" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
1.upto(10) do
|
||||
a = 1
|
||||
end
|
||||
CRYSTAL
|
||||
branchable = Branchable.new nodes.call_nodes.first
|
||||
branch = Branch.new nodes.assign_nodes.first, branchable
|
||||
branch.in_loop?.should be_false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,13 +57,15 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
|
||||
var_node = nodes.var_nodes.first
|
||||
scope.add_variable var_node
|
||||
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
scope.add_variable(var_node)
|
||||
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||
|
||||
variable = Variable.new(var_node, scope)
|
||||
variable.reference nodes.var_nodes.first, scope.inner_scopes.first
|
||||
variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
|
||||
|
||||
scope.references?(variable).should be_true
|
||||
end
|
||||
|
@ -77,13 +79,15 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
|
||||
var_node = nodes.var_nodes.first
|
||||
scope.add_variable var_node
|
||||
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
scope.add_variable(var_node)
|
||||
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||
|
||||
variable = Variable.new(var_node, scope)
|
||||
variable.reference nodes.var_nodes.first, scope.inner_scopes.first
|
||||
variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
|
||||
|
||||
scope.references?(variable, check_inner_scopes: false).should be_false
|
||||
end
|
||||
|
@ -98,9 +102,11 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
|
||||
var_node = nodes.var_nodes.first
|
||||
scope.add_variable var_node
|
||||
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
scope.add_variable(var_node)
|
||||
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||
|
||||
variable = Variable.new(var_node, scope)
|
||||
|
@ -120,7 +126,7 @@ module Ameba::AST
|
|||
describe "#find_variable" do
|
||||
it "returns the variable in the scope by name" do
|
||||
scope = Scope.new as_node("foo = 1")
|
||||
scope.add_variable Crystal::Var.new "foo"
|
||||
scope.add_variable(Crystal::Var.new "foo")
|
||||
scope.find_variable("foo").should_not be_nil
|
||||
end
|
||||
|
||||
|
@ -133,7 +139,7 @@ module Ameba::AST
|
|||
describe "#assign_variable" do
|
||||
it "creates a new assignment" do
|
||||
scope = Scope.new as_node("foo = 1")
|
||||
scope.add_variable Crystal::Var.new "foo"
|
||||
scope.add_variable(Crystal::Var.new "foo")
|
||||
scope.assign_variable("foo", Crystal::Var.new "foo")
|
||||
var = scope.find_variable("foo").should_not be_nil
|
||||
var.assignments.size.should eq 1
|
||||
|
@ -141,7 +147,7 @@ module Ameba::AST
|
|||
|
||||
it "does not create the assignment if variable is wrong" do
|
||||
scope = Scope.new as_node("foo = 1")
|
||||
scope.add_variable Crystal::Var.new "foo"
|
||||
scope.add_variable(Crystal::Var.new "foo")
|
||||
scope.assign_variable("bar", Crystal::Var.new "bar")
|
||||
var = scope.find_variable("foo").should_not be_nil
|
||||
var.assignments.size.should eq 0
|
||||
|
@ -174,6 +180,28 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
|
||||
describe "#def?" do
|
||||
context "when check_outer_scopes: true" do
|
||||
it "returns true if outer scope is Crystal::Def" do
|
||||
nodes = as_nodes("def foo; 3.times {}; end")
|
||||
outer_scope = Scope.new nodes.def_nodes.first
|
||||
scope = Scope.new nodes.block_nodes.first, outer_scope
|
||||
scope.def?(check_outer_scopes: true).should be_true
|
||||
end
|
||||
end
|
||||
|
||||
it "returns true if Crystal::Def" do
|
||||
nodes = as_nodes("def foo; end")
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
scope.def?.should be_true
|
||||
end
|
||||
|
||||
it "returns false otherwise" do
|
||||
scope = Scope.new as_node("a = 1")
|
||||
scope.def?.should be_false
|
||||
end
|
||||
end
|
||||
|
||||
describe "#in_macro?" do
|
||||
it "returns true if Crystal::Macro" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
|
|
|
@ -36,6 +36,43 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
|
||||
describe "#static/dynamic_literal?" do
|
||||
[
|
||||
Crystal::ArrayLiteral.new,
|
||||
Crystal::ArrayLiteral.new([Crystal::StringLiteral.new("foo")] of Crystal::ASTNode),
|
||||
Crystal::BoolLiteral.new(false),
|
||||
Crystal::CharLiteral.new('a'),
|
||||
Crystal::HashLiteral.new,
|
||||
Crystal::NamedTupleLiteral.new,
|
||||
Crystal::NilLiteral.new,
|
||||
Crystal::NumberLiteral.new(42),
|
||||
Crystal::RegexLiteral.new(Crystal::StringLiteral.new("")),
|
||||
Crystal::StringLiteral.new("foo"),
|
||||
Crystal::SymbolLiteral.new("foo"),
|
||||
Crystal::TupleLiteral.new([] of Crystal::ASTNode),
|
||||
Crystal::TupleLiteral.new([Crystal::StringLiteral.new("foo")] of Crystal::ASTNode),
|
||||
Crystal::RangeLiteral.new(
|
||||
Crystal::NumberLiteral.new(0),
|
||||
Crystal::NumberLiteral.new(10),
|
||||
true),
|
||||
].each do |literal|
|
||||
it "properly identifies static node #{literal}" do
|
||||
subject.static_literal?(literal).should be_true
|
||||
subject.dynamic_literal?(literal).should be_false
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
Crystal::ArrayLiteral.new([Crystal::Path.new(%w[IO])] of Crystal::ASTNode),
|
||||
Crystal::TupleLiteral.new([Crystal::Path.new(%w[IO])] of Crystal::ASTNode),
|
||||
].each do |literal|
|
||||
it "properly identifies dynamic node #{literal}" do
|
||||
subject.dynamic_literal?(literal).should be_true
|
||||
subject.static_literal?(literal).should be_false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#node_source" do
|
||||
it "returns original source of the node" do
|
||||
s = <<-CRYSTAL
|
||||
|
|
|
@ -85,30 +85,5 @@ module Ameba::AST
|
|||
assignment.branch.should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "#transformed?" do
|
||||
it "returns false if the assignment is not transformed by the compiler" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
def method(a)
|
||||
a = 2
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
variable = Variable.new(nodes.var_nodes.first, scope)
|
||||
assignment = Assignment.new(nodes.assign_nodes.first, variable, scope)
|
||||
assignment.transformed?.should be_false
|
||||
end
|
||||
|
||||
it "returns true if the assignment is transformed by the compiler" do
|
||||
nodes = as_nodes <<-CRYSTAL
|
||||
array.each do |(a, b)|
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.block_nodes.first
|
||||
variable = Variable.new(nodes.var_nodes.first, scope)
|
||||
assignment = Assignment.new(nodes.assign_nodes.first, variable, scope)
|
||||
assignment.transformed?.should be_true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,13 +85,16 @@ module Ameba::AST
|
|||
3.times { |i| a = a + i }
|
||||
end
|
||||
CRYSTAL
|
||||
scope = Scope.new nodes.def_nodes.first
|
||||
|
||||
var_node = nodes.var_nodes.first
|
||||
scope.add_variable var_node
|
||||
|
||||
scope = Scope.new(nodes.def_nodes.first)
|
||||
scope.add_variable(var_node)
|
||||
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||
|
||||
variable = Variable.new(var_node, scope)
|
||||
variable.reference nodes.var_nodes.last, scope.inner_scopes.last
|
||||
variable.reference(nodes.var_nodes.last, scope.inner_scopes.last)
|
||||
|
||||
variable.captured_by_block?.should be_truthy
|
||||
end
|
||||
|
||||
|
@ -101,8 +104,10 @@ module Ameba::AST
|
|||
a = 1
|
||||
end
|
||||
CRYSTAL
|
||||
scope.add_variable Crystal::Var.new "a"
|
||||
|
||||
scope.add_variable(Crystal::Var.new "a")
|
||||
variable = scope.variables.first
|
||||
|
||||
variable.captured_by_block?.should be_falsey
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::AST
|
||||
source = Source.new ""
|
||||
|
||||
describe FlowExpressionVisitor do
|
||||
it "creates an expression for return" do
|
||||
rule = FlowExpressionRule.new
|
||||
|
|
|
@ -10,14 +10,16 @@ module Ameba::Rule
|
|||
end
|
||||
|
||||
it "contains rules across all the available groups" do
|
||||
Rule.rules.map(&.group_name).uniq!.reject!(&.empty?).sort.should eq %w(
|
||||
Rule.rules.map(&.group_name).uniq!.reject!(&.empty?).sort.should eq %w[
|
||||
Ameba
|
||||
Documentation
|
||||
Layout
|
||||
Lint
|
||||
Metrics
|
||||
Naming
|
||||
Performance
|
||||
Style
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -48,25 +50,25 @@ module Ameba::Rule
|
|||
|
||||
it "returns false if source is not excluded from this rule" do
|
||||
rule = DummyRule.new
|
||||
rule.excluded = %w(some_source.cr)
|
||||
rule.excluded = %w[some_source.cr]
|
||||
rule.excluded?(Source.new "", "another_source.cr").should_not be_true
|
||||
end
|
||||
|
||||
it "returns true if source is excluded from this rule" do
|
||||
rule = DummyRule.new
|
||||
rule.excluded = %w(source.cr)
|
||||
rule.excluded = %w[source.cr]
|
||||
rule.excluded?(Source.new "", "source.cr").should be_true
|
||||
end
|
||||
|
||||
it "returns true if source matches the wildcard" do
|
||||
rule = DummyRule.new
|
||||
rule.excluded = %w(**/*.cr)
|
||||
rule.excluded = %w[**/*.cr]
|
||||
rule.excluded?(Source.new "", __FILE__).should be_true
|
||||
end
|
||||
|
||||
it "returns false if source does not match the wildcard" do
|
||||
rule = DummyRule.new
|
||||
rule.excluded = %w(*_spec.cr)
|
||||
rule.excluded = %w[*_spec.cr]
|
||||
rule.excluded?(Source.new "", "source.cr").should be_false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,97 +5,97 @@ module Ameba::Cli
|
|||
describe "Cmd" do
|
||||
describe ".run" do
|
||||
it "runs ameba" do
|
||||
r = Cli.run %w(-f silent file.cr)
|
||||
r = Cli.run %w[-f silent file.cr]
|
||||
r.should be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe ".parse_args" do
|
||||
%w(-s --silent).each do |f|
|
||||
it "accepts #{f} flag" do
|
||||
c = Cli.parse_args [f]
|
||||
%w[-s --silent].each do |flag|
|
||||
it "accepts #{flag} flag" do
|
||||
c = Cli.parse_args [flag]
|
||||
c.formatter.should eq :silent
|
||||
end
|
||||
end
|
||||
|
||||
%w(-c --config).each do |f|
|
||||
it "accepts #{f} flag" do
|
||||
c = Cli.parse_args [f, "config.yml"]
|
||||
%w[-c --config].each do |flag|
|
||||
it "accepts #{flag} flag" do
|
||||
c = Cli.parse_args [flag, "config.yml"]
|
||||
c.config.should eq Path["config.yml"]
|
||||
end
|
||||
end
|
||||
|
||||
%w(-f --format).each do |f|
|
||||
it "accepts #{f} flag" do
|
||||
c = Cli.parse_args [f, "my-formatter"]
|
||||
%w[-f --format].each do |flag|
|
||||
it "accepts #{flag} flag" do
|
||||
c = Cli.parse_args [flag, "my-formatter"]
|
||||
c.formatter.should eq "my-formatter"
|
||||
end
|
||||
end
|
||||
|
||||
it "accepts --only flag" do
|
||||
c = Cli.parse_args ["--only", "RULE1,RULE2"]
|
||||
c.only.should eq %w(RULE1 RULE2)
|
||||
c.only.should eq %w[RULE1 RULE2]
|
||||
end
|
||||
|
||||
it "accepts --except flag" do
|
||||
c = Cli.parse_args ["--except", "RULE1,RULE2"]
|
||||
c.except.should eq %w(RULE1 RULE2)
|
||||
c.except.should eq %w[RULE1 RULE2]
|
||||
end
|
||||
|
||||
it "defaults rules? flag to false" do
|
||||
c = Cli.parse_args %w(file.cr)
|
||||
c = Cli.parse_args %w[file.cr]
|
||||
c.rules?.should be_false
|
||||
end
|
||||
|
||||
it "defaults skip_reading_config? flag to false" do
|
||||
c = Cli.parse_args %w(file.cr)
|
||||
c = Cli.parse_args %w[file.cr]
|
||||
c.skip_reading_config?.should be_false
|
||||
end
|
||||
|
||||
it "accepts --rules flag" do
|
||||
c = Cli.parse_args %w(--rules)
|
||||
c = Cli.parse_args %w[--rules]
|
||||
c.rules?.should eq true
|
||||
end
|
||||
|
||||
it "defaults all? flag to false" do
|
||||
c = Cli.parse_args %w(file.cr)
|
||||
c = Cli.parse_args %w[file.cr]
|
||||
c.all?.should be_false
|
||||
end
|
||||
|
||||
it "accepts --all flag" do
|
||||
c = Cli.parse_args %w(--all)
|
||||
c = Cli.parse_args %w[--all]
|
||||
c.all?.should eq true
|
||||
end
|
||||
|
||||
it "accepts --gen-config flag" do
|
||||
c = Cli.parse_args %w(--gen-config)
|
||||
c = Cli.parse_args %w[--gen-config]
|
||||
c.formatter.should eq :todo
|
||||
end
|
||||
|
||||
it "accepts --no-color flag" do
|
||||
c = Cli.parse_args %w(--no-color)
|
||||
c = Cli.parse_args %w[--no-color]
|
||||
c.colors?.should be_false
|
||||
end
|
||||
|
||||
it "accepts --without-affected-code flag" do
|
||||
c = Cli.parse_args %w(--without-affected-code)
|
||||
c = Cli.parse_args %w[--without-affected-code]
|
||||
c.without_affected_code?.should be_true
|
||||
end
|
||||
|
||||
it "doesn't disable colors by default" do
|
||||
c = Cli.parse_args %w(--all)
|
||||
c = Cli.parse_args %w[--all]
|
||||
c.colors?.should be_true
|
||||
end
|
||||
|
||||
it "ignores --config if --gen-config flag passed" do
|
||||
c = Cli.parse_args %w(--gen-config --config my_config.yml)
|
||||
c = Cli.parse_args %w[--gen-config --config my_config.yml]
|
||||
c.formatter.should eq :todo
|
||||
c.skip_reading_config?.should be_true
|
||||
end
|
||||
|
||||
describe "-e/--explain" do
|
||||
it "configures file/line/column" do
|
||||
c = Cli.parse_args %w(--explain src/file.cr:3:5)
|
||||
c = Cli.parse_args %w[--explain src/file.cr:3:5]
|
||||
|
||||
location_to_explain = c.location_to_explain.should_not be_nil
|
||||
location_to_explain[:file].should eq "src/file.cr"
|
||||
|
@ -105,59 +105,59 @@ module Ameba::Cli
|
|||
|
||||
it "raises an error if location is not valid" do
|
||||
expect_raises(Exception, "location should have PATH:line:column") do
|
||||
Cli.parse_args %w(--explain src/file.cr:3)
|
||||
Cli.parse_args %w[--explain src/file.cr:3]
|
||||
end
|
||||
end
|
||||
|
||||
it "raises an error if line number is not valid" do
|
||||
expect_raises(Exception, "location should have PATH:line:column") do
|
||||
Cli.parse_args %w(--explain src/file.cr:a:3)
|
||||
Cli.parse_args %w[--explain src/file.cr:a:3]
|
||||
end
|
||||
end
|
||||
|
||||
it "raises an error if column number is not valid" do
|
||||
expect_raises(Exception, "location should have PATH:line:column") do
|
||||
Cli.parse_args %w(--explain src/file.cr:3:&)
|
||||
Cli.parse_args %w[--explain src/file.cr:3:&]
|
||||
end
|
||||
end
|
||||
|
||||
it "raises an error if line/column are missing" do
|
||||
expect_raises(Exception, "location should have PATH:line:column") do
|
||||
Cli.parse_args %w(--explain src/file.cr)
|
||||
Cli.parse_args %w[--explain src/file.cr]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "--fail-level" do
|
||||
it "configures fail level Convention" do
|
||||
c = Cli.parse_args %w(--fail-level convention)
|
||||
c = Cli.parse_args %w[--fail-level convention]
|
||||
c.fail_level.should eq Severity::Convention
|
||||
end
|
||||
|
||||
it "configures fail level Warning" do
|
||||
c = Cli.parse_args %w(--fail-level Warning)
|
||||
c = Cli.parse_args %w[--fail-level Warning]
|
||||
c.fail_level.should eq Severity::Warning
|
||||
end
|
||||
|
||||
it "configures fail level Error" do
|
||||
c = Cli.parse_args %w(--fail-level error)
|
||||
c = Cli.parse_args %w[--fail-level error]
|
||||
c.fail_level.should eq Severity::Error
|
||||
end
|
||||
|
||||
it "raises if fail level is incorrect" do
|
||||
expect_raises(Exception, "Incorrect severity name JohnDoe") do
|
||||
Cli.parse_args %w(--fail-level JohnDoe)
|
||||
Cli.parse_args %w[--fail-level JohnDoe]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "accepts unknown args as globs" do
|
||||
c = Cli.parse_args %w(source1.cr source2.cr)
|
||||
c.globs.should eq %w(source1.cr source2.cr)
|
||||
c = Cli.parse_args %w[source1.cr source2.cr]
|
||||
c.globs.should eq %w[source1.cr source2.cr]
|
||||
end
|
||||
|
||||
it "accepts one unknown arg as explain location if it has correct format" do
|
||||
c = Cli.parse_args %w(source.cr:3:22)
|
||||
c = Cli.parse_args %w[source.cr:3:22]
|
||||
|
||||
location_to_explain = c.location_to_explain.should_not be_nil
|
||||
location_to_explain[:file].should eq "source.cr"
|
||||
|
|
|
@ -2,7 +2,7 @@ require "../spec_helper"
|
|||
|
||||
module Ameba
|
||||
describe Config do
|
||||
config_sample = "config/ameba.yml"
|
||||
config_sample = "spec/fixtures/config.yml"
|
||||
|
||||
it "should have a list of available formatters" do
|
||||
Config::AVAILABLE_FORMATTERS.should_not be_nil
|
||||
|
@ -21,7 +21,7 @@ module Ameba
|
|||
Globs: src/*.cr
|
||||
CONFIG
|
||||
config = Config.new(yml)
|
||||
config.globs.should eq %w(src/*.cr)
|
||||
config.globs.should eq %w[src/*.cr]
|
||||
end
|
||||
|
||||
it "initializes globs as array" do
|
||||
|
@ -32,7 +32,7 @@ module Ameba
|
|||
- "!spec"
|
||||
CONFIG
|
||||
config = Config.new(yml)
|
||||
config.globs.should eq %w(src/*.cr !spec)
|
||||
config.globs.should eq %w[src/*.cr !spec]
|
||||
end
|
||||
|
||||
it "raises if Globs has a wrong type" do
|
||||
|
@ -51,7 +51,7 @@ module Ameba
|
|||
Excluded: spec
|
||||
CONFIG
|
||||
config = Config.new(yml)
|
||||
config.excluded.should eq %w(spec)
|
||||
config.excluded.should eq %w[spec]
|
||||
end
|
||||
|
||||
it "initializes excluded as array" do
|
||||
|
@ -62,7 +62,7 @@ module Ameba
|
|||
- lib/*.cr
|
||||
CONFIG
|
||||
config = Config.new(yml)
|
||||
config.excluded.should eq %w(spec lib/*.cr)
|
||||
config.excluded.should eq %w[spec lib/*.cr]
|
||||
end
|
||||
|
||||
it "raises if Excluded has a wrong type" do
|
||||
|
@ -84,6 +84,12 @@ module Ameba
|
|||
config.formatter.should_not be_nil
|
||||
end
|
||||
|
||||
it "raises when custom config file doesn't exist" do
|
||||
expect_raises(Exception, "Unable to load config file: Config file does not exist") do
|
||||
Config.load "foo.yml"
|
||||
end
|
||||
end
|
||||
|
||||
it "loads default config" do
|
||||
config = Config.load
|
||||
config.should_not be_nil
|
||||
|
@ -128,12 +134,12 @@ module Ameba
|
|||
end
|
||||
|
||||
it "returns a list of sources matching globs" do
|
||||
config.globs = %w(**/config_spec.cr)
|
||||
config.globs = %w[**/config_spec.cr]
|
||||
config.sources.size.should eq(1)
|
||||
end
|
||||
|
||||
it "returns a list of sources excluding 'Excluded'" do
|
||||
config.excluded = %w(**/config_spec.cr)
|
||||
config.excluded = %w[**/config_spec.cr]
|
||||
config.sources.any?(&.fullpath.==(__FILE__)).should be_false
|
||||
end
|
||||
end
|
||||
|
@ -175,7 +181,7 @@ module Ameba
|
|||
|
||||
it "updates excluded property" do
|
||||
name = DummyRule.rule_name
|
||||
excluded = %w(spec/source.cr)
|
||||
excluded = %w[spec/source.cr]
|
||||
config.update_rule name, excluded: excluded
|
||||
rule = config.rules.find!(&.name.== name)
|
||||
rule.excluded.should eq excluded
|
||||
|
@ -194,7 +200,7 @@ module Ameba
|
|||
|
||||
it "updates multiple rules by excluded property" do
|
||||
name = DummyRule.rule_name
|
||||
excluded = %w(spec/source.cr)
|
||||
excluded = %w[spec/source.cr]
|
||||
config.update_rules [name], excluded: excluded
|
||||
rule = config.rules.find!(&.name.== name)
|
||||
rule.excluded.should eq excluded
|
||||
|
@ -209,7 +215,7 @@ module Ameba
|
|||
|
||||
it "updates a group by excluded property" do
|
||||
name = DummyRule.group_name
|
||||
excluded = %w(spec/source.cr)
|
||||
excluded = %w[spec/source.cr]
|
||||
config.update_rules [name], excluded: excluded
|
||||
rule = config.rules.find!(&.name.== DummyRule.rule_name)
|
||||
rule.excluded.should eq excluded
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
require "../../spec_helper"
|
||||
require "file_utils"
|
||||
|
||||
CONFIG_PATH = Path[Dir.tempdir] / Ameba::Config::FILENAME
|
||||
|
||||
module Ameba
|
||||
private def with_formatter(&)
|
||||
io = IO::Memory.new
|
||||
formatter = Formatter::TODOFormatter.new(io)
|
||||
formatter = Formatter::TODOFormatter.new(io, CONFIG_PATH)
|
||||
|
||||
yield formatter, io
|
||||
end
|
||||
|
@ -20,7 +22,7 @@ module Ameba
|
|||
|
||||
describe Formatter::TODOFormatter do
|
||||
::Spec.after_each do
|
||||
FileUtils.rm_rf(Ameba::Config::DEFAULT_PATH)
|
||||
FileUtils.rm_rf(CONFIG_PATH)
|
||||
end
|
||||
|
||||
context "problems not found" do
|
||||
|
@ -45,7 +47,7 @@ module Ameba
|
|||
s = Source.new "a = 1", "source.cr"
|
||||
s.add_issue DummyRule.new, {1, 2}, "message"
|
||||
formatter.finished([s])
|
||||
io.to_s.should contain "Created #{Config::DEFAULT_PATH}"
|
||||
io.to_s.should contain "Created #{CONFIG_PATH}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
require "../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
private def with_rule_collection_presenter(&)
|
||||
with_presenter(Presenter::RuleCollectionPresenter) do |presenter, io|
|
||||
rules = Config.load.rules
|
||||
presenter.run(rules)
|
||||
|
||||
output = io.to_s
|
||||
output = Formatter::Util.deansify(output).to_s
|
||||
|
||||
yield rules, output, presenter
|
||||
end
|
||||
end
|
||||
|
||||
describe Presenter::RuleCollectionPresenter do
|
||||
it "outputs rule collection details" do
|
||||
with_rule_collection_presenter do |rules, output|
|
||||
rules.each do |rule|
|
||||
output.should contain rule.name
|
||||
output.should contain rule.severity.symbol
|
||||
|
||||
if description = rule.description
|
||||
output.should contain description
|
||||
end
|
||||
end
|
||||
output.should contain "Total rules: #{rules.size}"
|
||||
output.should match /\d+ enabled/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
require "../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
private def rule_presenter_each_rule(&)
|
||||
with_presenter(Presenter::RulePresenter) do |presenter, io|
|
||||
rules = Config.load.rules
|
||||
rules.each do |rule|
|
||||
presenter.run(rule)
|
||||
|
||||
output = io.to_s
|
||||
output = Formatter::Util.deansify(output).to_s
|
||||
|
||||
yield rule, output, presenter
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Presenter::RulePresenter do
|
||||
it "outputs rule details" do
|
||||
rule_presenter_each_rule do |rule, output|
|
||||
output.should contain rule.name
|
||||
output.should contain rule.severity.to_s
|
||||
|
||||
if description = rule.description
|
||||
output.should contain description
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,113 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Documentation
|
||||
subject = DocumentationAdmonition.new
|
||||
|
||||
describe DocumentationAdmonition do
|
||||
it "passes for comments with admonition mid-word/sentence" do
|
||||
subject.admonitions.each do |admonition|
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
# Mentioning #{admonition} mid-sentence
|
||||
# x#{admonition}x
|
||||
# x#{admonition}
|
||||
# #{admonition}x
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails for comments with admonition" do
|
||||
subject.admonitions.each do |admonition|
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}: Single-line comment
|
||||
# ^{} error: Found a #{admonition} admonition in a comment
|
||||
CRYSTAL
|
||||
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# Text before ...
|
||||
# #{admonition}(some context): Part of multi-line comment
|
||||
# ^{} error: Found a #{admonition} admonition in a comment
|
||||
# Text after ...
|
||||
CRYSTAL
|
||||
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}
|
||||
# ^{} error: Found a #{admonition} admonition in a comment
|
||||
if rand > 0.5
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
context "with date" do
|
||||
it "passes for admonitions with future date" do
|
||||
subject.admonitions.each do |admonition|
|
||||
future_date = (Time.utc + 21.days).to_s(format: "%F")
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
# #{admonition}(#{future_date}): sth in the future
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails for admonitions with past date" do
|
||||
subject.admonitions.each do |admonition|
|
||||
past_date = (Time.utc - 21.days).to_s(format: "%F")
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}(#{past_date}): sth in the past
|
||||
# ^{} error: Found a #{admonition} admonition in a comment (21 days past)
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails for admonitions with yesterday's date" do
|
||||
subject.admonitions.each do |admonition|
|
||||
yesterday_date = (Time.utc - 1.day).to_s(format: "%F")
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}(#{yesterday_date}): sth in the past
|
||||
# ^{} error: Found a #{admonition} admonition in a comment (1 day past)
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails for admonitions with today's date" do
|
||||
subject.admonitions.each do |admonition|
|
||||
today_date = Time.utc.to_s(format: "%F")
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}(#{today_date}): sth in the past
|
||||
# ^{} error: Found a #{admonition} admonition in a comment (today is the day!)
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails for admonitions with invalid date" do
|
||||
subject.admonitions.each do |admonition|
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
# #{admonition}(0000-00-00): sth wrong
|
||||
# ^{} error: #{admonition} admonition error: Invalid time: "0000-00-00"
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "properties" do
|
||||
describe "#admonitions" do
|
||||
it "lets setting custom admonitions" do
|
||||
rule = DocumentationAdmonition.new
|
||||
rule.admonitions = %w[FOO BAR]
|
||||
|
||||
rule.admonitions.each do |admonition|
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
# #{admonition}
|
||||
# ^{} error: Found a #{admonition} admonition in a comment
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
subject.admonitions.each do |admonition|
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
# #{admonition}
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Lint
|
||||
module Ameba::Rule::Documentation
|
||||
subject = Documentation.new
|
||||
.tap(&.ignore_classes = false)
|
||||
.tap(&.ignore_modules = false)
|
|
@ -4,16 +4,16 @@ module Ameba
|
|||
subject = Rule::Lint::EmptyExpression.new
|
||||
|
||||
private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__)
|
||||
it %(detects empty expression "#{code}"), file, line do
|
||||
s = Source.new code
|
||||
it "detects empty expression #{code.inspect}", file, line do
|
||||
source = Source.new code
|
||||
rule = Rule::Lint::EmptyExpression.new
|
||||
rule.catch(s).should_not be_valid, file: file, line: line
|
||||
rule.catch(source).should_not be_valid, file: file, line: line
|
||||
end
|
||||
end
|
||||
|
||||
describe Rule::Lint::EmptyExpression do
|
||||
it "passes if there is no empty expression" do
|
||||
s = Source.new <<-CRYSTAL
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def method()
|
||||
end
|
||||
|
||||
|
@ -31,7 +31,6 @@ module Ameba
|
|||
begin "" end
|
||||
[nil] << nil
|
||||
CRYSTAL
|
||||
subject.catch(s).should be_valid
|
||||
end
|
||||
|
||||
it_detects_empty_expression %(())
|
||||
|
@ -91,10 +90,10 @@ module Ameba
|
|||
)
|
||||
|
||||
it "does not report empty expression in macro" do
|
||||
s = Source.new %q(
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
module MyModule
|
||||
macro conditional_error_for_inline_callbacks
|
||||
\{%
|
||||
\\{%
|
||||
raise ""
|
||||
%}
|
||||
end
|
||||
|
@ -102,8 +101,7 @@ module Ameba
|
|||
macro before_save(x = nil)
|
||||
end
|
||||
end
|
||||
)
|
||||
subject.catch(s).should be_valid
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,8 +6,12 @@ module Ameba::Rule::Lint
|
|||
describe LiteralsComparison do
|
||||
it "passes for valid cases" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
{start.year, start.month} == {stop.year, stop.month}
|
||||
["foo"] === [foo]
|
||||
"foo" == foo
|
||||
"foo" != foo
|
||||
"foo" == FOO
|
||||
FOO == "foo"
|
||||
foo == "foo"
|
||||
foo != "foo"
|
||||
CRYSTAL
|
||||
|
@ -15,8 +19,8 @@ module Ameba::Rule::Lint
|
|||
|
||||
it "reports if there is a dynamic comparison possibly evaluating to the same" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
[foo] === ["foo"]
|
||||
# ^^^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
|
||||
[foo] === [foo]
|
||||
# ^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ module Ameba::Rule::Lint
|
|||
(1..3).index { |i| i > 2 }.not_nil!(:foo)
|
||||
(1..3).rindex { |i| i > 2 }.not_nil!(:foo)
|
||||
(1..3).find { |i| i > 2 }.not_nil!(:foo)
|
||||
/(.)(.)(.)/.match("abc", &.itself).not_nil!
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
|
@ -36,6 +37,17 @@ module Ameba::Rule::Lint
|
|||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports if there is an `match` call followed by `not_nil!`" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
/(.)(.)(.)/.match("abc").not_nil![2]
|
||||
# ^^^^^^^^^^^^^^^^^^^^^ error: Use `match! {...}` instead of `match {...}.not_nil!`
|
||||
CRYSTAL
|
||||
|
||||
expect_correction source, <<-CRYSTAL
|
||||
/(.)(.)(.)/.match!("abc")[2]
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports if there is an `index` call with block followed by `not_nil!`" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
(1..3).index { |i| i > 2 }.not_nil!
|
||||
|
|
|
@ -6,41 +6,41 @@ module Ameba::Rule::Lint
|
|||
|
||||
it "passes if percent arrays are written correctly" do
|
||||
s = Source.new %q(
|
||||
%i(one two three)
|
||||
%w(one two three)
|
||||
%i[one two three]
|
||||
%w[one two three]
|
||||
|
||||
%i(1 2 3)
|
||||
%w(1 2 3)
|
||||
%i[1 2 3]
|
||||
%w[1 2 3]
|
||||
|
||||
%i()
|
||||
%w()
|
||||
%i[]
|
||||
%w[]
|
||||
)
|
||||
subject.catch(s).should be_valid
|
||||
end
|
||||
|
||||
it "fails if string percent array has commas" do
|
||||
s = Source.new %( %w(one, two) )
|
||||
s = Source.new %( %w[one, two] )
|
||||
subject.catch(s).should_not be_valid
|
||||
end
|
||||
|
||||
it "fails if string percent array has quotes" do
|
||||
s = Source.new %( %w("one" "two") )
|
||||
s = Source.new %( %w["one" "two"] )
|
||||
subject.catch(s).should_not be_valid
|
||||
end
|
||||
|
||||
it "fails if symbols percent array has commas" do
|
||||
s = Source.new %( %i(one, two) )
|
||||
s = Source.new %( %i[one, two] )
|
||||
subject.catch(s).should_not be_valid
|
||||
end
|
||||
|
||||
it "fails if symbols percent array has a colon" do
|
||||
s = Source.new %( %i(:one :two) )
|
||||
s = Source.new %( %i[:one :two] )
|
||||
subject.catch(s).should_not be_valid
|
||||
end
|
||||
|
||||
it "reports rule, location and message for %i" do
|
||||
s = Source.new %(
|
||||
%i(:one)
|
||||
%i[:one]
|
||||
), "source.cr"
|
||||
|
||||
subject.catch(s).should_not be_valid
|
||||
|
@ -54,7 +54,7 @@ module Ameba::Rule::Lint
|
|||
|
||||
it "reports rule, location and message for %w" do
|
||||
s = Source.new %(
|
||||
%w("one")
|
||||
%w["one"]
|
||||
), "source.cr"
|
||||
|
||||
subject.catch(s).should_not be_valid
|
||||
|
@ -71,14 +71,14 @@ module Ameba::Rule::Lint
|
|||
it "#string_array_unwanted_symbols" do
|
||||
rule = PercentArrays.new
|
||||
rule.string_array_unwanted_symbols = ","
|
||||
s = Source.new %( %w("one") )
|
||||
s = Source.new %( %w["one"] )
|
||||
rule.catch(s).should be_valid
|
||||
end
|
||||
|
||||
it "#symbol_array_unwanted_symbols" do
|
||||
rule = PercentArrays.new
|
||||
rule.symbol_array_unwanted_symbols = ","
|
||||
s = Source.new %( %i(:one) )
|
||||
s = Source.new %( %i[:one] )
|
||||
rule.catch(s).should be_valid
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,6 +31,30 @@ module Ameba::Rule::Lint
|
|||
CRYSTAL
|
||||
end
|
||||
|
||||
pending "reports if there is a shadowing in an unpacked variable in a block" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def some_method
|
||||
foo = 1
|
||||
|
||||
[{3}].each do |(foo)|
|
||||
# ^ error: Shadowing outer local variable `foo`
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
pending "reports if there is a shadowing in an unpacked variable in a block (2)" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def some_method
|
||||
foo = 1
|
||||
|
||||
[{[3]}].each do |((foo))|
|
||||
# ^ error: Shadowing outer local variable `foo`
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "does not report outer vars declared below shadowed block" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
methods = klass.methods.select { |m| m.annotation(MyAnn) }
|
||||
|
@ -44,7 +68,7 @@ module Ameba::Rule::Lint
|
|||
foo = 1
|
||||
|
||||
-> (foo : Int32) {}
|
||||
# ^ error: Shadowing outer local variable `foo`
|
||||
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
@ -69,7 +93,7 @@ module Ameba::Rule::Lint
|
|||
3.times do |foo|
|
||||
# ^ error: Shadowing outer local variable `foo`
|
||||
-> (foo : Int32) { foo + 1 }
|
||||
# ^ error: Shadowing outer local variable `foo`
|
||||
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module Ameba::Rule::Lint
|
|||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports if there is a shared var in spawn" do
|
||||
it "reports if there is a shared var in spawn (while)" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
i = 0
|
||||
while i < 10
|
||||
|
@ -56,6 +56,24 @@ module Ameba::Rule::Lint
|
|||
expect_no_corrections source
|
||||
end
|
||||
|
||||
it "reports if there is a shared var in spawn (loop)" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
i = 0
|
||||
loop do
|
||||
break if i >= 10
|
||||
spawn do
|
||||
puts(i)
|
||||
# ^ error: Shared variable `i` is used in fiber
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
Fiber.yield
|
||||
CRYSTAL
|
||||
|
||||
expect_no_corrections source
|
||||
end
|
||||
|
||||
it "reports reassigned reference to shared var in spawn" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
channel = Channel(String).new
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Lint
|
||||
subject = SpecFilename.new
|
||||
|
||||
describe SpecFilename do
|
||||
it "passes if filename is correct" do
|
||||
expect_no_issues subject, code: "", path: "spec/foo_spec.cr"
|
||||
expect_no_issues subject, code: "", path: "spec/foo/bar_spec.cr"
|
||||
end
|
||||
|
||||
it "fails if filename is wrong" do
|
||||
expect_issue subject, <<-CRYSTAL, path: "spec/foo.cr"
|
||||
|
||||
# ^{} error: Spec filename should have `_spec` suffix: foo_spec.cr, not foo.cr
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
context "properties" do
|
||||
context "#ignored_dirs" do
|
||||
it "provide sane defaults" do
|
||||
expect_no_issues subject, code: "", path: "spec/support/foo.cr"
|
||||
expect_no_issues subject, code: "", path: "spec/fixtures/foo.cr"
|
||||
expect_no_issues subject, code: "", path: "spec/data/foo.cr"
|
||||
end
|
||||
end
|
||||
|
||||
context "#ignored_filenames" do
|
||||
it "ignores spec_helper by default" do
|
||||
expect_no_issues subject, code: "", path: "spec/spec_helper.cr"
|
||||
expect_no_issues subject, code: "", path: "spec/foo/spec_helper.cr"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -115,12 +115,12 @@ module Ameba::Rule::Lint
|
|||
|
||||
first.rule.should_not be_nil
|
||||
first.location.to_s.should eq "source_spec.cr:1:11"
|
||||
first.end_location.to_s.should eq ""
|
||||
first.end_location.to_s.should eq "source_spec.cr:1:21"
|
||||
first.message.should eq "Focused spec item detected"
|
||||
|
||||
second.rule.should_not be_nil
|
||||
second.location.to_s.should eq "source_spec.cr:2:13"
|
||||
second.end_location.to_s.should eq ""
|
||||
second.end_location.to_s.should eq "source_spec.cr:2:23"
|
||||
second.message.should eq "Focused spec item detected"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
private def check_typos_bin!
|
||||
unless Ameba::Rule::Lint::Typos::BIN_PATH
|
||||
pending! "`typos` executable is not available"
|
||||
end
|
||||
end
|
||||
|
||||
module Ameba::Rule::Lint
|
||||
subject = Typos.new
|
||||
.tap(&.fail_on_error = true)
|
||||
|
||||
describe Typos do
|
||||
it "reports typos" do
|
||||
check_typos_bin!
|
||||
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
# method with no arugments
|
||||
# ^^^^^^^^^ error: Typo found: arugments -> arguments
|
||||
def tpos
|
||||
# ^^^^ error: Typo found: tpos -> typos
|
||||
:otput
|
||||
# ^^^^^ error: Typo found: otput -> output
|
||||
end
|
||||
CRYSTAL
|
||||
|
||||
expect_correction source, <<-CRYSTAL
|
||||
# method with no arguments
|
||||
def typos
|
||||
:output
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
|
@ -52,7 +52,7 @@ module Ameba::Rule::Lint
|
|||
it "reports if proc argument is unused" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
-> (a : Int32, b : String) do
|
||||
# ^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
|
||||
# ^^^^^^^^^^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
|
||||
a = a + 1
|
||||
end
|
||||
CRYSTAL
|
||||
|
@ -306,7 +306,7 @@ module Ameba::Rule::Lint
|
|||
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
->(a : Int32) {}
|
||||
# ^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
|
||||
# ^^^^^^^^^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,93 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = AccessorMethodName.new
|
||||
|
||||
describe AccessorMethodName do
|
||||
it "passes if accessor method name is correct" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class Foo
|
||||
def self.instance
|
||||
end
|
||||
|
||||
def self.instance=(value)
|
||||
end
|
||||
|
||||
def user
|
||||
end
|
||||
|
||||
def user=(user)
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "passes if accessor method is defined in top-level scope" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def get_user
|
||||
end
|
||||
|
||||
def set_user(user)
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "fails if accessor method is defined with receiver in top-level scope" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def Foo.get_user
|
||||
# ^^^^^^^^ error: Favour method name 'user' over 'get_user'
|
||||
end
|
||||
|
||||
def Foo.set_user(user)
|
||||
# ^^^^^^^^ error: Favour method name 'user=' over 'set_user'
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "fails if accessor method name is wrong" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
class Foo
|
||||
def self.get_instance
|
||||
# ^^^^^^^^^^^^ error: Favour method name 'instance' over 'get_instance'
|
||||
end
|
||||
|
||||
def self.set_instance(value)
|
||||
# ^^^^^^^^^^^^ error: Favour method name 'instance=' over 'set_instance'
|
||||
end
|
||||
|
||||
def get_user
|
||||
# ^^^^^^^^ error: Favour method name 'user' over 'get_user'
|
||||
end
|
||||
|
||||
def set_user(user)
|
||||
# ^^^^^^^^ error: Favour method name 'user=' over 'set_user'
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "ignores if alternative name isn't valid syntax" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class Foo
|
||||
def get_404
|
||||
end
|
||||
|
||||
def set_404(value)
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "ignores if the method has unexpected arity" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class Foo
|
||||
def get_user(type)
|
||||
end
|
||||
|
||||
def set_user(user, type)
|
||||
end
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,151 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = AsciiIdentifiers.new
|
||||
|
||||
describe AsciiIdentifiers do
|
||||
it "reports classes with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
class BigAwesome🐺
|
||||
# ^^^^^^^^^^^ error: Identifier contains non-ascii characters
|
||||
@🐺_name : String
|
||||
# ^^^^^^^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports modules with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
module Bąk
|
||||
# ^^^ error: Identifier contains non-ascii characters
|
||||
@@bąk_name : String
|
||||
# ^^^^^^^^^^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports enums with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
enum TypeOf🔥
|
||||
# ^^^^^^^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports defs with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def łódź
|
||||
# ^^^^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports defs with parameter names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def forest_adventure(include_🐺 = true, include_🐿 = true)
|
||||
# ^ error: Identifier contains non-ascii characters
|
||||
# ^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports defs with parameter default values containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def forest_adventure(animal_type = :🐺)
|
||||
# ^^ error: Identifier contains non-ascii characters
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports argument names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
%w[wensleydale cheddar brie].each { |🧀| nil }
|
||||
# ^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports calls with arguments containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
%i[🐺 🐿].index!(:🐺)
|
||||
# ^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports calls with named arguments containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
%i[🐺 🐿].index!(obj: :🐺)
|
||||
# ^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports aliases with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
alias JSON🧀 = JSON::Any
|
||||
# ^^^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports constants with names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
I_LOVE_🍣 = true
|
||||
# ^^^^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports assignments with variable names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
space_👾 = true
|
||||
# ^^^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports multiple assignments with variable names containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
foo, space_👾 = true, true
|
||||
# ^^^^^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports assignments with symbol literals containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
foo = :신장
|
||||
# ^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports multiple assignments with symbol literals containing non-ascii characters" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
foo, bar = :신장, true
|
||||
# ^^^ error: Identifier contains non-ascii characters
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "passes for strings with non-ascii characters" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
space = "👾"
|
||||
space = :invader # 👾
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
context "properties" do
|
||||
context "#ignore_symbols" do
|
||||
it "returns `false` by default" do
|
||||
rule = AsciiIdentifiers.new
|
||||
rule.ignore_symbols?.should be_false
|
||||
end
|
||||
|
||||
it "stops reporting symbol literals if set to `true`" do
|
||||
rule = AsciiIdentifiers.new
|
||||
rule.ignore_symbols = true
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
def forest_adventure(animal_type = :🐺); end
|
||||
%i[🐺 🐿].index!(:🐺)
|
||||
foo, bar = :신장, true
|
||||
foo = :신장
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = BinaryOperatorParameterName.new
|
||||
|
||||
describe BinaryOperatorParameterName do
|
||||
it "ignores `other` parameter name in binary method definitions" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def +(other); end
|
||||
def -(other); end
|
||||
def *(other); end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "ignores binary method definitions with arity other than 1" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def +; end
|
||||
def +(foo, bar); end
|
||||
def -; end
|
||||
def -(foo, bar); end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "ignores non-binary method definitions" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def foo(bar); end
|
||||
def bąk(genus); end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports binary methods definitions with incorrectly named parameter" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def +(foo); end
|
||||
# ^ error: When defining the `+` operator, name its argument `other`
|
||||
def -(foo); end
|
||||
# ^ error: When defining the `-` operator, name its argument `other`
|
||||
def *(foo); end
|
||||
# ^ error: When defining the `*` operator, name its argument `other`
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "ignores methods from #excluded_operators" do
|
||||
subject.excluded_operators.each do |op|
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def #{op}(foo); end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,100 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = BlockParameterName.new
|
||||
.tap(&.min_name_length = 3)
|
||||
.tap(&.allowed_names = %w[_ e i j k v])
|
||||
|
||||
describe BlockParameterName do
|
||||
it "passes if block parameter name matches #allowed_names" do
|
||||
subject.allowed_names.each do |name|
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
%w[].each { |#{name}| }
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails if block parameter name doesn't match #allowed_names" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
# ^ error: Disallowed block parameter name found
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
context "properties" do
|
||||
context "#min_name_length" do
|
||||
it "allows setting custom values" do
|
||||
rule = BlockParameterName.new
|
||||
rule.allowed_names = %w[a b c]
|
||||
|
||||
rule.min_name_length = 3
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
# ^ error: Disallowed block parameter name found
|
||||
CRYSTAL
|
||||
|
||||
rule.min_name_length = 1
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
context "#allow_names_ending_in_numbers" do
|
||||
it "allows setting custom values" do
|
||||
rule = BlockParameterName.new
|
||||
rule.min_name_length = 1
|
||||
rule.allowed_names = %w[]
|
||||
|
||||
rule.allow_names_ending_in_numbers = false
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
%w[].each { |x1| }
|
||||
# ^ error: Disallowed block parameter name found
|
||||
CRYSTAL
|
||||
|
||||
rule.allow_names_ending_in_numbers = true
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
%w[].each { |x1| }
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
context "#allowed_names" do
|
||||
it "allows setting custom names" do
|
||||
rule = BlockParameterName.new
|
||||
rule.min_name_length = 3
|
||||
|
||||
rule.allowed_names = %w[a b c]
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
# ^ error: Disallowed block parameter name found
|
||||
CRYSTAL
|
||||
|
||||
rule.allowed_names = %w[x y z]
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
context "#forbidden_names" do
|
||||
it "allows setting custom names" do
|
||||
rule = BlockParameterName.new
|
||||
rule.min_name_length = 1
|
||||
rule.allowed_names = %w[]
|
||||
|
||||
rule.forbidden_names = %w[x y z]
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
# ^ error: Disallowed block parameter name found
|
||||
CRYSTAL
|
||||
|
||||
rule.forbidden_names = %w[a b c]
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
%w[].each { |x| }
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
subject = Rule::Style::ConstantNames.new
|
||||
subject = Rule::Naming::ConstantNames.new
|
||||
|
||||
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
|
||||
it "reports constant name #{expected}", file, line do
|
||||
rule = Rule::Style::ConstantNames.new
|
||||
rule = Rule::Naming::ConstantNames.new
|
||||
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||
%{name} = #{value}
|
||||
# ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name}
|
||||
|
@ -13,7 +13,7 @@ module Ameba
|
|||
end
|
||||
end
|
||||
|
||||
describe Rule::Style::ConstantNames do
|
||||
describe Rule::Naming::ConstantNames do
|
||||
it "passes if type names are screaming-cased" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
LUCKY_NUMBERS = [3, 7, 11]
|
|
@ -0,0 +1,19 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = Filename.new
|
||||
|
||||
describe Filename do
|
||||
it "passes if filename is correct" do
|
||||
expect_no_issues subject, code: "", path: "src/foo.cr"
|
||||
expect_no_issues subject, code: "", path: "src/foo_bar.cr"
|
||||
end
|
||||
|
||||
it "fails if filename is wrong" do
|
||||
expect_issue subject, <<-CRYSTAL, path: "src/fooBar.cr"
|
||||
|
||||
# ^{} error: Filename should be underscore-cased: foo_bar.cr, not fooBar.cr
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
subject = Rule::Style::MethodNames.new
|
||||
subject = Rule::Naming::MethodNames.new
|
||||
|
||||
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
|
||||
it "reports method name #{expected}", file, line do
|
||||
rule = Rule::Style::MethodNames.new
|
||||
rule = Rule::Naming::MethodNames.new
|
||||
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||
def %{name}; end
|
||||
# ^{name} error: Method name should be underscore-cased: #{expected}, not %{name}
|
||||
|
@ -13,7 +13,7 @@ module Ameba
|
|||
end
|
||||
end
|
||||
|
||||
describe Rule::Style::MethodNames do
|
||||
describe Rule::Naming::MethodNames do
|
||||
it "passes if method names are underscore-cased" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class Person
|
|
@ -1,6 +1,6 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Style
|
||||
module Ameba::Rule::Naming
|
||||
subject = PredicateName.new
|
||||
|
||||
describe PredicateName do
|
||||
|
@ -21,8 +21,18 @@ module Ameba::Rule::Style
|
|||
|
||||
it "fails if predicate name is wrong" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
class Image
|
||||
def self.is_valid?(x)
|
||||
# ^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?'
|
||||
end
|
||||
end
|
||||
|
||||
def is_valid?(x)
|
||||
# ^^^^^^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?'
|
||||
# ^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?'
|
||||
end
|
||||
|
||||
def is_valid(x)
|
||||
# ^^^^^^^^ error: Favour method name 'valid?' over 'is_valid'
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Style
|
||||
module Ameba::Rule::Naming
|
||||
subject = QueryBoolMethods.new
|
||||
|
||||
describe QueryBoolMethods do
|
|
@ -0,0 +1,53 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Naming
|
||||
subject = RescuedExceptionsVariableName.new
|
||||
|
||||
describe RescuedExceptionsVariableName do
|
||||
it "passes if exception handler variable name matches #allowed_names" do
|
||||
subject.allowed_names.each do |name|
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
def foo
|
||||
raise "foo"
|
||||
rescue #{name}
|
||||
nil
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
it "fails if exception handler variable name doesn't match #allowed_names" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
def foo
|
||||
raise "foo"
|
||||
rescue wtf
|
||||
# ^^^^^^^^ error: Disallowed variable name, use one of these instead: 'e', 'ex', 'exception', 'error'
|
||||
nil
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
context "properties" do
|
||||
context "#allowed_names" do
|
||||
it "returns sensible defaults" do
|
||||
rule = RescuedExceptionsVariableName.new
|
||||
rule.allowed_names.should eq %w[e ex exception error]
|
||||
end
|
||||
|
||||
it "allows setting custom names" do
|
||||
rule = RescuedExceptionsVariableName.new
|
||||
rule.allowed_names = %w[foo]
|
||||
|
||||
expect_issue rule, <<-CRYSTAL
|
||||
def foo
|
||||
raise "foo"
|
||||
rescue e
|
||||
# ^^^^^^ error: Disallowed variable name, use 'foo' instead
|
||||
nil
|
||||
end
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,19 +1,19 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
subject = Rule::Style::TypeNames.new
|
||||
subject = Rule::Naming::TypeNames.new
|
||||
|
||||
private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__)
|
||||
it "reports type name #{expected}", file, line do
|
||||
rule = Rule::Style::TypeNames.new
|
||||
rule = Rule::Naming::TypeNames.new
|
||||
expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line
|
||||
%{type} %{name}; end
|
||||
# ^{type}^{name}^^^^ error: Type name should be camelcased: #{expected}, but it was %{name}
|
||||
%{type} %{name}; end
|
||||
_{type} # ^{name} error: Type name should be camelcased: #{expected}, but it was %{name}
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
|
||||
describe Rule::Style::TypeNames do
|
||||
describe Rule::Naming::TypeNames do
|
||||
it "passes if type names are camelcased" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class ParseError < Exception
|
||||
|
@ -46,7 +46,7 @@ module Ameba
|
|||
it "reports alias name" do
|
||||
expect_issue subject, <<-CRYSTAL
|
||||
alias Numeric_value = Int32
|
||||
# ^{} error: Type name should be camelcased: NumericValue, but it was Numeric_value
|
||||
# ^^^^^^^^^^^^^ error: Type name should be camelcased: NumericValue, but it was Numeric_value
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
|
@ -1,11 +1,11 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba
|
||||
subject = Rule::Style::VariableNames.new
|
||||
subject = Rule::Naming::VariableNames.new
|
||||
|
||||
private def it_reports_var_name(name, value, expected, *, file = __FILE__, line = __LINE__)
|
||||
it "reports variable name #{expected}", file, line do
|
||||
rule = Rule::Style::VariableNames.new
|
||||
rule = Rule::Naming::VariableNames.new
|
||||
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||
%{name} = #{value}
|
||||
# ^{name} error: Var name should be underscore-cased: #{expected}, not %{name}
|
||||
|
@ -13,7 +13,7 @@ module Ameba
|
|||
end
|
||||
end
|
||||
|
||||
describe Rule::Style::VariableNames do
|
||||
describe Rule::Naming::VariableNames do
|
||||
it "passes if var names are underscore-cased" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
class Greeting
|
|
@ -48,7 +48,7 @@ module Ameba::Rule::Performance
|
|||
context "properties" do
|
||||
it "#filter_names" do
|
||||
rule = AnyAfterFilter.new
|
||||
rule.filter_names = %w(select)
|
||||
rule.filter_names = %w[select]
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
[1, 2, 3].reject { |e| e > 2 }.any?
|
||||
|
|
|
@ -46,7 +46,7 @@ module Ameba::Rule::Performance
|
|||
context "properties" do
|
||||
it "#call_names" do
|
||||
rule = ChainedCallWithNoBang.new
|
||||
rule.call_names = %w(uniq)
|
||||
rule.call_names = %w[uniq]
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
[1, 2, 3].select { |e| e > 2 }.reverse
|
||||
|
|
|
@ -6,7 +6,6 @@ module Ameba::Rule::Performance
|
|||
describe ExcessiveAllocations do
|
||||
it "passes if there is no potential performance improvements" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
"Alice".chars.each
|
||||
"Alice".chars.each(arg) { |c| puts c }
|
||||
"Alice".chars(arg).each { |c| puts c }
|
||||
"Alice\nBob".lines.each(arg) { |l| puts l }
|
||||
|
|
|
@ -64,7 +64,7 @@ module Ameba::Rule::Performance
|
|||
context "properties" do
|
||||
it "#filter_names" do
|
||||
rule = FirstLastAfterFilter.new
|
||||
rule.filter_names = %w(reject)
|
||||
rule.filter_names = %w[reject]
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
[1, 2, 3].select { |e| e > 2 }.first
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
require "../../../spec_helper"
|
||||
|
||||
module Ameba::Rule::Performance
|
||||
subject = MinMaxAfterMap.new
|
||||
|
||||
describe MinMaxAfterMap do
|
||||
it "passes if there are no potential performance improvements" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
%w[Alice Bob].map { |name| name.size }.min(2)
|
||||
%w[Alice Bob].map { |name| name.size }.max(2)
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "reports if there is a `min/max/minmax` call followed by `map`" do
|
||||
source = expect_issue subject, <<-CRYSTAL
|
||||
%w[Alice Bob].map { |name| name.size }.min
|
||||
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `min_of {...}` instead of `map {...}.min`.
|
||||
%w[Alice Bob].map(&.size).max.zero?
|
||||
# ^^^^^^^^^^^^^^^ error: Use `max_of {...}` instead of `map {...}.max`.
|
||||
%w[Alice Bob].map(&.size).minmax?
|
||||
# ^^^^^^^^^^^^^^^^^^^ error: Use `minmax_of? {...}` instead of `map {...}.minmax?`.
|
||||
CRYSTAL
|
||||
|
||||
expect_correction source, <<-CRYSTAL
|
||||
%w[Alice Bob].min_of { |name| name.size }
|
||||
%w[Alice Bob].max_of(&.size).zero?
|
||||
%w[Alice Bob].minmax_of?(&.size)
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
it "does not report if source is a spec" do
|
||||
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
|
||||
%w[Alice Bob].map(&.size).min
|
||||
CRYSTAL
|
||||
end
|
||||
|
||||
context "macro" do
|
||||
it "doesn't report in macro scope" do
|
||||
expect_no_issues subject, <<-CRYSTAL
|
||||
{{ %w[Alice Bob].map(&.size).min }}
|
||||
CRYSTAL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,7 +46,7 @@ module Ameba::Rule::Performance
|
|||
context "properties" do
|
||||
it "#filter_names" do
|
||||
rule = SizeAfterFilter.new
|
||||
rule.filter_names = %w(select)
|
||||
rule.filter_names = %w[select]
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
[1, 2, 3].reject(&.empty?).size
|
||||
|
|
|
@ -44,7 +44,7 @@ module Ameba::Rule::Style
|
|||
context "properties" do
|
||||
it "#filter_names" do
|
||||
rule = IsAFilter.new
|
||||
rule.filter_names = %w(select)
|
||||
rule.filter_names = %w[select]
|
||||
|
||||
expect_no_issues rule, <<-CRYSTAL
|
||||
[1, 2, nil].reject(&.nil?)
|
||||
|
|
|
@ -123,7 +123,7 @@ module Ameba
|
|||
it "#int_min_digits" do
|
||||
rule = Rule::Style::LargeNumbers.new
|
||||
rule.int_min_digits = 10
|
||||
expect_no_issues rule, %q(1200000)
|
||||
expect_no_issues rule, "1200000"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Ameba::Rule::Style
|
|||
subject = ParenthesesAroundCondition.new
|
||||
|
||||
describe ParenthesesAroundCondition do
|
||||
{% for keyword in %w(if unless while until) %}
|
||||
{% for keyword in %w[if unless while until] %}
|
||||
context "{{ keyword.id }}" do
|
||||
it "reports if redundant parentheses are found" do
|
||||
source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }}
|
||||
|
|
|
@ -95,7 +95,7 @@ module Ameba
|
|||
end
|
||||
|
||||
it "does not run other rules" do
|
||||
rules = [Rule::Lint::Syntax.new, Rule::Style::ConstantNames.new] of Rule::Base
|
||||
rules = [Rule::Lint::Syntax.new, Rule::Naming::ConstantNames.new] of Rule::Base
|
||||
source = Source.new <<-CRYSTAL
|
||||
MyBadConstant = 1
|
||||
|
||||
|
|
|
@ -22,23 +22,23 @@ module Ameba
|
|||
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
|
||||
)
|
||||
|
||||
it_tokenizes %(%w(1 2)),
|
||||
%w(STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
|
||||
it_tokenizes %(%w[1 2]),
|
||||
%w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
|
||||
|
||||
it_tokenizes %(%i(one two)),
|
||||
%w(SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
|
||||
it_tokenizes %(%i[one two]),
|
||||
%w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
|
||||
|
||||
it_tokenizes %(
|
||||
class A
|
||||
def method
|
||||
puts "hello"
|
||||
end
|
||||
class A
|
||||
def method
|
||||
puts "hello"
|
||||
end
|
||||
), %w(
|
||||
end
|
||||
), %w[
|
||||
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
|
||||
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
|
||||
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Lint/ComparisonToBoolean:
|
||||
Enabled: true
|
|
@ -6,7 +6,7 @@ module Ameba
|
|||
# Dummy Rule which does nothing.
|
||||
class DummyRule < Rule::Base
|
||||
properties do
|
||||
description : String = "Dummy rule that does nothing."
|
||||
description "Dummy rule that does nothing."
|
||||
dummy true
|
||||
end
|
||||
|
||||
|
@ -92,7 +92,7 @@ module Ameba
|
|||
|
||||
class PerfRule < Rule::Performance::Base
|
||||
properties do
|
||||
description : String = "Sample performance rule"
|
||||
description "Sample performance rule"
|
||||
end
|
||||
|
||||
def test(source)
|
||||
|
@ -259,6 +259,7 @@ module Ameba
|
|||
Crystal::MacroLiteral,
|
||||
Crystal::Expressions,
|
||||
Crystal::ControlExpression,
|
||||
Crystal::Call,
|
||||
}
|
||||
|
||||
def initialize(node)
|
||||
|
@ -282,6 +283,13 @@ module Ameba
|
|||
end
|
||||
end
|
||||
|
||||
def with_presenter(klass, &)
|
||||
io = IO::Memory.new
|
||||
presenter = klass.new(io)
|
||||
|
||||
yield presenter, io
|
||||
end
|
||||
|
||||
def as_node(source)
|
||||
Crystal::Parser.new(source).parse
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@ require "./ameba/ast/**"
|
|||
require "./ameba/ext/**"
|
||||
require "./ameba/rule/**"
|
||||
require "./ameba/formatter/*"
|
||||
require "./ameba/presenter/*"
|
||||
require "./ameba/source/**"
|
||||
|
||||
# Ameba's entry module.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require "./util"
|
||||
|
||||
module Ameba::AST
|
||||
# Represents the branch in Crystal code.
|
||||
# Branch is a part of a branchable statement.
|
||||
|
@ -67,6 +69,8 @@ module Ameba::AST
|
|||
|
||||
# :nodoc:
|
||||
private class BranchVisitor < Crystal::Visitor
|
||||
include Util
|
||||
|
||||
@current_branch : Crystal::ASTNode?
|
||||
|
||||
property branchable : Branchable?
|
||||
|
@ -79,7 +83,7 @@ module Ameba::AST
|
|||
on_branchable_start(node, branches)
|
||||
end
|
||||
|
||||
private def on_branchable_start(node, branches : Array | Tuple)
|
||||
private def on_branchable_start(node, branches : Enumerable)
|
||||
@branchable = Branchable.new(node, @branchable)
|
||||
|
||||
branches.each do |branch_node|
|
||||
|
@ -172,6 +176,18 @@ module Ameba::AST
|
|||
def end_visit(node : Crystal::MacroFor)
|
||||
on_branchable_end node
|
||||
end
|
||||
|
||||
def visit(node : Crystal::Call)
|
||||
if loop?(node) && (block = node.block)
|
||||
on_branchable_start node, block.body
|
||||
end
|
||||
end
|
||||
|
||||
def end_visit(node : Crystal::Call)
|
||||
if loop?(node) && node.block
|
||||
on_branchable_end node
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,9 +34,8 @@ module Ameba::AST
|
|||
# The actual AST node that represents a current scope.
|
||||
getter node : Crystal::ASTNode
|
||||
|
||||
delegate to_s, to: node
|
||||
delegate location, to: node
|
||||
delegate end_location, to: node
|
||||
delegate location, end_location, to_s,
|
||||
to: @node
|
||||
|
||||
def_equals_and_hash node, location
|
||||
|
||||
|
@ -181,14 +180,19 @@ module Ameba::AST
|
|||
@visibility || outer_scope.try(&.visibility)
|
||||
end
|
||||
|
||||
# Returns `true` if current scope is a def, `false` otherwise.
|
||||
def def?
|
||||
node.is_a?(Crystal::Def)
|
||||
end
|
||||
{% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
|
||||
{% method_name = type.underscore %}
|
||||
# Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
|
||||
def {{ method_name }}?(*, check_outer_scopes = false)
|
||||
node.is_a?(Crystal::{{ type }}) ||
|
||||
!!(check_outer_scopes &&
|
||||
outer_scope.try(&.{{ method_name }}?(check_outer_scopes: true)))
|
||||
end
|
||||
{% end %}
|
||||
|
||||
# Returns `true` if this scope is a top level scope, `false` otherwise.
|
||||
def top_level?
|
||||
outer_scope.nil? || type_definition?
|
||||
outer_scope.nil?
|
||||
end
|
||||
|
||||
# Returns `true` if var is an argument in current scope, `false` otherwise.
|
||||
|
|
|
@ -21,8 +21,8 @@ module Ameba::AST::Util
|
|||
static_literal?(node.to)}
|
||||
when Crystal::ArrayLiteral,
|
||||
Crystal::TupleLiteral
|
||||
{true, node.elements.all? do |el|
|
||||
static_literal?(el)
|
||||
{true, node.elements.all? do |element|
|
||||
static_literal?(element)
|
||||
end}
|
||||
when Crystal::HashLiteral
|
||||
{true, node.entries.all? do |entry|
|
||||
|
|
|
@ -19,9 +19,8 @@ module Ameba::AST
|
|||
# Variable of this argument (may be the same node)
|
||||
getter variable : Variable
|
||||
|
||||
delegate location, to: @node
|
||||
delegate end_location, to: @node
|
||||
delegate to_s, to: @node
|
||||
delegate location, end_location, to_s,
|
||||
to: @node
|
||||
|
||||
# Creates a new argument.
|
||||
#
|
||||
|
|
|
@ -19,9 +19,8 @@ module Ameba::AST
|
|||
# A scope assignment belongs to
|
||||
getter scope : Scope
|
||||
|
||||
delegate to_s, to: @node
|
||||
delegate location, to: @node
|
||||
delegate end_location, to: @node
|
||||
delegate location, end_location, to_s,
|
||||
to: @node
|
||||
|
||||
# Creates a new assignment.
|
||||
#
|
||||
|
@ -32,9 +31,7 @@ module Ameba::AST
|
|||
return unless scope = @variable.scope
|
||||
|
||||
@branch = Branch.of(@node, scope)
|
||||
@referenced = true if @variable.special? ||
|
||||
@variable.scope.type_definition? ||
|
||||
referenced_in_loop?
|
||||
@referenced = true if @variable.special? || referenced_in_loop?
|
||||
end
|
||||
|
||||
def referenced_in_loop?
|
||||
|
@ -75,31 +72,5 @@ module Ameba::AST
|
|||
node
|
||||
end
|
||||
end
|
||||
|
||||
# Indicates whether the node is a transformed assignment by the compiler.
|
||||
# i.e.
|
||||
#
|
||||
# ```
|
||||
# collection.each do |(a, b)|
|
||||
# puts b
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# is transformed to:
|
||||
#
|
||||
# ```
|
||||
# collection.each do |__arg0|
|
||||
# a = __arg0[0]
|
||||
# b = __arg0[1]
|
||||
# puts(b)
|
||||
# end
|
||||
# ```
|
||||
def transformed?
|
||||
return false unless (assign = node).is_a?(Crystal::Assign)
|
||||
return false unless (value = assign.value).is_a?(Crystal::Call)
|
||||
return false unless (obj = value.obj).is_a?(Crystal::Var)
|
||||
|
||||
obj.name.starts_with? "__arg"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,10 +2,8 @@ module Ameba::AST
|
|||
class InstanceVariable
|
||||
getter node : Crystal::InstanceVar
|
||||
|
||||
delegate location, to: @node
|
||||
delegate end_location, to: @node
|
||||
delegate name, to: @node
|
||||
delegate to_s, to: @node
|
||||
delegate location, end_location, name, to_s,
|
||||
to: @node
|
||||
|
||||
def initialize(@node)
|
||||
end
|
||||
|
|
|
@ -2,9 +2,8 @@ module Ameba::AST
|
|||
class TypeDecVariable
|
||||
getter node : Crystal::TypeDeclaration
|
||||
|
||||
delegate location, to: @node
|
||||
delegate end_location, to: @node
|
||||
delegate to_s, to: @node
|
||||
delegate location, end_location, to_s,
|
||||
to: @node
|
||||
|
||||
def initialize(@node)
|
||||
end
|
||||
|
|
|
@ -17,10 +17,8 @@ module Ameba::AST
|
|||
# Node of the first assignment which can be available before any reference.
|
||||
getter assign_before_reference : Crystal::ASTNode?
|
||||
|
||||
delegate location, to: @node
|
||||
delegate end_location, to: @node
|
||||
delegate name, to: @node
|
||||
delegate to_s, to: @node
|
||||
delegate location, end_location, name, to_s,
|
||||
to: @node
|
||||
|
||||
# Creates a new variable(in the scope).
|
||||
#
|
||||
|
@ -54,7 +52,7 @@ module Ameba::AST
|
|||
#
|
||||
# ```
|
||||
# variable = Variable.new(node, scope)
|
||||
# variable.reference(var_node)
|
||||
# variable.reference(var_node, some_scope)
|
||||
# variable.referenced? # => true
|
||||
# ```
|
||||
def referenced?
|
||||
|
@ -74,6 +72,11 @@ module Ameba::AST
|
|||
end
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def reference(scope : Scope)
|
||||
reference(node, scope)
|
||||
end
|
||||
|
||||
# Reference variable's assignments.
|
||||
#
|
||||
# ```
|
||||
|
@ -136,7 +139,7 @@ module Ameba::AST
|
|||
case assign
|
||||
when Crystal::Assign then eql?(assign.target)
|
||||
when Crystal::OpAssign then eql?(assign.target)
|
||||
when Crystal::MultiAssign then assign.targets.any? { |t| eql?(t) }
|
||||
when Crystal::MultiAssign then assign.targets.any? { |target| eql?(target) }
|
||||
when Crystal::UninitializedVar then eql?(assign.var)
|
||||
else
|
||||
false
|
||||
|
@ -208,9 +211,9 @@ module Ameba::AST
|
|||
return if references.size > assignments.size
|
||||
return if assignments.any?(&.op_assign?)
|
||||
|
||||
@assign_before_reference = assignments.find { |ass|
|
||||
!ass.in_branch?
|
||||
}.try &.node
|
||||
@assign_before_reference = assignments
|
||||
.find(&.in_branch?.!)
|
||||
.try(&.node)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@ module Ameba::AST
|
|||
# Uses the same logic than rubocop. See
|
||||
# https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21
|
||||
# Except "for", because crystal doesn't have a "for" loop.
|
||||
{% for node in %i(if while until rescue or and) %}
|
||||
{% for node in %i[if while until rescue or and] %}
|
||||
# :nodoc:
|
||||
def visit(node : Crystal::{{ node.id.capitalize }})
|
||||
@complexity += 1 unless macro_condition?
|
||||
|
|
|
@ -32,6 +32,7 @@ module Ameba::AST
|
|||
IsA,
|
||||
LibDef,
|
||||
ModuleDef,
|
||||
MultiAssign,
|
||||
NilLiteral,
|
||||
StringInterpolation,
|
||||
Unless,
|
||||
|
|
|
@ -43,7 +43,7 @@ module Ameba::AST
|
|||
end
|
||||
|
||||
private def traverse_case(node)
|
||||
node.whens.each { |n| traverse_node n.body }
|
||||
node.whens.each { |when_node| traverse_node when_node.body }
|
||||
traverse_node(node.else)
|
||||
end
|
||||
|
||||
|
@ -54,7 +54,7 @@ module Ameba::AST
|
|||
private def traverse_exception_handler(node)
|
||||
traverse_node node.body
|
||||
traverse_node node.else
|
||||
node.rescues.try &.each { |n| traverse_node n.body }
|
||||
node.rescues.try &.each { |rescue_node| traverse_node rescue_node.body }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,6 @@ module Ameba::AST
|
|||
}
|
||||
|
||||
SPECIAL_NODE_NAMES = %w[super previous_def]
|
||||
RECORD_NODE_NAME = "record"
|
||||
|
||||
@scope_queue = [] of Scope
|
||||
@current_scope : Scope
|
||||
|
@ -154,7 +153,7 @@ module Ameba::AST
|
|||
|
||||
# :nodoc:
|
||||
def visit(node : Crystal::Var)
|
||||
variable = @current_scope.find_variable node.name
|
||||
variable = @current_scope.find_variable(node.name)
|
||||
|
||||
case
|
||||
when @current_scope.arg?(node) # node is an argument
|
||||
|
@ -162,7 +161,7 @@ module Ameba::AST
|
|||
when variable.nil? && @current_assign # node is a variable
|
||||
@current_scope.add_variable(node)
|
||||
when variable # node is a reference
|
||||
reference = variable.reference node, @current_scope
|
||||
reference = variable.reference(node, @current_scope)
|
||||
if @current_assign.is_a?(Crystal::OpAssign) || !reference.target_of?(@current_assign)
|
||||
variable.reference_assignments!
|
||||
end
|
||||
|
@ -171,26 +170,39 @@ module Ameba::AST
|
|||
|
||||
# :nodoc:
|
||||
def visit(node : Crystal::Call)
|
||||
case
|
||||
when @current_scope.def?
|
||||
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
|
||||
@current_scope.arguments.each do |arg|
|
||||
variable = arg.variable
|
||||
scope = @current_scope
|
||||
|
||||
ref = variable.reference(variable.node, @current_scope)
|
||||
ref.explicit = false
|
||||
end
|
||||
case
|
||||
when (scope.top_level? || scope.type_definition?) && record_macro?(node)
|
||||
return false
|
||||
when scope.type_definition? && accessor_macro?(node)
|
||||
return false
|
||||
when scope.def? && special_node?(node)
|
||||
scope.arguments.each do |arg|
|
||||
ref = arg.variable.reference(scope)
|
||||
ref.explicit = false
|
||||
end
|
||||
true
|
||||
when @current_scope.top_level? && record_macro?(node)
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
private def special_node?(node)
|
||||
node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
|
||||
end
|
||||
|
||||
private def accessor_macro?(node)
|
||||
node.name.matches? /^(class_)?(getter[?!]?|setter|property[?!]?)$/
|
||||
end
|
||||
|
||||
private def record_macro?(node)
|
||||
node.name == RECORD_NODE_NAME && node.args.first?.is_a?(Crystal::Path)
|
||||
return false unless node.name == "record"
|
||||
|
||||
case node.args.first?
|
||||
when Crystal::Path, Crystal::Generic
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private def skip?(node)
|
||||
|
|
|
@ -28,7 +28,14 @@ module Ameba::Cli
|
|||
configure_rules(config, opts)
|
||||
|
||||
if opts.rules?
|
||||
print_rules(config)
|
||||
print_rules(config.rules)
|
||||
end
|
||||
|
||||
if describe_rule_name = opts.describe_rule
|
||||
unless rule = config.rules.find(&.name.== describe_rule_name)
|
||||
raise "Unknown rule"
|
||||
end
|
||||
describe_rule(rule)
|
||||
end
|
||||
|
||||
runner = Ameba.run(config)
|
||||
|
@ -49,6 +56,7 @@ module Ameba::Cli
|
|||
property globs : Array(String)?
|
||||
property only : Array(String)?
|
||||
property except : Array(String)?
|
||||
property describe_rule : String?
|
||||
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
||||
property fail_level : Severity?
|
||||
property? skip_reading_config = false
|
||||
|
@ -67,11 +75,11 @@ module Ameba::Cli
|
|||
parser.on("-h", "--help", "Show this help") { print_help(parser) }
|
||||
parser.on("-r", "--rules", "Show all available rules") { opts.rules = true }
|
||||
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
||||
parser.unknown_args do |f|
|
||||
if f.size == 1 && f.first =~ /.+:\d+:\d+/
|
||||
configure_explain_opts(f.first, opts)
|
||||
parser.unknown_args do |arr|
|
||||
if arr.size == 1 && arr.first.matches?(/.+:\d+:\d+/)
|
||||
configure_explain_opts(arr.first, opts)
|
||||
else
|
||||
opts.globs = f unless f.empty?
|
||||
opts.globs = arr unless arr.empty?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -119,6 +127,11 @@ module Ameba::Cli
|
|||
configure_explain_opts(loc, opts)
|
||||
end
|
||||
|
||||
parser.on("-d", "--describe Category/Rule",
|
||||
"Describe a rule with specified name") do |rule_name|
|
||||
configure_describe_opts(rule_name, opts)
|
||||
end
|
||||
|
||||
parser.on("--without-affected-code",
|
||||
"Stop showing affected code while using a default formatter") do
|
||||
opts.without_affected_code = true
|
||||
|
@ -152,6 +165,11 @@ module Ameba::Cli
|
|||
opts.without_affected_code?
|
||||
end
|
||||
|
||||
private def configure_describe_opts(rule_name, opts)
|
||||
opts.describe_rule = rule_name.presence
|
||||
opts.formatter = :silent
|
||||
end
|
||||
|
||||
private def configure_explain_opts(loc, opts)
|
||||
location_to_explain = parse_explain_location(loc)
|
||||
opts.location_to_explain = location_to_explain
|
||||
|
@ -183,14 +201,13 @@ module Ameba::Cli
|
|||
exit 0
|
||||
end
|
||||
|
||||
private def print_rules(config)
|
||||
config.rules.each do |rule|
|
||||
puts "%s [%s] - %s" % {
|
||||
rule.name.colorize(:white),
|
||||
rule.severity.symbol.to_s.colorize(:green),
|
||||
rule.description.colorize(:dark_gray),
|
||||
}
|
||||
end
|
||||
private def describe_rule(rule)
|
||||
Presenter::RulePresenter.new.run(rule)
|
||||
exit 0
|
||||
end
|
||||
|
||||
private def print_rules(rules)
|
||||
Presenter::RuleCollectionPresenter.new.run(rules)
|
||||
exit 0
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,8 +97,9 @@ class Ameba::Config
|
|||
@excluded = load_array_section(config, "Excluded")
|
||||
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
|
||||
|
||||
return unless formatter_name = load_formatter_name(config)
|
||||
self.formatter = formatter_name
|
||||
if formatter_name = load_formatter_name(config)
|
||||
self.formatter = formatter_name
|
||||
end
|
||||
end
|
||||
|
||||
# Loads YAML configuration file by `path`.
|
||||
|
@ -115,12 +116,13 @@ class Ameba::Config
|
|||
end
|
||||
Config.new YAML.parse(content)
|
||||
rescue e
|
||||
raise "Config file is invalid: #{e.message}"
|
||||
raise "Unable to load config file: #{e.message}"
|
||||
end
|
||||
|
||||
protected def self.read_config(path = nil)
|
||||
if path
|
||||
return File.exists?(path) ? File.read(path) : nil
|
||||
return File.read(path) if File.exists?(path)
|
||||
raise "Config file does not exist"
|
||||
end
|
||||
each_config_path do |config_path|
|
||||
return File.read(config_path) if File.exists?(config_path)
|
||||
|
@ -202,13 +204,13 @@ class Ameba::Config
|
|||
#
|
||||
# ```
|
||||
# config = Ameba::Config.load
|
||||
# config.update_rules %w(Rule1 Rule2), enabled: true
|
||||
# config.update_rules %w[Rule1 Rule2], enabled: true
|
||||
# ```
|
||||
#
|
||||
# also it allows to update groups of rules:
|
||||
#
|
||||
# ```
|
||||
# config.update_rules %w(Group1 Group2), enabled: true
|
||||
# config.update_rules %w[Group1 Group2], enabled: true
|
||||
# ```
|
||||
def update_rules(names, enabled = true, excluded = nil)
|
||||
names.try &.each do |name|
|
||||
|
@ -243,20 +245,20 @@ class Ameba::Config
|
|||
# Define rule properties
|
||||
macro properties(&block)
|
||||
{% definitions = [] of NamedTuple %}
|
||||
{% if block.body.is_a? Assign %}
|
||||
{% definitions << {var: block.body.target, value: block.body.value} %}
|
||||
{% elsif block.body.is_a? Call %}
|
||||
{% definitions << {var: block.body.name, value: block.body.args.first} %}
|
||||
{% elsif block.body.is_a? TypeDeclaration %}
|
||||
{% definitions << {var: block.body.var, value: block.body.value, type: block.body.type} %}
|
||||
{% if (prop = block.body).is_a? Call %}
|
||||
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
|
||||
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
|
||||
{% else %}
|
||||
{% definitions << {var: prop.name, value: prop.args.first} %}
|
||||
{% end %}
|
||||
{% elsif block.body.is_a? Expressions %}
|
||||
{% for prop in block.body.expressions %}
|
||||
{% if prop.is_a? Assign %}
|
||||
{% definitions << {var: prop.target, value: prop.value} %}
|
||||
{% elsif prop.is_a? Call %}
|
||||
{% definitions << {var: prop.name, value: prop.args.first} %}
|
||||
{% elsif prop.is_a? TypeDeclaration %}
|
||||
{% definitions << {var: prop.var, value: prop.value, type: prop.type} %}
|
||||
{% if prop.is_a? Call %}
|
||||
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
|
||||
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
|
||||
{% else %}
|
||||
{% definitions << {var: prop.name, value: prop.args.first} %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
@ -322,9 +324,10 @@ class Ameba::Config
|
|||
|
||||
macro included
|
||||
GROUP_SEVERITY = {
|
||||
Lint: Ameba::Severity::Warning,
|
||||
Metrics: Ameba::Severity::Warning,
|
||||
Performance: Ameba::Severity::Warning,
|
||||
Documentation: Ameba::Severity::Warning,
|
||||
Lint: Ameba::Severity::Warning,
|
||||
Metrics: Ameba::Severity::Warning,
|
||||
Performance: Ameba::Severity::Warning,
|
||||
}
|
||||
|
||||
class_getter default_severity : Ameba::Severity do
|
||||
|
|
|
@ -17,13 +17,13 @@ module Ameba::Formatter
|
|||
# A list of sources to inspect is passed as an argument.
|
||||
def started(sources); end
|
||||
|
||||
# Callback that indicates when source inspection is finished.
|
||||
# Callback that indicates when source inspection is started.
|
||||
# A corresponding source is passed as an argument.
|
||||
def source_finished(source : Source); end
|
||||
def source_started(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
|
||||
def source_finished(source : Source); end
|
||||
|
||||
# Callback that indicates when inspection is finished.
|
||||
# A list of inspected sources is passed as an argument.
|
||||
|
|
|
@ -4,8 +4,6 @@ module Ameba::Formatter
|
|||
# A formatter that shows the detailed explanation of the issue at
|
||||
# a specific location.
|
||||
class ExplainFormatter
|
||||
HEADING_MARKER = "## "
|
||||
|
||||
include Util
|
||||
|
||||
getter output : IO::FileDescriptor | IO::Memory
|
||||
|
@ -64,9 +62,8 @@ module Ameba::Formatter
|
|||
rule.name.colorize(:magenta),
|
||||
rule.severity.to_s.colorize(rule.severity.color),
|
||||
}
|
||||
|
||||
if rule.responds_to?(:description)
|
||||
output_paragraph rule.description
|
||||
if rule_description = colorize_code_fences(rule.description)
|
||||
output_paragraph rule_description
|
||||
end
|
||||
|
||||
rule_doc = colorize_code_fences(rule.class.parsed_doc)
|
||||
|
@ -84,7 +81,7 @@ module Ameba::Formatter
|
|||
end
|
||||
|
||||
private def output_title(title)
|
||||
output << HEADING_MARKER.colorize(:yellow)
|
||||
output << "### ".colorize(:yellow)
|
||||
output << title.upcase.colorize(:yellow)
|
||||
output << "\n\n"
|
||||
end
|
||||
|
@ -95,7 +92,7 @@ module Ameba::Formatter
|
|||
|
||||
private def output_paragraph(paragraph : Array)
|
||||
paragraph.each do |line|
|
||||
output << ' ' << line << '\n'
|
||||
output << " " << line << '\n'
|
||||
end
|
||||
output << '\n'
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ module Ameba::Formatter
|
|||
# Basically, it takes all issues reported and disables corresponding rules
|
||||
# or excludes failed sources from these rules.
|
||||
class TODOFormatter < DotFormatter
|
||||
def initialize(@output = STDOUT)
|
||||
def initialize(@output = STDOUT, @config_path : Path = Config::DEFAULT_PATH)
|
||||
end
|
||||
|
||||
def finished(sources)
|
||||
|
@ -26,25 +26,30 @@ module Ameba::Formatter
|
|||
end
|
||||
|
||||
private def generate_todo_config(issues)
|
||||
file = File.new(Config::DEFAULT_PATH, mode: "w")
|
||||
file << header
|
||||
rule_issues_map(issues).each do |rule, rule_issues|
|
||||
file << "\n# Problems found: #{rule_issues.size}"
|
||||
file << "\n# Run `ameba --only #{rule.name}` for details"
|
||||
file << rule_todo(rule, rule_issues).gsub("---", "")
|
||||
File.open(@config_path, mode: "w") do |file|
|
||||
file << header
|
||||
|
||||
rule_issues_map(issues).each do |rule, rule_issues|
|
||||
rule_todo = rule_todo(rule, rule_issues)
|
||||
rule_todo =
|
||||
{rule_todo.name => rule_todo}
|
||||
.to_yaml.gsub("---", "")
|
||||
|
||||
file << "\n# Problems found: #{rule_issues.size}"
|
||||
file << "\n# Run `ameba --only #{rule.name}` for details"
|
||||
file << rule_todo
|
||||
end
|
||||
file
|
||||
end
|
||||
file
|
||||
ensure
|
||||
file.close if file
|
||||
end
|
||||
|
||||
private def rule_issues_map(issues)
|
||||
Hash(Rule::Base, Array(Issue)).new.tap do |h|
|
||||
Hash(Rule::Base, Array(Issue)).new.tap do |hash|
|
||||
issues.each do |issue|
|
||||
next if issue.disabled? || issue.rule.is_a?(Rule::Lint::Syntax)
|
||||
next if issue.correctable? && config[:autocorrect]?
|
||||
|
||||
(h[issue.rule] ||= Array(Issue).new) << issue
|
||||
(hash[issue.rule] ||= Array(Issue).new) << issue
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -60,11 +65,11 @@ module Ameba::Formatter
|
|||
end
|
||||
|
||||
private def rule_todo(rule, issues)
|
||||
rule.excluded = issues
|
||||
.compact_map(&.location.try &.filename.try &.to_s)
|
||||
.uniq!
|
||||
|
||||
{rule.name => rule}.to_yaml
|
||||
rule.dup.tap do |rule_todo|
|
||||
rule_todo.excluded = issues
|
||||
.compact_map(&.location.try &.filename.try &.to_s)
|
||||
.uniq!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
module Ameba::Formatter
|
||||
module Util
|
||||
extend self
|
||||
|
||||
def deansify(message : String?) : String?
|
||||
message.try &.gsub(/\x1b[^m]*m/, "").presence
|
||||
end
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
module Ameba::Presenter
|
||||
private ENABLED_MARK = "✓".colorize(:green)
|
||||
private DISABLED_MARK = "x".colorize(:red)
|
||||
|
||||
class BasePresenter
|
||||
# TODO: allow other IOs
|
||||
getter output : IO::FileDescriptor | IO::Memory
|
||||
|
||||
def initialize(@output = STDOUT)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
module Ameba::Presenter
|
||||
class RuleCollectionPresenter < BasePresenter
|
||||
def run(rules)
|
||||
rules = rules.to_h do |rule|
|
||||
name = rule.name.split('/')
|
||||
name = "%s/%s" % {
|
||||
name[0...-1].join('/').colorize(:light_gray),
|
||||
name.last.colorize(:white),
|
||||
}
|
||||
{name, rule}
|
||||
end
|
||||
longest_name = rules.max_of(&.first.size)
|
||||
|
||||
rules.group_by(&.last.group).each do |group, group_rules|
|
||||
output.puts "— %s" % group.colorize(:light_blue).underline
|
||||
output.puts
|
||||
group_rules.each do |name, rule|
|
||||
output.puts " %s [%s] %s %s" % {
|
||||
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
|
||||
rule.severity.symbol.to_s.colorize(:green),
|
||||
name.ljust(longest_name),
|
||||
rule.description.colorize(:dark_gray),
|
||||
}
|
||||
end
|
||||
output.puts
|
||||
end
|
||||
|
||||
output.puts "Total rules: %s / %s enabled" % {
|
||||
rules.size.to_s.colorize(:light_blue),
|
||||
rules.count(&.last.enabled?).to_s.colorize(:light_blue),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
module Ameba::Presenter
|
||||
class RulePresenter < BasePresenter
|
||||
def run(rule)
|
||||
output.puts
|
||||
output_title "Rule info"
|
||||
output_paragraph "%s of a %s severity [enabled: %s]" % {
|
||||
rule.name.colorize(:magenta),
|
||||
rule.severity.to_s.colorize(rule.severity.color),
|
||||
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
|
||||
}
|
||||
if rule_description = colorize_code_fences(rule.description)
|
||||
output_paragraph rule_description
|
||||
end
|
||||
|
||||
if rule_doc = colorize_code_fences(rule.class.parsed_doc)
|
||||
output_title "Detailed description"
|
||||
output_paragraph rule_doc
|
||||
end
|
||||
end
|
||||
|
||||
private def output_title(title)
|
||||
output.print "### %s\n\n" % title.upcase.colorize(:yellow)
|
||||
end
|
||||
|
||||
private def output_paragraph(paragraph : String)
|
||||
output_paragraph(paragraph.lines)
|
||||
end
|
||||
|
||||
private def output_paragraph(paragraph : Array)
|
||||
paragraph.each do |line|
|
||||
output.puts " #{line}"
|
||||
end
|
||||
output.puts
|
||||
end
|
||||
|
||||
private def colorize_code_fences(string)
|
||||
return unless string
|
||||
string
|
||||
.gsub(/```(.+?)```/m, &.colorize(:dark_gray))
|
||||
.gsub(/`(?!`)(.+?)`/, &.colorize(:dark_gray))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,10 @@
|
|||
require "./ast/util"
|
||||
|
||||
module Ameba
|
||||
# Represents a module used to report issues.
|
||||
module Reportable
|
||||
include AST::Util
|
||||
|
||||
# List of reported issues.
|
||||
getter issues = [] of Issue
|
||||
|
||||
|
@ -30,13 +34,19 @@ module Ameba
|
|||
end
|
||||
|
||||
# Adds a new issue for Crystal AST *node*.
|
||||
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil) : Issue
|
||||
add_issue rule, node.location, node.end_location, message, status, block
|
||||
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil, *, prefer_name_location = false) : Issue
|
||||
location = name_location(node) if prefer_name_location
|
||||
location ||= node.location
|
||||
|
||||
end_location = name_end_location(node) if prefer_name_location
|
||||
end_location ||= node.end_location
|
||||
|
||||
add_issue rule, location, end_location, message, status, block
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue
|
||||
add_issue rule, node, message, status, block
|
||||
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, *, prefer_name_location = false, &block : Source::Corrector ->) : Issue
|
||||
add_issue rule, node, message, status, block, prefer_name_location: prefer_name_location
|
||||
end
|
||||
|
||||
# Adds a new issue for Crystal *token*.
|
||||
|
|
|
@ -32,14 +32,15 @@ module Ameba::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 issue.
|
||||
#
|
||||
# Be default it uses a node visitor to traverse all the nodes in the source.
|
||||
# By default it uses a node visitor to traverse all the nodes in the source.
|
||||
#
|
||||
# NOTE: Must be overridden for other type of rules.
|
||||
def test(source : Source)
|
||||
AST::NodeVisitor.new self, source
|
||||
end
|
||||
|
||||
# NOTE: Can't be abstract
|
||||
def test(source : Source, node : Crystal::ASTNode, *opts)
|
||||
# can't be abstract
|
||||
end
|
||||
|
||||
# A convenient addition to `#test` method that does the same
|
||||
|
@ -114,7 +115,7 @@ module Ameba::Rule
|
|||
|
||||
# Adds an issue to the *source*
|
||||
macro issue_for(*args, **kwargs, &block)
|
||||
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }}
|
||||
source.add_issue(self, {{ args.splat }}, {{ kwargs.double_splat }}) {{ block }}
|
||||
end
|
||||
|
||||
protected def self.rule_name
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
module Ameba::Rule::Lint
|
||||
module Ameba::Rule::Documentation
|
||||
# A rule that enforces documentation for public types:
|
||||
# modules, classes, enums, methods and macros.
|
||||
#
|
||||
# YAML configuration example:
|
||||
#
|
||||
# ```
|
||||
# Lint/Documentation:
|
||||
# Documentation/Documentation:
|
||||
# Enabled: true
|
||||
# IgnoreClasses: false
|
||||
# IgnoreModules: true
|
|
@ -0,0 +1,96 @@
|
|||
module Ameba::Rule::Documentation
|
||||
# A rule that reports documentation admonitions.
|
||||
#
|
||||
# Optionally, these can fail at an appropriate time.
|
||||
#
|
||||
# ```
|
||||
# def get_user(id)
|
||||
# # TODO(2024-04-24) Fix this hack when the database migration is complete
|
||||
# if id < 1_000_000
|
||||
# v1_api_call(id)
|
||||
# else
|
||||
# v2_api_call(id)
|
||||
# end
|
||||
# end
|
||||
# ```
|
||||
#
|
||||
# `TODO` comments are used to remind yourself of source code related things.
|
||||
#
|
||||
# The premise here is that `TODO` should be dealt with in the near future
|
||||
# and are therefore reported by Ameba.
|
||||
#
|
||||
# `FIXME` comments are used to indicate places where source code needs fixing.
|
||||
#
|
||||
# The premise here is that `FIXME` should indeed be fixed as soon as possible
|
||||
# and are therefore reported by Ameba.
|
||||
#
|
||||
# YAML configuration example:
|
||||
#
|
||||
# ```
|
||||
# Documentation/DocumentationAdmonition:
|
||||
# Enabled: true
|
||||
# Admonitions: [TODO, FIXME, BUG]
|
||||
# Timezone: UTC
|
||||
# ```
|
||||
class DocumentationAdmonition < Base
|
||||
properties do
|
||||
description "Reports documentation admonitions"
|
||||
admonitions %w[TODO FIXME BUG]
|
||||
timezone "UTC"
|
||||
end
|
||||
|
||||
MSG = "Found a %s admonition in a comment"
|
||||
MSG_LATE = "Found a %s admonition in a comment (%s)"
|
||||
MSG_ERR = "%s admonition error: %s"
|
||||
|
||||
@[YAML::Field(ignore: true)]
|
||||
private getter location : Time::Location {
|
||||
Time::Location.load(self.timezone)
|
||||
}
|
||||
|
||||
def test(source)
|
||||
Tokenizer.new(source).run do |token|
|
||||
next unless token.type.comment?
|
||||
next unless doc = token.value.to_s
|
||||
|
||||
pattern =
|
||||
/^#\s*(?<admonition>#{Regex.union(admonitions)})(?:\((?<context>.+?)\))?(?:\W+|$)/m
|
||||
|
||||
matches = doc.scan(pattern)
|
||||
matches.each do |match|
|
||||
admonition = match["admonition"]
|
||||
begin
|
||||
case expr = match["context"]?.presence
|
||||
when /\A\d{4}-\d{2}-\d{2}\Z/ # date
|
||||
# ameba:disable Lint/NotNil
|
||||
date = Time.parse(expr.not_nil!, "%F", location)
|
||||
issue_for_date source, token, admonition, date
|
||||
when /\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?\Z/ # date + time (no tz)
|
||||
# ameba:disable Lint/NotNil
|
||||
date = Time.parse(expr.not_nil!, "%F #{$1?.presence ? "%T" : "%R"}", location)
|
||||
issue_for_date source, token, admonition, date
|
||||
else
|
||||
issue_for token, MSG % admonition
|
||||
end
|
||||
rescue ex
|
||||
issue_for token, MSG_ERR % {admonition, "#{ex}: #{expr.inspect}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private def issue_for_date(source, node, admonition, date)
|
||||
diff = Time.utc - date.to_utc
|
||||
|
||||
return if diff.negative?
|
||||
|
||||
past = case diff
|
||||
when 0.seconds..1.day then "today is the day!"
|
||||
when 1.day..2.days then "1 day past"
|
||||
else "#{diff.total_days.to_i} days past"
|
||||
end
|
||||
|
||||
issue_for node, MSG_LATE % {admonition, past}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -28,7 +28,7 @@ module Ameba::Rule::Lint
|
|||
end
|
||||
|
||||
MSG = "Comparison to a boolean is pointless"
|
||||
OP_NAMES = %w(== != ===)
|
||||
OP_NAMES = %w[== != ===]
|
||||
|
||||
def test(source, node : Crystal::Call)
|
||||
return unless node.name.in?(OP_NAMES)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Ameba::Rule::Lint
|
|||
class DebugCalls < Base
|
||||
properties do
|
||||
description "Disallows debug-related calls"
|
||||
method_names %w(p p! pp pp!)
|
||||
method_names %w[p p! pp pp!]
|
||||
end
|
||||
|
||||
MSG = "Possibly forgotten debug-related `%s` call detected"
|
||||
|
|
|
@ -28,8 +28,6 @@ module Ameba::Rule::Lint
|
|||
# Enabled: true
|
||||
# ```
|
||||
class EmptyExpression < Base
|
||||
include AST::Util
|
||||
|
||||
properties do
|
||||
description "Disallows empty expressions"
|
||||
end
|
||||
|
|
|
@ -23,7 +23,7 @@ module Ameba::Rule::Lint
|
|||
description "Identifies comparisons between literals"
|
||||
end
|
||||
|
||||
OP_NAMES = %w(=== == !=)
|
||||
OP_NAMES = %w[=== == !=]
|
||||
|
||||
MSG = "Comparison always evaluates to %s"
|
||||
MSG_LIKELY = "Comparison most likely evaluates to %s"
|
||||
|
@ -36,14 +36,15 @@ module Ameba::Rule::Lint
|
|||
arg_is_literal, arg_is_static = literal_kind?(arg)
|
||||
|
||||
return unless obj_is_literal && arg_is_literal
|
||||
return unless obj.to_s == arg.to_s
|
||||
|
||||
is_dynamic = !obj_is_static || !arg_is_static
|
||||
|
||||
what =
|
||||
case node.name
|
||||
when "===" then "the same"
|
||||
when "==" then (obj.to_s == arg.to_s).to_s
|
||||
when "!=" then (obj.to_s != arg.to_s).to_s
|
||||
when "==" then "true"
|
||||
when "!=" then "false"
|
||||
end
|
||||
|
||||
issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what
|
||||
|
|
|
@ -20,8 +20,6 @@ module Ameba::Rule::Lint
|
|||
# Enabled: true
|
||||
# ```
|
||||
class MissingBlockArgument < Base
|
||||
include AST::Util
|
||||
|
||||
properties do
|
||||
description "Disallows yielding method definitions without block argument"
|
||||
end
|
||||
|
@ -36,10 +34,7 @@ module Ameba::Rule::Lint
|
|||
def test(source, node : Crystal::Def, scope : AST::Scope)
|
||||
return if !scope.yields? || node.block_arg
|
||||
|
||||
return unless location = node.name_location
|
||||
end_location = name_end_location(node)
|
||||
|
||||
issue_for location, end_location, MSG
|
||||
issue_for node, MSG, prefer_name_location: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,27 +26,21 @@ module Ameba::Rule::Lint
|
|||
# Enabled: true
|
||||
# ```
|
||||
class NotNil < Base
|
||||
include AST::Util
|
||||
|
||||
properties do
|
||||
description "Identifies usage of `not_nil!` calls"
|
||||
end
|
||||
|
||||
NOT_NIL_NAME = "not_nil!"
|
||||
MSG = "Avoid using `not_nil!`"
|
||||
MSG = "Avoid using `not_nil!`"
|
||||
|
||||
def test(source)
|
||||
AST::NodeVisitor.new self, source, skip: :macro
|
||||
end
|
||||
|
||||
def test(source, node : Crystal::Call)
|
||||
return unless node.name == NOT_NIL_NAME
|
||||
return unless node.name == "not_nil!"
|
||||
return unless node.obj && node.args.empty?
|
||||
|
||||
return unless name_location = node.name_location
|
||||
return unless end_location = name_end_location(node)
|
||||
|
||||
issue_for name_location, end_location, MSG
|
||||
issue_for node, MSG, prefer_name_location: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
module Ameba::Rule::Lint
|
||||
# This rule is used to identify usage of `index/rindex/find` calls
|
||||
# This rule is used to identify usage of `index/rindex/find/match` calls
|
||||
# followed by a call to `not_nil!`.
|
||||
#
|
||||
# For example, this is considered a code smell:
|
||||
#
|
||||
# ```
|
||||
# %w[Alice Bob].find(&.match(/^A./)).not_nil!
|
||||
# %w[Alice Bob].find(&.chars.any?(&.in?('o', 'b'))).not_nil!
|
||||
# ```
|
||||
#
|
||||
# And can be written as this:
|
||||
#
|
||||
# ```
|
||||
# %w[Alice Bob].find!(&.match(/^A./))
|
||||
# %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b')))
|
||||
# ```
|
||||
#
|
||||
# YAML configuration example:
|
||||
|
@ -24,25 +24,24 @@ module Ameba::Rule::Lint
|
|||
include AST::Util
|
||||
|
||||
properties do
|
||||
description "Identifies usage of `index/rindex/find` calls followed by `not_nil!`"
|
||||
description "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`"
|
||||
end
|
||||
|
||||
BLOCK_CALL_NAMES = %w(index rindex find)
|
||||
CALL_NAMES = %w(index rindex)
|
||||
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
|
||||
|
||||
NOT_NIL_NAME = "not_nil!"
|
||||
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
|
||||
BLOCK_CALL_NAMES = %w[index rindex find]
|
||||
CALL_NAMES = %w[index rindex match]
|
||||
|
||||
def test(source)
|
||||
AST::NodeVisitor.new self, source, skip: :macro
|
||||
end
|
||||
|
||||
def test(source, node : Crystal::Call)
|
||||
return unless node.name == NOT_NIL_NAME && node.args.empty?
|
||||
return unless node.name == "not_nil!" && node.args.empty?
|
||||
return unless (obj = node.obj).is_a?(Crystal::Call)
|
||||
return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES)
|
||||
|
||||
return unless name_location = obj.name_location
|
||||
return unless name_location = name_location(obj)
|
||||
return unless name_location_end = name_end_location(obj)
|
||||
return unless end_location = name_end_location(node)
|
||||
|
||||
|
|
|
@ -4,15 +4,15 @@ module Ameba::Rule::Lint
|
|||
# For example, this is usually written by mistake:
|
||||
#
|
||||
# ```
|
||||
# %i(:one, :two)
|
||||
# %w("one", "two")
|
||||
# %i[:one, :two]
|
||||
# %w["one", "two"]
|
||||
# ```
|
||||
#
|
||||
# And the expected example is:
|
||||
#
|
||||
# ```
|
||||
# %i(one two)
|
||||
# %w(one two)
|
||||
# %i[one two]
|
||||
# %w[one two]
|
||||
# ```
|
||||
#
|
||||
# YAML configuration example:
|
||||
|
@ -42,7 +42,7 @@ module Ameba::Rule::Lint
|
|||
start_token = token.dup
|
||||
when .string?
|
||||
if (_start = start_token) && !issue
|
||||
issue = array_entry_invalid?(token.value, _start.raw)
|
||||
issue = array_entry_invalid?(token.value.to_s, _start.raw)
|
||||
end
|
||||
when .string_array_end?
|
||||
if (_start = start_token) && (_issue = issue)
|
||||
|
@ -63,7 +63,7 @@ module Ameba::Rule::Lint
|
|||
end
|
||||
|
||||
private def check_array_entry(entry, symbols, literal)
|
||||
MSG % {symbols, literal} if entry =~ /[#{symbols}]/
|
||||
MSG % {symbols, literal} if entry.matches?(/[#{Regex.escape(symbols)}]/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,8 +30,8 @@ module Ameba::Rule::Lint
|
|||
MSG = "Redundant use of `Object#to_s` in interpolation"
|
||||
|
||||
def test(source, node : Crystal::StringInterpolation)
|
||||
string_coercion_nodes(node).each do |n|
|
||||
issue_for n.name_location, n.end_location, MSG
|
||||
string_coercion_nodes(node).each do |expr|
|
||||
issue_for name_location(expr), expr.end_location, MSG
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ module Ameba::Rule::Lint
|
|||
end
|
||||
|
||||
private def report(source, node, msg)
|
||||
issue_for node.name_location, node.name_end_location, msg
|
||||
issue_for node, msg, prefer_name_location: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,7 +40,7 @@ module Ameba::Rule::Lint
|
|||
!(block = node.block) ||
|
||||
with_index_arg?(block)
|
||||
|
||||
issue_for node.name_location, node.name_end_location, MSG
|
||||
issue_for node, MSG, prefer_name_location: true
|
||||
end
|
||||
|
||||
private def with_index_arg?(block : Crystal::Block)
|
||||
|
|
|
@ -53,13 +53,16 @@ module Ameba::Rule::Lint
|
|||
return unless outer_scope = scope.outer_scope
|
||||
|
||||
scope.arguments.reject(&.ignored?).each do |arg|
|
||||
variable = outer_scope.find_variable(arg.name)
|
||||
# TODO: handle unpacked variables from `Block#unpacks`
|
||||
next unless name = arg.name.presence
|
||||
|
||||
variable = outer_scope.find_variable(name)
|
||||
|
||||
next if variable.nil? || !variable.declared_before?(arg)
|
||||
next if outer_scope.assigns_ivar?(arg.name)
|
||||
next if outer_scope.assigns_type_dec?(arg.name)
|
||||
next if outer_scope.assigns_ivar?(name)
|
||||
next if outer_scope.assigns_type_dec?(name)
|
||||
|
||||
issue_for arg.node, MSG % arg.name
|
||||
issue_for arg.node, MSG % name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
require "file_utils"
|
||||
|
||||
module Ameba::Rule::Lint
|
||||
# A rule that enforces spec filenames to have `_spec` suffix.
|
||||
#
|
||||
# YAML configuration example:
|
||||
#
|
||||
# ```
|
||||
# Lint/SpecFilename:
|
||||
# Enabled: true
|
||||
# ```
|
||||
class SpecFilename < Base
|
||||
properties do
|
||||
description "Enforces spec filenames to have `_spec` suffix"
|
||||
ignored_dirs %w[spec/support spec/fixtures spec/data]
|
||||
ignored_filenames %w[spec_helper]
|
||||
end
|
||||
|
||||
MSG = "Spec filename should have `_spec` suffix: %s.cr, not %s.cr"
|
||||
|
||||
private LOCATION = {1, 1}
|
||||
|
||||
# TODO: fix the assumption that *source.path* contains relative path
|
||||
def test(source : Source)
|
||||
path_ = Path[source.path].to_posix
|
||||
name = path_.stem
|
||||
path = path_.to_s
|
||||
|
||||
# check files only within spec/ directory
|
||||
return unless path.starts_with?("spec/")
|
||||
# ignore files having `_spec` suffix
|
||||
return if name.ends_with?("_spec")
|
||||
|
||||
# ignore known false-positives
|
||||
ignored_dirs.each do |substr|
|
||||
return if path.starts_with?("#{substr}/")
|
||||
end
|
||||
return if name.in?(ignored_filenames)
|
||||
|
||||
expected = "#{name}_spec"
|
||||
|
||||
issue_for LOCATION, MSG % {expected, name} do
|
||||
new_path =
|
||||
path_.sibling(expected + path_.extension)
|
||||
|
||||
FileUtils.mv(path, new_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -49,8 +49,9 @@ module Ameba::Rule::Lint
|
|||
description "Reports focused spec items"
|
||||
end
|
||||
|
||||
MSG = "Focused spec item detected"
|
||||
SPEC_ITEM_NAMES = %w(describe context it pending)
|
||||
MSG = "Focused spec item detected"
|
||||
|
||||
SPEC_ITEM_NAMES = %w[describe context it pending]
|
||||
|
||||
def test(source)
|
||||
return unless source.spec?
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
module Ameba::Rule::Lint
|
||||
# A rule that reports typos found in source files.
|
||||
#
|
||||
# NOTE: Needs [typos](https://github.com/crate-ci/typos) CLI tool.
|
||||
# NOTE: See the chapter on [false positives](https://github.com/crate-ci/typos#false-positives).
|
||||
#
|
||||
# YAML configuration example:
|
||||
#
|
||||
# ```
|
||||
# Lint/Typos:
|
||||
# Enabled: true
|
||||
# BinPath: ~
|
||||
# FailOnError: false
|
||||
# ```
|
||||
class Typos < Base
|
||||
properties do
|
||||
description "Reports typos found in source files"
|
||||
|
||||
bin_path nil, as: String?
|
||||
fail_on_error false
|
||||
end
|
||||
|
||||
MSG = "Typo found: %s -> %s"
|
||||
|
||||
BIN_PATH = Process.find_executable("typos")
|
||||
|
||||
def bin_path : String?
|
||||
@bin_path || BIN_PATH
|
||||
end
|
||||
|
||||
def test(source : Source)
|
||||
typos = typos_from(source)
|
||||
typos.try &.each do |typo|
|
||||
corrections = typo.corrections
|
||||
message = MSG % {
|
||||
typo.typo, corrections.join(" | "),
|
||||
}
|
||||
if corrections.size == 1
|
||||
issue_for typo.location, typo.end_location, message do |corrector|
|
||||
corrector.replace(typo.location, typo.end_location, corrections.first)
|
||||
end
|
||||
else
|
||||
issue_for typo.location, typo.end_location, message
|
||||
end
|
||||
end
|
||||
rescue ex
|
||||
raise ex if fail_on_error?
|
||||
end
|
||||
|
||||
private record Typo,
|
||||
path : String,
|
||||
typo : String,
|
||||
corrections : Array(String),
|
||||
location : {Int32, Int32},
|
||||
end_location : {Int32, Int32} do
|
||||
def self.parse(str) : self?
|
||||
issue = JSON.parse(str)
|
||||
|
||||
return unless issue["type"] == "typo"
|
||||
|
||||
typo = issue["typo"].as_s
|
||||
corrections = issue["corrections"].as_a.map(&.as_s)
|
||||
|
||||
return if typo.empty? || corrections.empty?
|
||||
|
||||
path = issue["path"].as_s
|
||||
line_no = issue["line_num"].as_i
|
||||
col_no = issue["byte_offset"].as_i + 1
|
||||
end_col_no = col_no + typo.size - 1
|
||||
|
||||
new(path, typo, corrections,
|
||||
{line_no, col_no}, {line_no, end_col_no})
|
||||
end
|
||||
end
|
||||
|
||||
protected def typos_from(source : Source) : Array(Typo)?
|
||||
unless bin_path = self.bin_path
|
||||
if fail_on_error?
|
||||
raise RuntimeError.new "Could not find `typos` executable"
|
||||
end
|
||||
return
|
||||
end
|
||||
status = Process.run(bin_path, args: %w[--format json -],
|
||||
input: IO::Memory.new(source.code),
|
||||
output: output = IO::Memory.new,
|
||||
)
|
||||
return if status.success?
|
||||
|
||||
([] of Typo).tap do |typos|
|
||||
# NOTE: `--format json` is actually JSON Lines (`jsonl`)
|
||||
output.to_s.each_line do |line|
|
||||
Typo.parse(line).try { |typo| typos << typo }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -42,8 +42,6 @@ module Ameba::Rule::Lint
|
|||
# Enabled: true
|
||||
# ```
|
||||
class UnreachableCode < Base
|
||||
include AST::Util
|
||||
|
||||
properties do
|
||||
description "Reports unreachable code"
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue