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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
# Login against a Docker registry except on PR
|
||||||
# https://github.com/docker/login-action
|
# https://github.com/docker/login-action
|
||||||
- name: Log into ${{ env.REGISTRY }} registry
|
- name: Log into ${{ env.REGISTRY }} registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
@ -45,7 +45,7 @@ jobs:
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
@ -61,7 +61,7 @@ jobs:
|
||||||
# https://github.com/docker/build-push-action
|
# https://github.com/docker/build-push-action
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -18,17 +18,24 @@ jobs:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Set timezone to UTC
|
||||||
|
uses: szenius/set-timezone@v1.2
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1
|
uses: crystal-lang/install-crystal@v1
|
||||||
with:
|
with:
|
||||||
crystal: ${{ matrix.crystal }}
|
crystal: ${{ matrix.crystal }}
|
||||||
|
|
||||||
- name: Download source
|
- name: Download source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: shards install
|
run: shards install
|
||||||
|
|
||||||
|
- name: Install typos-cli
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: brew install typos-cli
|
||||||
|
|
||||||
- name: Run specs
|
- name: Run specs
|
||||||
run: crystal spec
|
run: crystal spec
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
uses: crystal-lang/install-crystal@v1
|
uses: crystal-lang/install-crystal@v1
|
||||||
|
|
||||||
- name: Download source
|
- name: Download source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: shards install
|
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
|
CRYSTAL_BIN ?= crystal
|
||||||
|
# The shards command to use
|
||||||
SHARDS_BIN ?= shards
|
SHARDS_BIN ?= shards
|
||||||
PREFIX ?= /usr/local
|
# The install command to use
|
||||||
|
INSTALL_BIN ?= /usr/bin/install
|
||||||
|
|
||||||
SHARD_BIN ?= ../../bin
|
SHARD_BIN ?= ../../bin
|
||||||
CRFLAGS ?= -Dpreview_mt
|
CRFLAGS ?= -Dpreview_mt
|
||||||
|
|
||||||
|
SRC_SOURCES ::= $(shell find src -name '*.cr' 2>/dev/null)
|
||||||
|
DOC_SOURCE ::= src/**
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: build
|
||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build:
|
build: ## Build the application binary
|
||||||
|
build: $(BUILD_TARGET)
|
||||||
|
|
||||||
|
$(BUILD_TARGET): $(SRC_SOURCES)
|
||||||
$(SHARDS_BIN) build $(CRFLAGS)
|
$(SHARDS_BIN) build $(CRFLAGS)
|
||||||
|
|
||||||
|
docs: ## Generate API docs
|
||||||
|
docs: $(SRC_SOURCES)
|
||||||
|
$(CRYSTAL_BIN) docs -o docs $(DOC_SOURCE)
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: build
|
lint: ## Run ameba on ameba's code base
|
||||||
./bin/ameba
|
lint: $(BUILD_TARGET)
|
||||||
|
$(BUILD_TARGET)
|
||||||
|
|
||||||
.PHONY: spec
|
.PHONY: spec
|
||||||
|
spec: ## Run the spec suite
|
||||||
spec:
|
spec:
|
||||||
$(CRYSTAL_BIN) spec
|
$(CRYSTAL_BIN) spec
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
clean: ## Remove application binary
|
||||||
clean:
|
clean:
|
||||||
rm -f ./bin/ameba ./bin/ameba.dwarf
|
@rm -f "$(BUILD_TARGET)" "$(BUILD_TARGET).dwarf"
|
||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: build
|
install: ## Install application binary into $DESTDIR
|
||||||
mkdir -p $(PREFIX)/bin
|
install: $(BUILD_TARGET)
|
||||||
cp ./bin/ameba $(PREFIX)/bin
|
$(INSTALL_BIN) -m 0755 "$(BUILD_TARGET)" "$(BINDIR)/ameba"
|
||||||
|
|
||||||
.PHONY: bin
|
.PHONY: bin
|
||||||
bin: build
|
bin: build
|
||||||
mkdir -p $(SHARD_BIN)
|
mkdir -p $(SHARD_BIN)
|
||||||
cp ./bin/ameba $(SHARD_BIN)
|
cp $(BUILD_TARGET) $(SHARD_BIN)
|
||||||
|
|
||||||
.PHONY: run_file
|
|
||||||
run_file:
|
|
||||||
cp -n ./bin/ameba.cr $(SHARD_BIN) || true
|
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
test: ## Run the spec suite and linter
|
||||||
test: spec lint
|
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>
|
</sup>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<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/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>
|
<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>
|
</p>
|
||||||
|
@ -118,7 +118,6 @@ Add this to your application's `shard.yml`:
|
||||||
development_dependencies:
|
development_dependencies:
|
||||||
ameba:
|
ameba:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
version: ~> 1.4.0
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Build `bin/ameba` binary within your project directory while running `shards install`.
|
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:**
|
**List of sources to run Ameba on can be configured globally via:**
|
||||||
|
|
||||||
- `Globs` section - an array of wildcards (or paths) to include to the
|
- `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.
|
files with `*.cr` extension except those which exist in `lib` folder.
|
||||||
- `Excluded` section - an array of wildcards (or paths) to exclude from the
|
- `Excluded` section - an array of wildcards (or paths) to exclude from the
|
||||||
source list defined by `Globs`. Defaults to an empty array.
|
source list defined by `Globs`. Defaults to an empty array.
|
||||||
|
@ -186,8 +185,8 @@ Excluded:
|
||||||
``` yaml
|
``` yaml
|
||||||
Style/RedundantBegin:
|
Style/RedundantBegin:
|
||||||
Excluded:
|
Excluded:
|
||||||
- src/server/processor.cr
|
- src/server/processor.cr
|
||||||
- src/server/api.cr
|
- src/server/api.cr
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
@ -240,4 +239,4 @@ time = Time.epoch(1483859302) # ameba:disable Style, Lint
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
- [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer
|
- [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,
|
20,
|
||||||
30,
|
30,
|
||||||
40,
|
40,
|
||||||
].each do |n|
|
].each do |n| # ameba:disable Naming/BlockParameterName
|
||||||
config = Ameba::Config.load
|
config = Ameba::Config.load
|
||||||
config.formatter = Ameba::Formatter::BaseFormatter.new
|
config.formatter = Ameba::Formatter::BaseFormatter.new
|
||||||
config.globs = get_files(n)
|
config.globs = get_files(n)
|
||||||
|
|
10
shard.yml
10
shard.yml
|
@ -1,20 +1,22 @@
|
||||||
name: ameba
|
name: ameba
|
||||||
version: 1.4.3
|
version: 1.6.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Vitalii Elenhaupt <velenhaupt@gmail.com>
|
- Vitalii Elenhaupt <velenhaupt@gmail.com>
|
||||||
|
- Sijawusz Pur Rahnama <sija@sija.pl>
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
ameba:
|
ameba:
|
||||||
main: src/cli.cr
|
main: src/cli.cr
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
# TODO: remove pre-compiled executable in future releases
|
postinstall: shards build -Dpreview_mt
|
||||||
postinstall: make bin && make run_file
|
|
||||||
|
|
||||||
|
# TODO: remove pre-compiled executable in future releases
|
||||||
executables:
|
executables:
|
||||||
- ameba
|
- ameba
|
||||||
|
- ameba.cr
|
||||||
|
|
||||||
crystal: "~> 1.7.0"
|
crystal: ~> 1.10
|
||||||
|
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
|
@ -298,6 +298,34 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
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
|
describe "#initialize" do
|
||||||
it "creates new branch" do
|
it "creates new branch" do
|
||||||
nodes = as_nodes <<-CRYSTAL
|
nodes = as_nodes <<-CRYSTAL
|
||||||
|
@ -358,6 +386,30 @@ module Ameba::AST
|
||||||
branch = Branch.new nodes.assign_nodes.first, branchable
|
branch = Branch.new nodes.assign_nodes.first, branchable
|
||||||
branch.in_loop?.should be_false
|
branch.in_loop?.should be_false
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,13 +57,15 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
scope = Scope.new nodes.def_nodes.first
|
|
||||||
var_node = nodes.var_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)
|
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||||
|
|
||||||
variable = Variable.new(var_node, 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
|
scope.references?(variable).should be_true
|
||||||
end
|
end
|
||||||
|
@ -77,13 +79,15 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
scope = Scope.new nodes.def_nodes.first
|
|
||||||
var_node = nodes.var_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)
|
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||||
|
|
||||||
variable = Variable.new(var_node, 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
|
scope.references?(variable, check_inner_scopes: false).should be_false
|
||||||
end
|
end
|
||||||
|
@ -98,9 +102,11 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
scope = Scope.new nodes.def_nodes.first
|
|
||||||
var_node = nodes.var_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)
|
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||||
|
|
||||||
variable = Variable.new(var_node, scope)
|
variable = Variable.new(var_node, scope)
|
||||||
|
@ -120,7 +126,7 @@ module Ameba::AST
|
||||||
describe "#find_variable" do
|
describe "#find_variable" do
|
||||||
it "returns the variable in the scope by name" do
|
it "returns the variable in the scope by name" do
|
||||||
scope = Scope.new as_node("foo = 1")
|
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
|
scope.find_variable("foo").should_not be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,7 +139,7 @@ module Ameba::AST
|
||||||
describe "#assign_variable" do
|
describe "#assign_variable" do
|
||||||
it "creates a new assignment" do
|
it "creates a new assignment" do
|
||||||
scope = Scope.new as_node("foo = 1")
|
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")
|
scope.assign_variable("foo", Crystal::Var.new "foo")
|
||||||
var = scope.find_variable("foo").should_not be_nil
|
var = scope.find_variable("foo").should_not be_nil
|
||||||
var.assignments.size.should eq 1
|
var.assignments.size.should eq 1
|
||||||
|
@ -141,7 +147,7 @@ module Ameba::AST
|
||||||
|
|
||||||
it "does not create the assignment if variable is wrong" do
|
it "does not create the assignment if variable is wrong" do
|
||||||
scope = Scope.new as_node("foo = 1")
|
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")
|
scope.assign_variable("bar", Crystal::Var.new "bar")
|
||||||
var = scope.find_variable("foo").should_not be_nil
|
var = scope.find_variable("foo").should_not be_nil
|
||||||
var.assignments.size.should eq 0
|
var.assignments.size.should eq 0
|
||||||
|
@ -174,6 +180,28 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
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
|
describe "#in_macro?" do
|
||||||
it "returns true if Crystal::Macro" do
|
it "returns true if Crystal::Macro" do
|
||||||
nodes = as_nodes <<-CRYSTAL
|
nodes = as_nodes <<-CRYSTAL
|
||||||
|
|
|
@ -36,6 +36,43 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
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
|
describe "#node_source" do
|
||||||
it "returns original source of the node" do
|
it "returns original source of the node" do
|
||||||
s = <<-CRYSTAL
|
s = <<-CRYSTAL
|
||||||
|
|
|
@ -85,30 +85,5 @@ module Ameba::AST
|
||||||
assignment.branch.should be_nil
|
assignment.branch.should be_nil
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,13 +85,16 @@ module Ameba::AST
|
||||||
3.times { |i| a = a + i }
|
3.times { |i| a = a + i }
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
scope = Scope.new nodes.def_nodes.first
|
|
||||||
var_node = nodes.var_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)
|
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
|
||||||
|
|
||||||
variable = Variable.new(var_node, 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
|
variable.captured_by_block?.should be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -101,8 +104,10 @@ module Ameba::AST
|
||||||
a = 1
|
a = 1
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
scope.add_variable Crystal::Var.new "a"
|
|
||||||
|
scope.add_variable(Crystal::Var.new "a")
|
||||||
variable = scope.variables.first
|
variable = scope.variables.first
|
||||||
|
|
||||||
variable.captured_by_block?.should be_falsey
|
variable.captured_by_block?.should be_falsey
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
require "../../../spec_helper"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
source = Source.new ""
|
|
||||||
|
|
||||||
describe FlowExpressionVisitor do
|
describe FlowExpressionVisitor do
|
||||||
it "creates an expression for return" do
|
it "creates an expression for return" do
|
||||||
rule = FlowExpressionRule.new
|
rule = FlowExpressionRule.new
|
||||||
|
|
|
@ -10,14 +10,16 @@ module Ameba::Rule
|
||||||
end
|
end
|
||||||
|
|
||||||
it "contains rules across all the available groups" do
|
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
|
Ameba
|
||||||
|
Documentation
|
||||||
Layout
|
Layout
|
||||||
Lint
|
Lint
|
||||||
Metrics
|
Metrics
|
||||||
|
Naming
|
||||||
Performance
|
Performance
|
||||||
Style
|
Style
|
||||||
)
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,25 +50,25 @@ module Ameba::Rule
|
||||||
|
|
||||||
it "returns false if source is not excluded from this rule" do
|
it "returns false if source is not excluded from this rule" do
|
||||||
rule = DummyRule.new
|
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
|
rule.excluded?(Source.new "", "another_source.cr").should_not be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns true if source is excluded from this rule" do
|
it "returns true if source is excluded from this rule" do
|
||||||
rule = DummyRule.new
|
rule = DummyRule.new
|
||||||
rule.excluded = %w(source.cr)
|
rule.excluded = %w[source.cr]
|
||||||
rule.excluded?(Source.new "", "source.cr").should be_true
|
rule.excluded?(Source.new "", "source.cr").should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns true if source matches the wildcard" do
|
it "returns true if source matches the wildcard" do
|
||||||
rule = DummyRule.new
|
rule = DummyRule.new
|
||||||
rule.excluded = %w(**/*.cr)
|
rule.excluded = %w[**/*.cr]
|
||||||
rule.excluded?(Source.new "", __FILE__).should be_true
|
rule.excluded?(Source.new "", __FILE__).should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns false if source does not match the wildcard" do
|
it "returns false if source does not match the wildcard" do
|
||||||
rule = DummyRule.new
|
rule = DummyRule.new
|
||||||
rule.excluded = %w(*_spec.cr)
|
rule.excluded = %w[*_spec.cr]
|
||||||
rule.excluded?(Source.new "", "source.cr").should be_false
|
rule.excluded?(Source.new "", "source.cr").should be_false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,97 +5,97 @@ module Ameba::Cli
|
||||||
describe "Cmd" do
|
describe "Cmd" do
|
||||||
describe ".run" do
|
describe ".run" do
|
||||||
it "runs ameba" 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
|
r.should be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".parse_args" do
|
describe ".parse_args" do
|
||||||
%w(-s --silent).each do |f|
|
%w[-s --silent].each do |flag|
|
||||||
it "accepts #{f} flag" do
|
it "accepts #{flag} flag" do
|
||||||
c = Cli.parse_args [f]
|
c = Cli.parse_args [flag]
|
||||||
c.formatter.should eq :silent
|
c.formatter.should eq :silent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
%w(-c --config).each do |f|
|
%w[-c --config].each do |flag|
|
||||||
it "accepts #{f} flag" do
|
it "accepts #{flag} flag" do
|
||||||
c = Cli.parse_args [f, "config.yml"]
|
c = Cli.parse_args [flag, "config.yml"]
|
||||||
c.config.should eq Path["config.yml"]
|
c.config.should eq Path["config.yml"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
%w(-f --format).each do |f|
|
%w[-f --format].each do |flag|
|
||||||
it "accepts #{f} flag" do
|
it "accepts #{flag} flag" do
|
||||||
c = Cli.parse_args [f, "my-formatter"]
|
c = Cli.parse_args [flag, "my-formatter"]
|
||||||
c.formatter.should eq "my-formatter"
|
c.formatter.should eq "my-formatter"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --only flag" do
|
it "accepts --only flag" do
|
||||||
c = Cli.parse_args ["--only", "RULE1,RULE2"]
|
c = Cli.parse_args ["--only", "RULE1,RULE2"]
|
||||||
c.only.should eq %w(RULE1 RULE2)
|
c.only.should eq %w[RULE1 RULE2]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --except flag" do
|
it "accepts --except flag" do
|
||||||
c = Cli.parse_args ["--except", "RULE1,RULE2"]
|
c = Cli.parse_args ["--except", "RULE1,RULE2"]
|
||||||
c.except.should eq %w(RULE1 RULE2)
|
c.except.should eq %w[RULE1 RULE2]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "defaults rules? flag to false" do
|
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
|
c.rules?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "defaults skip_reading_config? flag to false" do
|
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
|
c.skip_reading_config?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --rules flag" do
|
it "accepts --rules flag" do
|
||||||
c = Cli.parse_args %w(--rules)
|
c = Cli.parse_args %w[--rules]
|
||||||
c.rules?.should eq true
|
c.rules?.should eq true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "defaults all? flag to false" do
|
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
|
c.all?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --all flag" do
|
it "accepts --all flag" do
|
||||||
c = Cli.parse_args %w(--all)
|
c = Cli.parse_args %w[--all]
|
||||||
c.all?.should eq true
|
c.all?.should eq true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --gen-config flag" do
|
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
|
c.formatter.should eq :todo
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --no-color flag" do
|
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
|
c.colors?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts --without-affected-code flag" do
|
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
|
c.without_affected_code?.should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't disable colors by default" do
|
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
|
c.colors?.should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
it "ignores --config if --gen-config flag passed" do
|
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.formatter.should eq :todo
|
||||||
c.skip_reading_config?.should be_true
|
c.skip_reading_config?.should be_true
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "-e/--explain" do
|
describe "-e/--explain" do
|
||||||
it "configures file/line/column" 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 = c.location_to_explain.should_not be_nil
|
||||||
location_to_explain[:file].should eq "src/file.cr"
|
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
|
it "raises an error if location is not valid" do
|
||||||
expect_raises(Exception, "location should have PATH:line:column") 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
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises an error if line number is not valid" do
|
it "raises an error if line number is not valid" do
|
||||||
expect_raises(Exception, "location should have PATH:line:column") 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
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises an error if column number is not valid" do
|
it "raises an error if column number is not valid" do
|
||||||
expect_raises(Exception, "location should have PATH:line:column") 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
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises an error if line/column are missing" do
|
it "raises an error if line/column are missing" do
|
||||||
expect_raises(Exception, "location should have PATH:line:column") 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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "--fail-level" do
|
context "--fail-level" do
|
||||||
it "configures fail level Convention" 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
|
c.fail_level.should eq Severity::Convention
|
||||||
end
|
end
|
||||||
|
|
||||||
it "configures fail level Warning" do
|
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
|
c.fail_level.should eq Severity::Warning
|
||||||
end
|
end
|
||||||
|
|
||||||
it "configures fail level Error" do
|
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
|
c.fail_level.should eq Severity::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises if fail level is incorrect" do
|
it "raises if fail level is incorrect" do
|
||||||
expect_raises(Exception, "Incorrect severity name JohnDoe") 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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts unknown args as globs" do
|
it "accepts unknown args as globs" do
|
||||||
c = Cli.parse_args %w(source1.cr source2.cr)
|
c = Cli.parse_args %w[source1.cr source2.cr]
|
||||||
c.globs.should eq %w(source1.cr source2.cr)
|
c.globs.should eq %w[source1.cr source2.cr]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "accepts one unknown arg as explain location if it has correct format" do
|
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 = c.location_to_explain.should_not be_nil
|
||||||
location_to_explain[:file].should eq "source.cr"
|
location_to_explain[:file].should eq "source.cr"
|
||||||
|
|
|
@ -2,7 +2,7 @@ require "../spec_helper"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
describe Config do
|
describe Config do
|
||||||
config_sample = "config/ameba.yml"
|
config_sample = "spec/fixtures/config.yml"
|
||||||
|
|
||||||
it "should have a list of available formatters" do
|
it "should have a list of available formatters" do
|
||||||
Config::AVAILABLE_FORMATTERS.should_not be_nil
|
Config::AVAILABLE_FORMATTERS.should_not be_nil
|
||||||
|
@ -21,7 +21,7 @@ module Ameba
|
||||||
Globs: src/*.cr
|
Globs: src/*.cr
|
||||||
CONFIG
|
CONFIG
|
||||||
config = Config.new(yml)
|
config = Config.new(yml)
|
||||||
config.globs.should eq %w(src/*.cr)
|
config.globs.should eq %w[src/*.cr]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "initializes globs as array" do
|
it "initializes globs as array" do
|
||||||
|
@ -32,7 +32,7 @@ module Ameba
|
||||||
- "!spec"
|
- "!spec"
|
||||||
CONFIG
|
CONFIG
|
||||||
config = Config.new(yml)
|
config = Config.new(yml)
|
||||||
config.globs.should eq %w(src/*.cr !spec)
|
config.globs.should eq %w[src/*.cr !spec]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises if Globs has a wrong type" do
|
it "raises if Globs has a wrong type" do
|
||||||
|
@ -51,7 +51,7 @@ module Ameba
|
||||||
Excluded: spec
|
Excluded: spec
|
||||||
CONFIG
|
CONFIG
|
||||||
config = Config.new(yml)
|
config = Config.new(yml)
|
||||||
config.excluded.should eq %w(spec)
|
config.excluded.should eq %w[spec]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "initializes excluded as array" do
|
it "initializes excluded as array" do
|
||||||
|
@ -62,7 +62,7 @@ module Ameba
|
||||||
- lib/*.cr
|
- lib/*.cr
|
||||||
CONFIG
|
CONFIG
|
||||||
config = Config.new(yml)
|
config = Config.new(yml)
|
||||||
config.excluded.should eq %w(spec lib/*.cr)
|
config.excluded.should eq %w[spec lib/*.cr]
|
||||||
end
|
end
|
||||||
|
|
||||||
it "raises if Excluded has a wrong type" do
|
it "raises if Excluded has a wrong type" do
|
||||||
|
@ -84,6 +84,12 @@ module Ameba
|
||||||
config.formatter.should_not be_nil
|
config.formatter.should_not be_nil
|
||||||
end
|
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
|
it "loads default config" do
|
||||||
config = Config.load
|
config = Config.load
|
||||||
config.should_not be_nil
|
config.should_not be_nil
|
||||||
|
@ -128,12 +134,12 @@ module Ameba
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a list of sources matching globs" do
|
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)
|
config.sources.size.should eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a list of sources excluding 'Excluded'" do
|
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
|
config.sources.any?(&.fullpath.==(__FILE__)).should be_false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -175,7 +181,7 @@ module Ameba
|
||||||
|
|
||||||
it "updates excluded property" do
|
it "updates excluded property" do
|
||||||
name = DummyRule.rule_name
|
name = DummyRule.rule_name
|
||||||
excluded = %w(spec/source.cr)
|
excluded = %w[spec/source.cr]
|
||||||
config.update_rule name, excluded: excluded
|
config.update_rule name, excluded: excluded
|
||||||
rule = config.rules.find!(&.name.== name)
|
rule = config.rules.find!(&.name.== name)
|
||||||
rule.excluded.should eq excluded
|
rule.excluded.should eq excluded
|
||||||
|
@ -194,7 +200,7 @@ module Ameba
|
||||||
|
|
||||||
it "updates multiple rules by excluded property" do
|
it "updates multiple rules by excluded property" do
|
||||||
name = DummyRule.rule_name
|
name = DummyRule.rule_name
|
||||||
excluded = %w(spec/source.cr)
|
excluded = %w[spec/source.cr]
|
||||||
config.update_rules [name], excluded: excluded
|
config.update_rules [name], excluded: excluded
|
||||||
rule = config.rules.find!(&.name.== name)
|
rule = config.rules.find!(&.name.== name)
|
||||||
rule.excluded.should eq excluded
|
rule.excluded.should eq excluded
|
||||||
|
@ -209,7 +215,7 @@ module Ameba
|
||||||
|
|
||||||
it "updates a group by excluded property" do
|
it "updates a group by excluded property" do
|
||||||
name = DummyRule.group_name
|
name = DummyRule.group_name
|
||||||
excluded = %w(spec/source.cr)
|
excluded = %w[spec/source.cr]
|
||||||
config.update_rules [name], excluded: excluded
|
config.update_rules [name], excluded: excluded
|
||||||
rule = config.rules.find!(&.name.== DummyRule.rule_name)
|
rule = config.rules.find!(&.name.== DummyRule.rule_name)
|
||||||
rule.excluded.should eq excluded
|
rule.excluded.should eq excluded
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
require "../../spec_helper"
|
require "../../spec_helper"
|
||||||
require "file_utils"
|
require "file_utils"
|
||||||
|
|
||||||
|
CONFIG_PATH = Path[Dir.tempdir] / Ameba::Config::FILENAME
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
private def with_formatter(&)
|
private def with_formatter(&)
|
||||||
io = IO::Memory.new
|
io = IO::Memory.new
|
||||||
formatter = Formatter::TODOFormatter.new(io)
|
formatter = Formatter::TODOFormatter.new(io, CONFIG_PATH)
|
||||||
|
|
||||||
yield formatter, io
|
yield formatter, io
|
||||||
end
|
end
|
||||||
|
@ -20,7 +22,7 @@ module Ameba
|
||||||
|
|
||||||
describe Formatter::TODOFormatter do
|
describe Formatter::TODOFormatter do
|
||||||
::Spec.after_each do
|
::Spec.after_each do
|
||||||
FileUtils.rm_rf(Ameba::Config::DEFAULT_PATH)
|
FileUtils.rm_rf(CONFIG_PATH)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "problems not found" do
|
context "problems not found" do
|
||||||
|
@ -45,7 +47,7 @@ module Ameba
|
||||||
s = Source.new "a = 1", "source.cr"
|
s = Source.new "a = 1", "source.cr"
|
||||||
s.add_issue DummyRule.new, {1, 2}, "message"
|
s.add_issue DummyRule.new, {1, 2}, "message"
|
||||||
formatter.finished([s])
|
formatter.finished([s])
|
||||||
io.to_s.should contain "Created #{Config::DEFAULT_PATH}"
|
io.to_s.should contain "Created #{CONFIG_PATH}"
|
||||||
end
|
end
|
||||||
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"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba::Rule::Lint
|
module Ameba::Rule::Documentation
|
||||||
subject = Documentation.new
|
subject = Documentation.new
|
||||||
.tap(&.ignore_classes = false)
|
.tap(&.ignore_classes = false)
|
||||||
.tap(&.ignore_modules = false)
|
.tap(&.ignore_modules = false)
|
|
@ -4,16 +4,16 @@ module Ameba
|
||||||
subject = Rule::Lint::EmptyExpression.new
|
subject = Rule::Lint::EmptyExpression.new
|
||||||
|
|
||||||
private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__)
|
private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__)
|
||||||
it %(detects empty expression "#{code}"), file, line do
|
it "detects empty expression #{code.inspect}", file, line do
|
||||||
s = Source.new code
|
source = Source.new code
|
||||||
rule = Rule::Lint::EmptyExpression.new
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Rule::Lint::EmptyExpression do
|
describe Rule::Lint::EmptyExpression do
|
||||||
it "passes if there is no empty expression" do
|
it "passes if there is no empty expression" do
|
||||||
s = Source.new <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
def method()
|
def method()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,7 +31,6 @@ module Ameba
|
||||||
begin "" end
|
begin "" end
|
||||||
[nil] << nil
|
[nil] << nil
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
subject.catch(s).should be_valid
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it_detects_empty_expression %(())
|
it_detects_empty_expression %(())
|
||||||
|
@ -91,10 +90,10 @@ module Ameba
|
||||||
)
|
)
|
||||||
|
|
||||||
it "does not report empty expression in macro" do
|
it "does not report empty expression in macro" do
|
||||||
s = Source.new %q(
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
module MyModule
|
module MyModule
|
||||||
macro conditional_error_for_inline_callbacks
|
macro conditional_error_for_inline_callbacks
|
||||||
\{%
|
\\{%
|
||||||
raise ""
|
raise ""
|
||||||
%}
|
%}
|
||||||
end
|
end
|
||||||
|
@ -102,8 +101,7 @@ module Ameba
|
||||||
macro before_save(x = nil)
|
macro before_save(x = nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
)
|
CRYSTAL
|
||||||
subject.catch(s).should be_valid
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,12 @@ module Ameba::Rule::Lint
|
||||||
describe LiteralsComparison do
|
describe LiteralsComparison do
|
||||||
it "passes for valid cases" do
|
it "passes for valid cases" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
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"
|
||||||
foo == "foo"
|
foo == "foo"
|
||||||
foo != "foo"
|
foo != "foo"
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
|
@ -15,8 +19,8 @@ module Ameba::Rule::Lint
|
||||||
|
|
||||||
it "reports if there is a dynamic comparison possibly evaluating to the same" do
|
it "reports if there is a dynamic comparison possibly evaluating to the same" do
|
||||||
expect_issue subject, <<-CRYSTAL
|
expect_issue subject, <<-CRYSTAL
|
||||||
[foo] === ["foo"]
|
[foo] === [foo]
|
||||||
# ^^^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
|
# ^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ module Ameba::Rule::Lint
|
||||||
(1..3).index { |i| i > 2 }.not_nil!(:foo)
|
(1..3).index { |i| i > 2 }.not_nil!(:foo)
|
||||||
(1..3).rindex { |i| i > 2 }.not_nil!(:foo)
|
(1..3).rindex { |i| i > 2 }.not_nil!(:foo)
|
||||||
(1..3).find { |i| i > 2 }.not_nil!(:foo)
|
(1..3).find { |i| i > 2 }.not_nil!(:foo)
|
||||||
|
/(.)(.)(.)/.match("abc", &.itself).not_nil!
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -36,6 +37,17 @@ module Ameba::Rule::Lint
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
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
|
it "reports if there is an `index` call with block followed by `not_nil!`" do
|
||||||
source = expect_issue subject, <<-CRYSTAL
|
source = expect_issue subject, <<-CRYSTAL
|
||||||
(1..3).index { |i| i > 2 }.not_nil!
|
(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
|
it "passes if percent arrays are written correctly" do
|
||||||
s = Source.new %q(
|
s = Source.new %q(
|
||||||
%i(one two three)
|
%i[one two three]
|
||||||
%w(one two three)
|
%w[one two three]
|
||||||
|
|
||||||
%i(1 2 3)
|
%i[1 2 3]
|
||||||
%w(1 2 3)
|
%w[1 2 3]
|
||||||
|
|
||||||
%i()
|
%i[]
|
||||||
%w()
|
%w[]
|
||||||
)
|
)
|
||||||
subject.catch(s).should be_valid
|
subject.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fails if string percent array has commas" do
|
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
|
subject.catch(s).should_not be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fails if string percent array has quotes" do
|
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
|
subject.catch(s).should_not be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fails if symbols percent array has commas" do
|
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
|
subject.catch(s).should_not be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "fails if symbols percent array has a colon" do
|
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
|
subject.catch(s).should_not be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "reports rule, location and message for %i" do
|
it "reports rule, location and message for %i" do
|
||||||
s = Source.new %(
|
s = Source.new %(
|
||||||
%i(:one)
|
%i[:one]
|
||||||
), "source.cr"
|
), "source.cr"
|
||||||
|
|
||||||
subject.catch(s).should_not be_valid
|
subject.catch(s).should_not be_valid
|
||||||
|
@ -54,7 +54,7 @@ module Ameba::Rule::Lint
|
||||||
|
|
||||||
it "reports rule, location and message for %w" do
|
it "reports rule, location and message for %w" do
|
||||||
s = Source.new %(
|
s = Source.new %(
|
||||||
%w("one")
|
%w["one"]
|
||||||
), "source.cr"
|
), "source.cr"
|
||||||
|
|
||||||
subject.catch(s).should_not be_valid
|
subject.catch(s).should_not be_valid
|
||||||
|
@ -71,14 +71,14 @@ module Ameba::Rule::Lint
|
||||||
it "#string_array_unwanted_symbols" do
|
it "#string_array_unwanted_symbols" do
|
||||||
rule = PercentArrays.new
|
rule = PercentArrays.new
|
||||||
rule.string_array_unwanted_symbols = ","
|
rule.string_array_unwanted_symbols = ","
|
||||||
s = Source.new %( %w("one") )
|
s = Source.new %( %w["one"] )
|
||||||
rule.catch(s).should be_valid
|
rule.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
it "#symbol_array_unwanted_symbols" do
|
it "#symbol_array_unwanted_symbols" do
|
||||||
rule = PercentArrays.new
|
rule = PercentArrays.new
|
||||||
rule.symbol_array_unwanted_symbols = ","
|
rule.symbol_array_unwanted_symbols = ","
|
||||||
s = Source.new %( %i(:one) )
|
s = Source.new %( %i[:one] )
|
||||||
rule.catch(s).should be_valid
|
rule.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,6 +31,30 @@ module Ameba::Rule::Lint
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
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
|
it "does not report outer vars declared below shadowed block" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
methods = klass.methods.select { |m| m.annotation(MyAnn) }
|
methods = klass.methods.select { |m| m.annotation(MyAnn) }
|
||||||
|
@ -44,7 +68,7 @@ module Ameba::Rule::Lint
|
||||||
foo = 1
|
foo = 1
|
||||||
|
|
||||||
-> (foo : Int32) {}
|
-> (foo : Int32) {}
|
||||||
# ^ error: Shadowing outer local variable `foo`
|
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
||||||
|
@ -69,7 +93,7 @@ module Ameba::Rule::Lint
|
||||||
3.times do |foo|
|
3.times do |foo|
|
||||||
# ^ error: Shadowing outer local variable `foo`
|
# ^ error: Shadowing outer local variable `foo`
|
||||||
-> (foo : Int32) { foo + 1 }
|
-> (foo : Int32) { foo + 1 }
|
||||||
# ^ error: Shadowing outer local variable `foo`
|
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,7 @@ module Ameba::Rule::Lint
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
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
|
source = expect_issue subject, <<-CRYSTAL
|
||||||
i = 0
|
i = 0
|
||||||
while i < 10
|
while i < 10
|
||||||
|
@ -56,6 +56,24 @@ module Ameba::Rule::Lint
|
||||||
expect_no_corrections source
|
expect_no_corrections source
|
||||||
end
|
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
|
it "reports reassigned reference to shared var in spawn" do
|
||||||
source = expect_issue subject, <<-CRYSTAL
|
source = expect_issue subject, <<-CRYSTAL
|
||||||
channel = Channel(String).new
|
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.rule.should_not be_nil
|
||||||
first.location.to_s.should eq "source_spec.cr:1:11"
|
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"
|
first.message.should eq "Focused spec item detected"
|
||||||
|
|
||||||
second.rule.should_not be_nil
|
second.rule.should_not be_nil
|
||||||
second.location.to_s.should eq "source_spec.cr:2:13"
|
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"
|
second.message.should eq "Focused spec item detected"
|
||||||
end
|
end
|
||||||
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
|
it "reports if proc argument is unused" do
|
||||||
source = expect_issue subject, <<-CRYSTAL
|
source = expect_issue subject, <<-CRYSTAL
|
||||||
-> (a : Int32, b : String) do
|
-> (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
|
a = a + 1
|
||||||
end
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
|
@ -306,7 +306,7 @@ module Ameba::Rule::Lint
|
||||||
|
|
||||||
expect_issue rule, <<-CRYSTAL
|
expect_issue rule, <<-CRYSTAL
|
||||||
->(a : Int32) {}
|
->(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
|
CRYSTAL
|
||||||
end
|
end
|
||||||
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"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
subject = Rule::Style::ConstantNames.new
|
subject = Rule::Naming::ConstantNames.new
|
||||||
|
|
||||||
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
|
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
|
||||||
it "reports constant name #{expected}", file, line do
|
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
|
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||||
%{name} = #{value}
|
%{name} = #{value}
|
||||||
# ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name}
|
# ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name}
|
||||||
|
@ -13,7 +13,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Rule::Style::ConstantNames do
|
describe Rule::Naming::ConstantNames do
|
||||||
it "passes if type names are screaming-cased" do
|
it "passes if type names are screaming-cased" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
LUCKY_NUMBERS = [3, 7, 11]
|
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"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
subject = Rule::Style::MethodNames.new
|
subject = Rule::Naming::MethodNames.new
|
||||||
|
|
||||||
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
|
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
|
||||||
it "reports method name #{expected}", file, line do
|
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
|
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||||
def %{name}; end
|
def %{name}; end
|
||||||
# ^{name} error: Method name should be underscore-cased: #{expected}, not %{name}
|
# ^{name} error: Method name should be underscore-cased: #{expected}, not %{name}
|
||||||
|
@ -13,7 +13,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Rule::Style::MethodNames do
|
describe Rule::Naming::MethodNames do
|
||||||
it "passes if method names are underscore-cased" do
|
it "passes if method names are underscore-cased" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
class Person
|
class Person
|
|
@ -1,6 +1,6 @@
|
||||||
require "../../../spec_helper"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba::Rule::Style
|
module Ameba::Rule::Naming
|
||||||
subject = PredicateName.new
|
subject = PredicateName.new
|
||||||
|
|
||||||
describe PredicateName do
|
describe PredicateName do
|
||||||
|
@ -21,8 +21,18 @@ module Ameba::Rule::Style
|
||||||
|
|
||||||
it "fails if predicate name is wrong" do
|
it "fails if predicate name is wrong" do
|
||||||
expect_issue subject, <<-CRYSTAL
|
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)
|
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
|
end
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
|
@ -1,6 +1,6 @@
|
||||||
require "../../../spec_helper"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba::Rule::Style
|
module Ameba::Rule::Naming
|
||||||
subject = QueryBoolMethods.new
|
subject = QueryBoolMethods.new
|
||||||
|
|
||||||
describe QueryBoolMethods do
|
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"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
subject = Rule::Style::TypeNames.new
|
subject = Rule::Naming::TypeNames.new
|
||||||
|
|
||||||
private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__)
|
private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__)
|
||||||
it "reports type name #{expected}", file, line do
|
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
|
expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line
|
||||||
%{type} %{name}; end
|
%{type} %{name}; end
|
||||||
# ^{type}^{name}^^^^ error: Type name should be camelcased: #{expected}, but it was %{name}
|
_{type} # ^{name} error: Type name should be camelcased: #{expected}, but it was %{name}
|
||||||
CRYSTAL
|
CRYSTAL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Rule::Style::TypeNames do
|
describe Rule::Naming::TypeNames do
|
||||||
it "passes if type names are camelcased" do
|
it "passes if type names are camelcased" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
class ParseError < Exception
|
class ParseError < Exception
|
||||||
|
@ -46,7 +46,7 @@ module Ameba
|
||||||
it "reports alias name" do
|
it "reports alias name" do
|
||||||
expect_issue subject, <<-CRYSTAL
|
expect_issue subject, <<-CRYSTAL
|
||||||
alias Numeric_value = Int32
|
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
|
CRYSTAL
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -1,11 +1,11 @@
|
||||||
require "../../../spec_helper"
|
require "../../../spec_helper"
|
||||||
|
|
||||||
module Ameba
|
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__)
|
private def it_reports_var_name(name, value, expected, *, file = __FILE__, line = __LINE__)
|
||||||
it "reports variable name #{expected}", file, line do
|
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
|
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
|
||||||
%{name} = #{value}
|
%{name} = #{value}
|
||||||
# ^{name} error: Var name should be underscore-cased: #{expected}, not %{name}
|
# ^{name} error: Var name should be underscore-cased: #{expected}, not %{name}
|
||||||
|
@ -13,7 +13,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Rule::Style::VariableNames do
|
describe Rule::Naming::VariableNames do
|
||||||
it "passes if var names are underscore-cased" do
|
it "passes if var names are underscore-cased" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
class Greeting
|
class Greeting
|
|
@ -48,7 +48,7 @@ module Ameba::Rule::Performance
|
||||||
context "properties" do
|
context "properties" do
|
||||||
it "#filter_names" do
|
it "#filter_names" do
|
||||||
rule = AnyAfterFilter.new
|
rule = AnyAfterFilter.new
|
||||||
rule.filter_names = %w(select)
|
rule.filter_names = %w[select]
|
||||||
|
|
||||||
expect_no_issues rule, <<-CRYSTAL
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
[1, 2, 3].reject { |e| e > 2 }.any?
|
[1, 2, 3].reject { |e| e > 2 }.any?
|
||||||
|
|
|
@ -46,7 +46,7 @@ module Ameba::Rule::Performance
|
||||||
context "properties" do
|
context "properties" do
|
||||||
it "#call_names" do
|
it "#call_names" do
|
||||||
rule = ChainedCallWithNoBang.new
|
rule = ChainedCallWithNoBang.new
|
||||||
rule.call_names = %w(uniq)
|
rule.call_names = %w[uniq]
|
||||||
|
|
||||||
expect_no_issues rule, <<-CRYSTAL
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
[1, 2, 3].select { |e| e > 2 }.reverse
|
[1, 2, 3].select { |e| e > 2 }.reverse
|
||||||
|
|
|
@ -6,7 +6,6 @@ module Ameba::Rule::Performance
|
||||||
describe ExcessiveAllocations do
|
describe ExcessiveAllocations do
|
||||||
it "passes if there is no potential performance improvements" do
|
it "passes if there is no potential performance improvements" do
|
||||||
expect_no_issues subject, <<-CRYSTAL
|
expect_no_issues subject, <<-CRYSTAL
|
||||||
"Alice".chars.each
|
|
||||||
"Alice".chars.each(arg) { |c| puts c }
|
"Alice".chars.each(arg) { |c| puts c }
|
||||||
"Alice".chars(arg).each { |c| puts c }
|
"Alice".chars(arg).each { |c| puts c }
|
||||||
"Alice\nBob".lines.each(arg) { |l| puts l }
|
"Alice\nBob".lines.each(arg) { |l| puts l }
|
||||||
|
|
|
@ -64,7 +64,7 @@ module Ameba::Rule::Performance
|
||||||
context "properties" do
|
context "properties" do
|
||||||
it "#filter_names" do
|
it "#filter_names" do
|
||||||
rule = FirstLastAfterFilter.new
|
rule = FirstLastAfterFilter.new
|
||||||
rule.filter_names = %w(reject)
|
rule.filter_names = %w[reject]
|
||||||
|
|
||||||
expect_no_issues rule, <<-CRYSTAL
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
[1, 2, 3].select { |e| e > 2 }.first
|
[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
|
context "properties" do
|
||||||
it "#filter_names" do
|
it "#filter_names" do
|
||||||
rule = SizeAfterFilter.new
|
rule = SizeAfterFilter.new
|
||||||
rule.filter_names = %w(select)
|
rule.filter_names = %w[select]
|
||||||
|
|
||||||
expect_no_issues rule, <<-CRYSTAL
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
[1, 2, 3].reject(&.empty?).size
|
[1, 2, 3].reject(&.empty?).size
|
||||||
|
|
|
@ -44,7 +44,7 @@ module Ameba::Rule::Style
|
||||||
context "properties" do
|
context "properties" do
|
||||||
it "#filter_names" do
|
it "#filter_names" do
|
||||||
rule = IsAFilter.new
|
rule = IsAFilter.new
|
||||||
rule.filter_names = %w(select)
|
rule.filter_names = %w[select]
|
||||||
|
|
||||||
expect_no_issues rule, <<-CRYSTAL
|
expect_no_issues rule, <<-CRYSTAL
|
||||||
[1, 2, nil].reject(&.nil?)
|
[1, 2, nil].reject(&.nil?)
|
||||||
|
|
|
@ -123,7 +123,7 @@ module Ameba
|
||||||
it "#int_min_digits" do
|
it "#int_min_digits" do
|
||||||
rule = Rule::Style::LargeNumbers.new
|
rule = Rule::Style::LargeNumbers.new
|
||||||
rule.int_min_digits = 10
|
rule.int_min_digits = 10
|
||||||
expect_no_issues rule, %q(1200000)
|
expect_no_issues rule, "1200000"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Ameba::Rule::Style
|
||||||
subject = ParenthesesAroundCondition.new
|
subject = ParenthesesAroundCondition.new
|
||||||
|
|
||||||
describe ParenthesesAroundCondition do
|
describe ParenthesesAroundCondition do
|
||||||
{% for keyword in %w(if unless while until) %}
|
{% for keyword in %w[if unless while until] %}
|
||||||
context "{{ keyword.id }}" do
|
context "{{ keyword.id }}" do
|
||||||
it "reports if redundant parentheses are found" do
|
it "reports if redundant parentheses are found" do
|
||||||
source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }}
|
source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }}
|
||||||
|
|
|
@ -95,7 +95,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
|
|
||||||
it "does not run other rules" do
|
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
|
source = Source.new <<-CRYSTAL
|
||||||
MyBadConstant = 1
|
MyBadConstant = 1
|
||||||
|
|
||||||
|
|
|
@ -22,23 +22,23 @@ module Ameba
|
||||||
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
|
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
it_tokenizes %(%w(1 2)),
|
it_tokenizes %(%w[1 2]),
|
||||||
%w(STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
|
%w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
|
||||||
|
|
||||||
it_tokenizes %(%i(one two)),
|
it_tokenizes %(%i[one two]),
|
||||||
%w(SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
|
%w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
|
||||||
|
|
||||||
it_tokenizes %(
|
it_tokenizes %(
|
||||||
class A
|
class A
|
||||||
def method
|
def method
|
||||||
puts "hello"
|
puts "hello"
|
||||||
end
|
|
||||||
end
|
end
|
||||||
), %w(
|
end
|
||||||
|
), %w[
|
||||||
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
|
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
|
||||||
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
|
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
|
||||||
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
|
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
|
||||||
)
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Lint/ComparisonToBoolean:
|
||||||
|
Enabled: true
|
|
@ -6,7 +6,7 @@ module Ameba
|
||||||
# Dummy Rule which does nothing.
|
# Dummy Rule which does nothing.
|
||||||
class DummyRule < Rule::Base
|
class DummyRule < Rule::Base
|
||||||
properties do
|
properties do
|
||||||
description : String = "Dummy rule that does nothing."
|
description "Dummy rule that does nothing."
|
||||||
dummy true
|
dummy true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ module Ameba
|
||||||
|
|
||||||
class PerfRule < Rule::Performance::Base
|
class PerfRule < Rule::Performance::Base
|
||||||
properties do
|
properties do
|
||||||
description : String = "Sample performance rule"
|
description "Sample performance rule"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
|
@ -259,6 +259,7 @@ module Ameba
|
||||||
Crystal::MacroLiteral,
|
Crystal::MacroLiteral,
|
||||||
Crystal::Expressions,
|
Crystal::Expressions,
|
||||||
Crystal::ControlExpression,
|
Crystal::ControlExpression,
|
||||||
|
Crystal::Call,
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize(node)
|
def initialize(node)
|
||||||
|
@ -282,6 +283,13 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_presenter(klass, &)
|
||||||
|
io = IO::Memory.new
|
||||||
|
presenter = klass.new(io)
|
||||||
|
|
||||||
|
yield presenter, io
|
||||||
|
end
|
||||||
|
|
||||||
def as_node(source)
|
def as_node(source)
|
||||||
Crystal::Parser.new(source).parse
|
Crystal::Parser.new(source).parse
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,7 @@ require "./ameba/ast/**"
|
||||||
require "./ameba/ext/**"
|
require "./ameba/ext/**"
|
||||||
require "./ameba/rule/**"
|
require "./ameba/rule/**"
|
||||||
require "./ameba/formatter/*"
|
require "./ameba/formatter/*"
|
||||||
|
require "./ameba/presenter/*"
|
||||||
require "./ameba/source/**"
|
require "./ameba/source/**"
|
||||||
|
|
||||||
# Ameba's entry module.
|
# Ameba's entry module.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./util"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
# Represents the branch in Crystal code.
|
# Represents the branch in Crystal code.
|
||||||
# Branch is a part of a branchable statement.
|
# Branch is a part of a branchable statement.
|
||||||
|
@ -67,6 +69,8 @@ module Ameba::AST
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
private class BranchVisitor < Crystal::Visitor
|
private class BranchVisitor < Crystal::Visitor
|
||||||
|
include Util
|
||||||
|
|
||||||
@current_branch : Crystal::ASTNode?
|
@current_branch : Crystal::ASTNode?
|
||||||
|
|
||||||
property branchable : Branchable?
|
property branchable : Branchable?
|
||||||
|
@ -79,7 +83,7 @@ module Ameba::AST
|
||||||
on_branchable_start(node, branches)
|
on_branchable_start(node, branches)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def on_branchable_start(node, branches : Array | Tuple)
|
private def on_branchable_start(node, branches : Enumerable)
|
||||||
@branchable = Branchable.new(node, @branchable)
|
@branchable = Branchable.new(node, @branchable)
|
||||||
|
|
||||||
branches.each do |branch_node|
|
branches.each do |branch_node|
|
||||||
|
@ -172,6 +176,18 @@ module Ameba::AST
|
||||||
def end_visit(node : Crystal::MacroFor)
|
def end_visit(node : Crystal::MacroFor)
|
||||||
on_branchable_end node
|
on_branchable_end node
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,9 +34,8 @@ module Ameba::AST
|
||||||
# The actual AST node that represents a current scope.
|
# The actual AST node that represents a current scope.
|
||||||
getter node : Crystal::ASTNode
|
getter node : Crystal::ASTNode
|
||||||
|
|
||||||
delegate to_s, to: node
|
delegate location, end_location, to_s,
|
||||||
delegate location, to: node
|
to: @node
|
||||||
delegate end_location, to: node
|
|
||||||
|
|
||||||
def_equals_and_hash node, location
|
def_equals_and_hash node, location
|
||||||
|
|
||||||
|
@ -181,14 +180,19 @@ module Ameba::AST
|
||||||
@visibility || outer_scope.try(&.visibility)
|
@visibility || outer_scope.try(&.visibility)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns `true` if current scope is a def, `false` otherwise.
|
{% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
|
||||||
def def?
|
{% method_name = type.underscore %}
|
||||||
node.is_a?(Crystal::Def)
|
# Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
|
||||||
end
|
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.
|
# Returns `true` if this scope is a top level scope, `false` otherwise.
|
||||||
def top_level?
|
def top_level?
|
||||||
outer_scope.nil? || type_definition?
|
outer_scope.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns `true` if var is an argument in current scope, `false` otherwise.
|
# Returns `true` if var is an argument in current scope, `false` otherwise.
|
||||||
|
|
|
@ -21,8 +21,8 @@ module Ameba::AST::Util
|
||||||
static_literal?(node.to)}
|
static_literal?(node.to)}
|
||||||
when Crystal::ArrayLiteral,
|
when Crystal::ArrayLiteral,
|
||||||
Crystal::TupleLiteral
|
Crystal::TupleLiteral
|
||||||
{true, node.elements.all? do |el|
|
{true, node.elements.all? do |element|
|
||||||
static_literal?(el)
|
static_literal?(element)
|
||||||
end}
|
end}
|
||||||
when Crystal::HashLiteral
|
when Crystal::HashLiteral
|
||||||
{true, node.entries.all? do |entry|
|
{true, node.entries.all? do |entry|
|
||||||
|
|
|
@ -19,9 +19,8 @@ module Ameba::AST
|
||||||
# Variable of this argument (may be the same node)
|
# Variable of this argument (may be the same node)
|
||||||
getter variable : Variable
|
getter variable : Variable
|
||||||
|
|
||||||
delegate location, to: @node
|
delegate location, end_location, to_s,
|
||||||
delegate end_location, to: @node
|
to: @node
|
||||||
delegate to_s, to: @node
|
|
||||||
|
|
||||||
# Creates a new argument.
|
# Creates a new argument.
|
||||||
#
|
#
|
||||||
|
|
|
@ -19,9 +19,8 @@ module Ameba::AST
|
||||||
# A scope assignment belongs to
|
# A scope assignment belongs to
|
||||||
getter scope : Scope
|
getter scope : Scope
|
||||||
|
|
||||||
delegate to_s, to: @node
|
delegate location, end_location, to_s,
|
||||||
delegate location, to: @node
|
to: @node
|
||||||
delegate end_location, to: @node
|
|
||||||
|
|
||||||
# Creates a new assignment.
|
# Creates a new assignment.
|
||||||
#
|
#
|
||||||
|
@ -32,9 +31,7 @@ module Ameba::AST
|
||||||
return unless scope = @variable.scope
|
return unless scope = @variable.scope
|
||||||
|
|
||||||
@branch = Branch.of(@node, scope)
|
@branch = Branch.of(@node, scope)
|
||||||
@referenced = true if @variable.special? ||
|
@referenced = true if @variable.special? || referenced_in_loop?
|
||||||
@variable.scope.type_definition? ||
|
|
||||||
referenced_in_loop?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def referenced_in_loop?
|
def referenced_in_loop?
|
||||||
|
@ -75,31 +72,5 @@ module Ameba::AST
|
||||||
node
|
node
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,8 @@ module Ameba::AST
|
||||||
class InstanceVariable
|
class InstanceVariable
|
||||||
getter node : Crystal::InstanceVar
|
getter node : Crystal::InstanceVar
|
||||||
|
|
||||||
delegate location, to: @node
|
delegate location, end_location, name, to_s,
|
||||||
delegate end_location, to: @node
|
to: @node
|
||||||
delegate name, to: @node
|
|
||||||
delegate to_s, to: @node
|
|
||||||
|
|
||||||
def initialize(@node)
|
def initialize(@node)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,9 +2,8 @@ module Ameba::AST
|
||||||
class TypeDecVariable
|
class TypeDecVariable
|
||||||
getter node : Crystal::TypeDeclaration
|
getter node : Crystal::TypeDeclaration
|
||||||
|
|
||||||
delegate location, to: @node
|
delegate location, end_location, to_s,
|
||||||
delegate end_location, to: @node
|
to: @node
|
||||||
delegate to_s, to: @node
|
|
||||||
|
|
||||||
def initialize(@node)
|
def initialize(@node)
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,10 +17,8 @@ module Ameba::AST
|
||||||
# Node of the first assignment which can be available before any reference.
|
# Node of the first assignment which can be available before any reference.
|
||||||
getter assign_before_reference : Crystal::ASTNode?
|
getter assign_before_reference : Crystal::ASTNode?
|
||||||
|
|
||||||
delegate location, to: @node
|
delegate location, end_location, name, to_s,
|
||||||
delegate end_location, to: @node
|
to: @node
|
||||||
delegate name, to: @node
|
|
||||||
delegate to_s, to: @node
|
|
||||||
|
|
||||||
# Creates a new variable(in the scope).
|
# Creates a new variable(in the scope).
|
||||||
#
|
#
|
||||||
|
@ -54,7 +52,7 @@ module Ameba::AST
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# variable = Variable.new(node, scope)
|
# variable = Variable.new(node, scope)
|
||||||
# variable.reference(var_node)
|
# variable.reference(var_node, some_scope)
|
||||||
# variable.referenced? # => true
|
# variable.referenced? # => true
|
||||||
# ```
|
# ```
|
||||||
def referenced?
|
def referenced?
|
||||||
|
@ -74,6 +72,11 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# :ditto:
|
||||||
|
def reference(scope : Scope)
|
||||||
|
reference(node, scope)
|
||||||
|
end
|
||||||
|
|
||||||
# Reference variable's assignments.
|
# Reference variable's assignments.
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
|
@ -136,7 +139,7 @@ module Ameba::AST
|
||||||
case assign
|
case assign
|
||||||
when Crystal::Assign then eql?(assign.target)
|
when Crystal::Assign then eql?(assign.target)
|
||||||
when Crystal::OpAssign 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)
|
when Crystal::UninitializedVar then eql?(assign.var)
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
|
@ -208,9 +211,9 @@ module Ameba::AST
|
||||||
return if references.size > assignments.size
|
return if references.size > assignments.size
|
||||||
return if assignments.any?(&.op_assign?)
|
return if assignments.any?(&.op_assign?)
|
||||||
|
|
||||||
@assign_before_reference = assignments.find { |ass|
|
@assign_before_reference = assignments
|
||||||
!ass.in_branch?
|
.find(&.in_branch?.!)
|
||||||
}.try &.node
|
.try(&.node)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,7 +24,7 @@ module Ameba::AST
|
||||||
# Uses the same logic than rubocop. See
|
# Uses the same logic than rubocop. See
|
||||||
# https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21
|
# 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.
|
# 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:
|
# :nodoc:
|
||||||
def visit(node : Crystal::{{ node.id.capitalize }})
|
def visit(node : Crystal::{{ node.id.capitalize }})
|
||||||
@complexity += 1 unless macro_condition?
|
@complexity += 1 unless macro_condition?
|
||||||
|
|
|
@ -32,6 +32,7 @@ module Ameba::AST
|
||||||
IsA,
|
IsA,
|
||||||
LibDef,
|
LibDef,
|
||||||
ModuleDef,
|
ModuleDef,
|
||||||
|
MultiAssign,
|
||||||
NilLiteral,
|
NilLiteral,
|
||||||
StringInterpolation,
|
StringInterpolation,
|
||||||
Unless,
|
Unless,
|
||||||
|
|
|
@ -43,7 +43,7 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
|
|
||||||
private def traverse_case(node)
|
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)
|
traverse_node(node.else)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ module Ameba::AST
|
||||||
private def traverse_exception_handler(node)
|
private def traverse_exception_handler(node)
|
||||||
traverse_node node.body
|
traverse_node node.body
|
||||||
traverse_node node.else
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,6 @@ module Ameba::AST
|
||||||
}
|
}
|
||||||
|
|
||||||
SPECIAL_NODE_NAMES = %w[super previous_def]
|
SPECIAL_NODE_NAMES = %w[super previous_def]
|
||||||
RECORD_NODE_NAME = "record"
|
|
||||||
|
|
||||||
@scope_queue = [] of Scope
|
@scope_queue = [] of Scope
|
||||||
@current_scope : Scope
|
@current_scope : Scope
|
||||||
|
@ -154,7 +153,7 @@ module Ameba::AST
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def visit(node : Crystal::Var)
|
def visit(node : Crystal::Var)
|
||||||
variable = @current_scope.find_variable node.name
|
variable = @current_scope.find_variable(node.name)
|
||||||
|
|
||||||
case
|
case
|
||||||
when @current_scope.arg?(node) # node is an argument
|
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
|
when variable.nil? && @current_assign # node is a variable
|
||||||
@current_scope.add_variable(node)
|
@current_scope.add_variable(node)
|
||||||
when variable # node is a reference
|
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)
|
if @current_assign.is_a?(Crystal::OpAssign) || !reference.target_of?(@current_assign)
|
||||||
variable.reference_assignments!
|
variable.reference_assignments!
|
||||||
end
|
end
|
||||||
|
@ -171,26 +170,39 @@ module Ameba::AST
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def visit(node : Crystal::Call)
|
def visit(node : Crystal::Call)
|
||||||
case
|
scope = @current_scope
|
||||||
when @current_scope.def?
|
|
||||||
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
|
|
||||||
@current_scope.arguments.each do |arg|
|
|
||||||
variable = arg.variable
|
|
||||||
|
|
||||||
ref = variable.reference(variable.node, @current_scope)
|
case
|
||||||
ref.explicit = false
|
when (scope.top_level? || scope.type_definition?) && record_macro?(node)
|
||||||
end
|
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
|
end
|
||||||
true
|
|
||||||
when @current_scope.top_level? && record_macro?(node)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
true
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
private def record_macro?(node)
|
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
|
end
|
||||||
|
|
||||||
private def skip?(node)
|
private def skip?(node)
|
||||||
|
|
|
@ -28,7 +28,14 @@ module Ameba::Cli
|
||||||
configure_rules(config, opts)
|
configure_rules(config, opts)
|
||||||
|
|
||||||
if opts.rules?
|
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
|
end
|
||||||
|
|
||||||
runner = Ameba.run(config)
|
runner = Ameba.run(config)
|
||||||
|
@ -49,6 +56,7 @@ module Ameba::Cli
|
||||||
property globs : Array(String)?
|
property globs : Array(String)?
|
||||||
property only : Array(String)?
|
property only : Array(String)?
|
||||||
property except : Array(String)?
|
property except : Array(String)?
|
||||||
|
property describe_rule : String?
|
||||||
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
||||||
property fail_level : Severity?
|
property fail_level : Severity?
|
||||||
property? skip_reading_config = false
|
property? skip_reading_config = false
|
||||||
|
@ -67,11 +75,11 @@ module Ameba::Cli
|
||||||
parser.on("-h", "--help", "Show this help") { print_help(parser) }
|
parser.on("-h", "--help", "Show this help") { print_help(parser) }
|
||||||
parser.on("-r", "--rules", "Show all available rules") { opts.rules = true }
|
parser.on("-r", "--rules", "Show all available rules") { opts.rules = true }
|
||||||
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
||||||
parser.unknown_args do |f|
|
parser.unknown_args do |arr|
|
||||||
if f.size == 1 && f.first =~ /.+:\d+:\d+/
|
if arr.size == 1 && arr.first.matches?(/.+:\d+:\d+/)
|
||||||
configure_explain_opts(f.first, opts)
|
configure_explain_opts(arr.first, opts)
|
||||||
else
|
else
|
||||||
opts.globs = f unless f.empty?
|
opts.globs = arr unless arr.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -119,6 +127,11 @@ module Ameba::Cli
|
||||||
configure_explain_opts(loc, opts)
|
configure_explain_opts(loc, opts)
|
||||||
end
|
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",
|
parser.on("--without-affected-code",
|
||||||
"Stop showing affected code while using a default formatter") do
|
"Stop showing affected code while using a default formatter") do
|
||||||
opts.without_affected_code = true
|
opts.without_affected_code = true
|
||||||
|
@ -152,6 +165,11 @@ module Ameba::Cli
|
||||||
opts.without_affected_code?
|
opts.without_affected_code?
|
||||||
end
|
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)
|
private def configure_explain_opts(loc, opts)
|
||||||
location_to_explain = parse_explain_location(loc)
|
location_to_explain = parse_explain_location(loc)
|
||||||
opts.location_to_explain = location_to_explain
|
opts.location_to_explain = location_to_explain
|
||||||
|
@ -183,14 +201,13 @@ module Ameba::Cli
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
|
|
||||||
private def print_rules(config)
|
private def describe_rule(rule)
|
||||||
config.rules.each do |rule|
|
Presenter::RulePresenter.new.run(rule)
|
||||||
puts "%s [%s] - %s" % {
|
exit 0
|
||||||
rule.name.colorize(:white),
|
end
|
||||||
rule.severity.symbol.to_s.colorize(:green),
|
|
||||||
rule.description.colorize(:dark_gray),
|
private def print_rules(rules)
|
||||||
}
|
Presenter::RuleCollectionPresenter.new.run(rules)
|
||||||
end
|
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -97,8 +97,9 @@ class Ameba::Config
|
||||||
@excluded = load_array_section(config, "Excluded")
|
@excluded = load_array_section(config, "Excluded")
|
||||||
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
|
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
|
||||||
|
|
||||||
return unless formatter_name = load_formatter_name(config)
|
if formatter_name = load_formatter_name(config)
|
||||||
self.formatter = formatter_name
|
self.formatter = formatter_name
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Loads YAML configuration file by `path`.
|
# Loads YAML configuration file by `path`.
|
||||||
|
@ -115,12 +116,13 @@ class Ameba::Config
|
||||||
end
|
end
|
||||||
Config.new YAML.parse(content)
|
Config.new YAML.parse(content)
|
||||||
rescue e
|
rescue e
|
||||||
raise "Config file is invalid: #{e.message}"
|
raise "Unable to load config file: #{e.message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def self.read_config(path = nil)
|
protected def self.read_config(path = nil)
|
||||||
if path
|
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
|
end
|
||||||
each_config_path do |config_path|
|
each_config_path do |config_path|
|
||||||
return File.read(config_path) if File.exists?(config_path)
|
return File.read(config_path) if File.exists?(config_path)
|
||||||
|
@ -202,13 +204,13 @@ class Ameba::Config
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# config = Ameba::Config.load
|
# 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:
|
# 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)
|
def update_rules(names, enabled = true, excluded = nil)
|
||||||
names.try &.each do |name|
|
names.try &.each do |name|
|
||||||
|
@ -243,20 +245,20 @@ class Ameba::Config
|
||||||
# Define rule properties
|
# Define rule properties
|
||||||
macro properties(&block)
|
macro properties(&block)
|
||||||
{% definitions = [] of NamedTuple %}
|
{% definitions = [] of NamedTuple %}
|
||||||
{% if block.body.is_a? Assign %}
|
{% if (prop = block.body).is_a? Call %}
|
||||||
{% definitions << {var: block.body.target, value: block.body.value} %}
|
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
|
||||||
{% elsif block.body.is_a? Call %}
|
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
|
||||||
{% definitions << {var: block.body.name, value: block.body.args.first} %}
|
{% else %}
|
||||||
{% elsif block.body.is_a? TypeDeclaration %}
|
{% definitions << {var: prop.name, value: prop.args.first} %}
|
||||||
{% definitions << {var: block.body.var, value: block.body.value, type: block.body.type} %}
|
{% end %}
|
||||||
{% elsif block.body.is_a? Expressions %}
|
{% elsif block.body.is_a? Expressions %}
|
||||||
{% for prop in block.body.expressions %}
|
{% for prop in block.body.expressions %}
|
||||||
{% if prop.is_a? Assign %}
|
{% if prop.is_a? Call %}
|
||||||
{% definitions << {var: prop.target, value: prop.value} %}
|
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
|
||||||
{% elsif prop.is_a? Call %}
|
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
|
||||||
{% definitions << {var: prop.name, value: prop.args.first} %}
|
{% else %}
|
||||||
{% elsif prop.is_a? TypeDeclaration %}
|
{% definitions << {var: prop.name, value: prop.args.first} %}
|
||||||
{% definitions << {var: prop.var, value: prop.value, type: prop.type} %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
@ -322,9 +324,10 @@ class Ameba::Config
|
||||||
|
|
||||||
macro included
|
macro included
|
||||||
GROUP_SEVERITY = {
|
GROUP_SEVERITY = {
|
||||||
Lint: Ameba::Severity::Warning,
|
Documentation: Ameba::Severity::Warning,
|
||||||
Metrics: Ameba::Severity::Warning,
|
Lint: Ameba::Severity::Warning,
|
||||||
Performance: Ameba::Severity::Warning,
|
Metrics: Ameba::Severity::Warning,
|
||||||
|
Performance: Ameba::Severity::Warning,
|
||||||
}
|
}
|
||||||
|
|
||||||
class_getter default_severity : Ameba::Severity do
|
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.
|
# A list of sources to inspect is passed as an argument.
|
||||||
def started(sources); end
|
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.
|
# 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.
|
# Callback that indicates when source inspection is finished.
|
||||||
# A corresponding source is passed as an argument.
|
# 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.
|
# Callback that indicates when inspection is finished.
|
||||||
# A list of inspected sources is passed as an argument.
|
# 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 formatter that shows the detailed explanation of the issue at
|
||||||
# a specific location.
|
# a specific location.
|
||||||
class ExplainFormatter
|
class ExplainFormatter
|
||||||
HEADING_MARKER = "## "
|
|
||||||
|
|
||||||
include Util
|
include Util
|
||||||
|
|
||||||
getter output : IO::FileDescriptor | IO::Memory
|
getter output : IO::FileDescriptor | IO::Memory
|
||||||
|
@ -64,9 +62,8 @@ module Ameba::Formatter
|
||||||
rule.name.colorize(:magenta),
|
rule.name.colorize(:magenta),
|
||||||
rule.severity.to_s.colorize(rule.severity.color),
|
rule.severity.to_s.colorize(rule.severity.color),
|
||||||
}
|
}
|
||||||
|
if rule_description = colorize_code_fences(rule.description)
|
||||||
if rule.responds_to?(:description)
|
output_paragraph rule_description
|
||||||
output_paragraph rule.description
|
|
||||||
end
|
end
|
||||||
|
|
||||||
rule_doc = colorize_code_fences(rule.class.parsed_doc)
|
rule_doc = colorize_code_fences(rule.class.parsed_doc)
|
||||||
|
@ -84,7 +81,7 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
private def output_title(title)
|
private def output_title(title)
|
||||||
output << HEADING_MARKER.colorize(:yellow)
|
output << "### ".colorize(:yellow)
|
||||||
output << title.upcase.colorize(:yellow)
|
output << title.upcase.colorize(:yellow)
|
||||||
output << "\n\n"
|
output << "\n\n"
|
||||||
end
|
end
|
||||||
|
@ -95,7 +92,7 @@ module Ameba::Formatter
|
||||||
|
|
||||||
private def output_paragraph(paragraph : Array)
|
private def output_paragraph(paragraph : Array)
|
||||||
paragraph.each do |line|
|
paragraph.each do |line|
|
||||||
output << ' ' << line << '\n'
|
output << " " << line << '\n'
|
||||||
end
|
end
|
||||||
output << '\n'
|
output << '\n'
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ module Ameba::Formatter
|
||||||
# Basically, it takes all issues reported and disables corresponding rules
|
# Basically, it takes all issues reported and disables corresponding rules
|
||||||
# or excludes failed sources from these rules.
|
# or excludes failed sources from these rules.
|
||||||
class TODOFormatter < DotFormatter
|
class TODOFormatter < DotFormatter
|
||||||
def initialize(@output = STDOUT)
|
def initialize(@output = STDOUT, @config_path : Path = Config::DEFAULT_PATH)
|
||||||
end
|
end
|
||||||
|
|
||||||
def finished(sources)
|
def finished(sources)
|
||||||
|
@ -26,25 +26,30 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
private def generate_todo_config(issues)
|
private def generate_todo_config(issues)
|
||||||
file = File.new(Config::DEFAULT_PATH, mode: "w")
|
File.open(@config_path, mode: "w") do |file|
|
||||||
file << header
|
file << header
|
||||||
rule_issues_map(issues).each do |rule, rule_issues|
|
|
||||||
file << "\n# Problems found: #{rule_issues.size}"
|
rule_issues_map(issues).each do |rule, rule_issues|
|
||||||
file << "\n# Run `ameba --only #{rule.name}` for details"
|
rule_todo = rule_todo(rule, rule_issues)
|
||||||
file << rule_todo(rule, rule_issues).gsub("---", "")
|
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
|
end
|
||||||
file
|
|
||||||
ensure
|
|
||||||
file.close if file
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def rule_issues_map(issues)
|
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|
|
issues.each do |issue|
|
||||||
next if issue.disabled? || issue.rule.is_a?(Rule::Lint::Syntax)
|
next if issue.disabled? || issue.rule.is_a?(Rule::Lint::Syntax)
|
||||||
next if issue.correctable? && config[:autocorrect]?
|
next if issue.correctable? && config[:autocorrect]?
|
||||||
|
|
||||||
(h[issue.rule] ||= Array(Issue).new) << issue
|
(hash[issue.rule] ||= Array(Issue).new) << issue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -60,11 +65,11 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
private def rule_todo(rule, issues)
|
private def rule_todo(rule, issues)
|
||||||
rule.excluded = issues
|
rule.dup.tap do |rule_todo|
|
||||||
.compact_map(&.location.try &.filename.try &.to_s)
|
rule_todo.excluded = issues
|
||||||
.uniq!
|
.compact_map(&.location.try &.filename.try &.to_s)
|
||||||
|
.uniq!
|
||||||
{rule.name => rule}.to_yaml
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
module Ameba::Formatter
|
module Ameba::Formatter
|
||||||
module Util
|
module Util
|
||||||
|
extend self
|
||||||
|
|
||||||
def deansify(message : String?) : String?
|
def deansify(message : String?) : String?
|
||||||
message.try &.gsub(/\x1b[^m]*m/, "").presence
|
message.try &.gsub(/\x1b[^m]*m/, "").presence
|
||||||
end
|
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
|
module Ameba
|
||||||
# Represents a module used to report issues.
|
# Represents a module used to report issues.
|
||||||
module Reportable
|
module Reportable
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
# List of reported issues.
|
# List of reported issues.
|
||||||
getter issues = [] of Issue
|
getter issues = [] of Issue
|
||||||
|
|
||||||
|
@ -30,13 +34,19 @@ module Ameba
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for Crystal AST *node*.
|
# 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
|
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil, *, prefer_name_location = false) : Issue
|
||||||
add_issue rule, node.location, node.end_location, message, status, block
|
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
|
end
|
||||||
|
|
||||||
# :ditto:
|
# :ditto:
|
||||||
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue
|
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
|
add_issue rule, node, message, status, block, prefer_name_location: prefer_name_location
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for Crystal *token*.
|
# 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
|
# 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.
|
# 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.
|
# NOTE: Must be overridden for other type of rules.
|
||||||
def test(source : Source)
|
def test(source : Source)
|
||||||
AST::NodeVisitor.new self, source
|
AST::NodeVisitor.new self, source
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# NOTE: Can't be abstract
|
||||||
def test(source : Source, node : Crystal::ASTNode, *opts)
|
def test(source : Source, node : Crystal::ASTNode, *opts)
|
||||||
# can't be abstract
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# A convenient addition to `#test` method that does the same
|
# A convenient addition to `#test` method that does the same
|
||||||
|
@ -114,7 +115,7 @@ module Ameba::Rule
|
||||||
|
|
||||||
# Adds an issue to the *source*
|
# Adds an issue to the *source*
|
||||||
macro issue_for(*args, **kwargs, &block)
|
macro issue_for(*args, **kwargs, &block)
|
||||||
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }}
|
source.add_issue(self, {{ args.splat }}, {{ kwargs.double_splat }}) {{ block }}
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def self.rule_name
|
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:
|
# A rule that enforces documentation for public types:
|
||||||
# modules, classes, enums, methods and macros.
|
# modules, classes, enums, methods and macros.
|
||||||
#
|
#
|
||||||
# YAML configuration example:
|
# YAML configuration example:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# Lint/Documentation:
|
# Documentation/Documentation:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# IgnoreClasses: false
|
# IgnoreClasses: false
|
||||||
# IgnoreModules: true
|
# 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
|
end
|
||||||
|
|
||||||
MSG = "Comparison to a boolean is pointless"
|
MSG = "Comparison to a boolean is pointless"
|
||||||
OP_NAMES = %w(== != ===)
|
OP_NAMES = %w[== != ===]
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
return unless node.name.in?(OP_NAMES)
|
return unless node.name.in?(OP_NAMES)
|
||||||
|
|
|
@ -18,7 +18,7 @@ module Ameba::Rule::Lint
|
||||||
class DebugCalls < Base
|
class DebugCalls < Base
|
||||||
properties do
|
properties do
|
||||||
description "Disallows debug-related calls"
|
description "Disallows debug-related calls"
|
||||||
method_names %w(p p! pp pp!)
|
method_names %w[p p! pp pp!]
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Possibly forgotten debug-related `%s` call detected"
|
MSG = "Possibly forgotten debug-related `%s` call detected"
|
||||||
|
|
|
@ -28,8 +28,6 @@ module Ameba::Rule::Lint
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
class EmptyExpression < Base
|
class EmptyExpression < Base
|
||||||
include AST::Util
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows empty expressions"
|
description "Disallows empty expressions"
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Ameba::Rule::Lint
|
||||||
description "Identifies comparisons between literals"
|
description "Identifies comparisons between literals"
|
||||||
end
|
end
|
||||||
|
|
||||||
OP_NAMES = %w(=== == !=)
|
OP_NAMES = %w[=== == !=]
|
||||||
|
|
||||||
MSG = "Comparison always evaluates to %s"
|
MSG = "Comparison always evaluates to %s"
|
||||||
MSG_LIKELY = "Comparison most likely 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)
|
arg_is_literal, arg_is_static = literal_kind?(arg)
|
||||||
|
|
||||||
return unless obj_is_literal && arg_is_literal
|
return unless obj_is_literal && arg_is_literal
|
||||||
|
return unless obj.to_s == arg.to_s
|
||||||
|
|
||||||
is_dynamic = !obj_is_static || !arg_is_static
|
is_dynamic = !obj_is_static || !arg_is_static
|
||||||
|
|
||||||
what =
|
what =
|
||||||
case node.name
|
case node.name
|
||||||
when "===" then "the same"
|
when "===" then "the same"
|
||||||
when "==" then (obj.to_s == arg.to_s).to_s
|
when "==" then "true"
|
||||||
when "!=" then (obj.to_s != arg.to_s).to_s
|
when "!=" then "false"
|
||||||
end
|
end
|
||||||
|
|
||||||
issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what
|
issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what
|
||||||
|
|
|
@ -20,8 +20,6 @@ module Ameba::Rule::Lint
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
class MissingBlockArgument < Base
|
class MissingBlockArgument < Base
|
||||||
include AST::Util
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows yielding method definitions without block argument"
|
description "Disallows yielding method definitions without block argument"
|
||||||
end
|
end
|
||||||
|
@ -36,10 +34,7 @@ module Ameba::Rule::Lint
|
||||||
def test(source, node : Crystal::Def, scope : AST::Scope)
|
def test(source, node : Crystal::Def, scope : AST::Scope)
|
||||||
return if !scope.yields? || node.block_arg
|
return if !scope.yields? || node.block_arg
|
||||||
|
|
||||||
return unless location = node.name_location
|
issue_for node, MSG, prefer_name_location: true
|
||||||
end_location = name_end_location(node)
|
|
||||||
|
|
||||||
issue_for location, end_location, MSG
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,27 +26,21 @@ module Ameba::Rule::Lint
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
class NotNil < Base
|
class NotNil < Base
|
||||||
include AST::Util
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Identifies usage of `not_nil!` calls"
|
description "Identifies usage of `not_nil!` calls"
|
||||||
end
|
end
|
||||||
|
|
||||||
NOT_NIL_NAME = "not_nil!"
|
MSG = "Avoid using `not_nil!`"
|
||||||
MSG = "Avoid using `not_nil!`"
|
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
AST::NodeVisitor.new self, source, skip: :macro
|
AST::NodeVisitor.new self, source, skip: :macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
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 node.obj && node.args.empty?
|
||||||
|
|
||||||
return unless name_location = node.name_location
|
issue_for node, MSG, prefer_name_location: true
|
||||||
return unless end_location = name_end_location(node)
|
|
||||||
|
|
||||||
issue_for name_location, end_location, MSG
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
module Ameba::Rule::Lint
|
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!`.
|
# followed by a call to `not_nil!`.
|
||||||
#
|
#
|
||||||
# For example, this is considered a code smell:
|
# 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:
|
# And can be written as this:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# %w[Alice Bob].find!(&.match(/^A./))
|
# %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b')))
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# YAML configuration example:
|
# YAML configuration example:
|
||||||
|
@ -24,25 +24,24 @@ module Ameba::Rule::Lint
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
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
|
end
|
||||||
|
|
||||||
BLOCK_CALL_NAMES = %w(index rindex find)
|
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
|
||||||
CALL_NAMES = %w(index rindex)
|
|
||||||
|
|
||||||
NOT_NIL_NAME = "not_nil!"
|
BLOCK_CALL_NAMES = %w[index rindex find]
|
||||||
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
|
CALL_NAMES = %w[index rindex match]
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
AST::NodeVisitor.new self, source, skip: :macro
|
AST::NodeVisitor.new self, source, skip: :macro
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
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 = node.obj).is_a?(Crystal::Call)
|
||||||
return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES)
|
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 name_location_end = name_end_location(obj)
|
||||||
return unless end_location = name_end_location(node)
|
return unless end_location = name_end_location(node)
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,15 @@ module Ameba::Rule::Lint
|
||||||
# For example, this is usually written by mistake:
|
# For example, this is usually written by mistake:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# %i(:one, :two)
|
# %i[:one, :two]
|
||||||
# %w("one", "two")
|
# %w["one", "two"]
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# And the expected example is:
|
# And the expected example is:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# %i(one two)
|
# %i[one two]
|
||||||
# %w(one two)
|
# %w[one two]
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# YAML configuration example:
|
# YAML configuration example:
|
||||||
|
@ -42,7 +42,7 @@ module Ameba::Rule::Lint
|
||||||
start_token = token.dup
|
start_token = token.dup
|
||||||
when .string?
|
when .string?
|
||||||
if (_start = start_token) && !issue
|
if (_start = start_token) && !issue
|
||||||
issue = array_entry_invalid?(token.value, _start.raw)
|
issue = array_entry_invalid?(token.value.to_s, _start.raw)
|
||||||
end
|
end
|
||||||
when .string_array_end?
|
when .string_array_end?
|
||||||
if (_start = start_token) && (_issue = issue)
|
if (_start = start_token) && (_issue = issue)
|
||||||
|
@ -63,7 +63,7 @@ module Ameba::Rule::Lint
|
||||||
end
|
end
|
||||||
|
|
||||||
private def check_array_entry(entry, symbols, literal)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,8 +30,8 @@ module Ameba::Rule::Lint
|
||||||
MSG = "Redundant use of `Object#to_s` in interpolation"
|
MSG = "Redundant use of `Object#to_s` in interpolation"
|
||||||
|
|
||||||
def test(source, node : Crystal::StringInterpolation)
|
def test(source, node : Crystal::StringInterpolation)
|
||||||
string_coercion_nodes(node).each do |n|
|
string_coercion_nodes(node).each do |expr|
|
||||||
issue_for n.name_location, n.end_location, MSG
|
issue_for name_location(expr), expr.end_location, MSG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ module Ameba::Rule::Lint
|
||||||
end
|
end
|
||||||
|
|
||||||
private def report(source, node, msg)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ module Ameba::Rule::Lint
|
||||||
!(block = node.block) ||
|
!(block = node.block) ||
|
||||||
with_index_arg?(block)
|
with_index_arg?(block)
|
||||||
|
|
||||||
issue_for node.name_location, node.name_end_location, MSG
|
issue_for node, MSG, prefer_name_location: true
|
||||||
end
|
end
|
||||||
|
|
||||||
private def with_index_arg?(block : Crystal::Block)
|
private def with_index_arg?(block : Crystal::Block)
|
||||||
|
|
|
@ -53,13 +53,16 @@ module Ameba::Rule::Lint
|
||||||
return unless outer_scope = scope.outer_scope
|
return unless outer_scope = scope.outer_scope
|
||||||
|
|
||||||
scope.arguments.reject(&.ignored?).each do |arg|
|
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 variable.nil? || !variable.declared_before?(arg)
|
||||||
next if outer_scope.assigns_ivar?(arg.name)
|
next if outer_scope.assigns_ivar?(name)
|
||||||
next if outer_scope.assigns_type_dec?(arg.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
|
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"
|
description "Reports focused spec items"
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Focused spec item detected"
|
MSG = "Focused spec item detected"
|
||||||
SPEC_ITEM_NAMES = %w(describe context it pending)
|
|
||||||
|
SPEC_ITEM_NAMES = %w[describe context it pending]
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
return unless source.spec?
|
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
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
class UnreachableCode < Base
|
class UnreachableCode < Base
|
||||||
include AST::Util
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Reports unreachable code"
|
description "Reports unreachable code"
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue