diff --git a/.ameba.yml b/.ameba.yml index 1707999..817860f 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -10,4 +10,5 @@ Metrics/CyclomaticComplexity: # exclude all files in vendor/cache Globs: - "**/*.cr" + - "!vendor" - "!lib" diff --git a/.crystal-version b/.crystal-version index b50dd27..61ce01b 100644 --- a/.crystal-version +++ b/.crystal-version @@ -1 +1 @@ -1.13.1 +1.13.2 diff --git a/.dockerignore b/.dockerignore index 15a2b67..257efd6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,23 @@ docs/ -.cache/ +vendor/.cache/ +vendor/shards/install/ .github/ bin/ -assets/ -coverage/ data/ LICENSE Makefile README.md -CONTRIBUTING.md SECURITY.md docker-compose.yml +docker-compose.override.yml +docker-compose.production.yml tmp/ .ameba.yml +script/release +script/deploy +script/update +script/lint +script/format +script/acceptance +script/all +script/test diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 0fbc3ef..87e6af8 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -28,7 +28,7 @@ jobs: crystal: ${{ steps.crystal-version.outputs.crystal }} - name: bootstrap - run: script/bootstrap + run: script/bootstrap --ci - name: set directory permissions (for ci) run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 738b1f6..9933a08 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: crystal: ${{ steps.crystal-version.outputs.crystal }} - name: bootstrap - run: script/bootstrap + run: script/bootstrap --ci - name: build run: script/build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a06d002..f0f6ec2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: crystal: ${{ steps.crystal-version.outputs.crystal }} - name: bootstrap - run: script/bootstrap + run: script/bootstrap --ci - name: lint run: script/lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab0cb3a..fa1aed6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: crystal: ${{ steps.crystal-version.outputs.crystal }} - name: bootstrap - run: script/bootstrap + run: script/bootstrap --ci - name: fetch version run: | @@ -41,7 +41,7 @@ jobs: - name: build (linux x86_64) run: | mkdir -p releases - script/build + script/build --production mv ./bin/runway ./releases/runway-linux-x86_64 - name: generate artifact attestation diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f2f12b..6d58ecb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: crystal: ${{ steps.crystal-version.outputs.crystal }} - name: bootstrap - run: script/bootstrap + run: script/bootstrap --ci - name: test run: script/test diff --git a/.gitignore b/.gitignore index 53bba68..91ff933 100644 --- a/.gitignore +++ b/.gitignore @@ -2,12 +2,15 @@ .cache/shards/* tmp/ bin/ +!vendor/bin/ +!vendor/darwin_arm64/bin/ +!vendor/darwin_x86_64/bin/ +!vendor/linux_x86_64/bin/ .shards/ *.dwarf dist/ - -# do not ignore vendor/*/bin directories -!/vendor/*/bin/ +vendor/shards/install +vendor/.cache vendor/gems/ diff --git a/Dockerfile b/Dockerfile index 9fb6261..1a01f06 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,29 +11,29 @@ LABEL org.opencontainers.image.authors="Grant Birkinbine" WORKDIR /app # install build dependencies -RUN apt-get update && apt-get install libssh2-1-dev -y +RUN apt-get update && apt-get install libssh2-1-dev unzip wget -y + +# install yq +RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq && chmod +x /usr/bin/yq # copy core scripts -COPY script/preinstall script/preinstall -COPY script/update script/update -COPY script/bootstrap script/bootstrap -COPY script/postinstall script/postinstall +COPY script/ script/ # copy all vendored dependencies -COPY lib/ lib/ +COPY vendor/shards/cache/ vendor/shards/cache/ # copy shard files COPY shard.lock shard.lock COPY shard.yml shard.yml # bootstrap the project -RUN USE_LINUX_VENDOR=true script/bootstrap +RUN script/bootstrap --production # copy all source files (ensure to use a .dockerignore file for efficient copying) COPY . . # build the project -RUN script/build +RUN script/build --production # https://github.com/phusion/baseimage-docker FROM ghcr.io/phusion/baseimage:noble-1.0.0 diff --git a/acceptance/Dockerfile b/acceptance/Dockerfile index 642e274..27f7934 100644 --- a/acceptance/Dockerfile +++ b/acceptance/Dockerfile @@ -4,32 +4,29 @@ FROM 84codes/crystal:1.12.1-ubuntu-24.04 AS builder WORKDIR /app # install build dependencies -RUN apt-get update && apt-get install libssh2-1-dev -y +RUN apt-get update && apt-get install libssh2-1-dev unzip wget -y -# copy vendored dependencies -COPY vendor/linux_x86_64/bin/ vendor/linux_x86_64/bin/ +# install yq +RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq && chmod +x /usr/bin/yq # copy core scripts -COPY script/preinstall script/preinstall -COPY script/update script/update -COPY script/bootstrap script/bootstrap -COPY script/postinstall script/postinstall +COPY script/ script/ # copy all vendored dependencies -COPY lib/ lib/ +COPY vendor/shards/cache/ vendor/shards/cache/ # copy shard files COPY shard.lock shard.lock COPY shard.yml shard.yml # bootstrap the project -RUN USE_LINUX_VENDOR=true script/bootstrap +RUN script/bootstrap --production # copy all source files (ensure to use a .dockerignore file for efficient copying) COPY . . # build the project -RUN script/build +RUN script/build --production # https://github.com/phusion/baseimage-docker FROM ghcr.io/phusion/baseimage:noble-1.0.0 diff --git a/lib/.shards.info b/lib/.shards.info deleted file mode 100644 index 3ba4dd9..0000000 --- a/lib/.shards.info +++ /dev/null @@ -1,42 +0,0 @@ ---- -version: 1.0 -shards: - json_mapping: - git: https://github.com/crystal-lang/json_mapping.cr.git - version: 0.1.1 - halite: - git: https://github.com/icyleaf/halite.git - version: 0.12.0 - octokit: - git: https://github.com/octokit-cr/octokit.cr.git - version: 0.3.0 - emoji: - git: https://github.com/veelenga/emoji.cr.git - version: 0.5.0 - cron_parser: - git: https://github.com/kostya/cron_parser.git - version: 0.4.0 - future: - git: https://github.com/crystal-community/future.cr.git - version: 1.0.0 - tasker: - git: https://github.com/spider-gazelle/tasker.git - version: 2.1.4 - retriable: - git: https://github.com/sija/retriable.cr.git - version: 0.2.5 - ameba: - git: https://github.com/crystal-ameba/ameba.git - version: 1.6.1 - spectator: - git: https://github.com/icy-arctic-fox/spectator.git - version: 0.12.0 - crystal-kcov: - git: https://github.com/crystal-community/crystal-kcov.git - version: 0.2.3+git.commit.7e49fe22d7d47040c9de77eb77a6daa76ce0655d - ssh2: - git: https://github.com/spider-gazelle/ssh2.cr.git - version: 1.6.1 - crinja: - git: https://github.com/straight-shoota/crinja.git - version: 0.8.1 diff --git a/lib/ameba/.ameba.yml b/lib/ameba/.ameba.yml deleted file mode 100644 index f679ef0..0000000 --- a/lib/ameba/.ameba.yml +++ /dev/null @@ -1,7 +0,0 @@ -Documentation/DocumentationAdmonition: - Timezone: UTC - Admonitions: [FIXME, BUG] - -Lint/Typos: - Excluded: - - spec/ameba/rule/lint/typos_spec.cr diff --git a/lib/ameba/.dockerignore b/lib/ameba/.dockerignore deleted file mode 100644 index 69e9d6b..0000000 --- a/lib/ameba/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -.* -!Makefile -!shard.yml -!src \ No newline at end of file diff --git a/lib/ameba/.editorconfig b/lib/ameba/.editorconfig deleted file mode 100644 index 8f0c87a..0000000 --- a/lib/ameba/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -[*.cr] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true diff --git a/lib/ameba/.gitignore b/lib/ameba/.gitignore deleted file mode 100644 index 2e28b91..0000000 --- a/lib/ameba/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/doc/ -/lib/ -/bin/ameba -/bin/ameba.dwarf -/.shards/ - -# Libraries don't need dependency lock -# Dependencies will be locked in application that uses them -/shard.lock diff --git a/lib/ameba/Dockerfile b/lib/ameba/Dockerfile deleted file mode 100644 index 9d83603..0000000 --- a/lib/ameba/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM alpine:edge as builder -RUN apk add --update crystal shards yaml-dev musl-dev make -RUN mkdir /ameba -WORKDIR /ameba -COPY . /ameba/ -RUN make clean && make - -FROM alpine:latest -RUN apk add --update yaml pcre2 gc libevent libgcc -RUN mkdir /src -WORKDIR /src -COPY --from=builder /ameba/bin/ameba /usr/bin/ -RUN ameba -v -ENTRYPOINT [ "/usr/bin/ameba" ] diff --git a/lib/ameba/LICENSE b/lib/ameba/LICENSE deleted file mode 100644 index 3970ca6..0000000 --- a/lib/ameba/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2018-2020 Vitalii Elenhaupt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/lib/ameba/Makefile b/lib/ameba/Makefile deleted file mode 100644 index a34f0be..0000000 --- a/lib/ameba/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -CRYSTAL_BIN ?= crystal -SHARDS_BIN ?= shards -PREFIX ?= /usr/local -SHARD_BIN ?= ../../bin -CRFLAGS ?= -Dpreview_mt - -.PHONY: build -build: - $(SHARDS_BIN) build $(CRFLAGS) - -.PHONY: lint -lint: build - ./bin/ameba - -.PHONY: spec -spec: - $(CRYSTAL_BIN) spec - -.PHONY: clean -clean: - rm -f ./bin/ameba ./bin/ameba.dwarf - -.PHONY: install -install: build - mkdir -p $(PREFIX)/bin - cp ./bin/ameba $(PREFIX)/bin - -.PHONY: bin -bin: build - mkdir -p $(SHARD_BIN) - cp ./bin/ameba $(SHARD_BIN) - -.PHONY: test -test: spec lint diff --git a/lib/ameba/README.md b/lib/ameba/README.md deleted file mode 100644 index ecddc39..0000000 --- a/lib/ameba/README.md +++ /dev/null @@ -1,242 +0,0 @@ -

- -

Ameba

-

Code style linter for Crystal

-

- - (a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm) - -

-

- - - -

-

- -- [About](#about) -- [Usage](#usage) - - [Watch a tutorial](#watch-a-tutorial) - - [Autocorrection](#autocorrection) - - [Explain issues](#explain-issues) - - [Run in parallel](#run-in-parallel) -- [Installation](#installation) - - [As a project dependency:](#as-a-project-dependency) - - [OS X](#os-x) - - [Docker](#docker) - - [From sources](#from-sources) -- [Configuration](#configuration) - - [Sources](#sources) - - [Rules](#rules) - - [Inline disabling](#inline-disabling) -- [Editors \& integrations](#editors--integrations) -- [Credits \& inspirations](#credits--inspirations) -- [Contributors](#contributors) - -## About - -Ameba is a static code analysis tool for the Crystal language. -It enforces a consistent [Crystal code style](https://crystal-lang.org/reference/conventions/coding_style.html), -also catches code smells and wrong code constructions. - -See also [Roadmap](https://github.com/crystal-ameba/ameba/wiki). - -## Usage - -Run `ameba` binary within your project directory to catch code issues: - -```sh -$ ameba -Inspecting 107 files - -...............F.....................FF.................................................................... - -src/ameba/formatter/flycheck_formatter.cr:6:37 -[W] Lint/UnusedArgument: Unused argument `location`. If it's necessary, use `_` as an argument name to indicate that it won't be used. -> source.issues.each do |issue, location| - ^ - -src/ameba/formatter/base_formatter.cr:16:14 -[W] Lint/UselessAssign: Useless assignment to variable `s` -> return s += issues.size - ^ - -src/ameba/formatter/base_formatter.cr:16:7 [Correctable] -[C] Style/RedundantReturn: Redundant `return` detected -> return s += issues.size - ^---------------------^ - -Finished in 389.45 milliseconds -107 inspected, 3 failures -``` - -### Watch a tutorial - - - -[๐ŸŽฌ Watch the LuckyCast showing how to use Ameba](https://luckycasts.com/videos/ameba) - -### Autocorrection - -Rules that are marked as `[Correctable]` in the output can be automatically corrected using `--fix` flag: - -```sh -$ ameba --fix -``` - -### Explain issues - -Ameba allows you to dig deeper into an issue, by showing you details about the issue -and the reasoning by it being reported. - -To be convenient, you can just copy-paste the `PATH:line:column` string from the -report and paste behind the `ameba` command to check it out. - -```sh -$ ameba crystal/command/format.cr:26:83 # show explanation for the issue -$ ameba --explain crystal/command/format.cr:26:83 # same thing -``` - -### Run in parallel - -Some quick benchmark results measured while running Ameba on Crystal repo: - -```sh -$ CRYSTAL_WORKERS=1 ameba #=> 29.11 seconds -$ CRYSTAL_WORKERS=2 ameba #=> 19.49 seconds -$ CRYSTAL_WORKERS=4 ameba #=> 13.48 seconds -$ CRYSTAL_WORKERS=8 ameba #=> 10.14 seconds -``` - -## Installation - -### As a project dependency: - -Add this to your application's `shard.yml`: - -```yaml -development_dependencies: - ameba: - github: crystal-ameba/ameba -``` - -Build `bin/ameba` binary within your project directory while running `shards install`. - -### OS X - -```sh -$ brew tap crystal-ameba/ameba -$ brew install ameba -``` - -### Docker - -Build the image: - -```sh -$ docker build -t ghcr.io/crystal-ameba/ameba . -``` - -To use the resulting image on a local source folder, mount the current (or target) directory into `/src`: - -```sh -$ docker run -v $(pwd):/src ghcr.io/crystal-ameba/ameba -``` - -Also available on GitHub: https://github.com/crystal-ameba/ameba/pkgs/container/ameba - -### From sources - -```sh -$ git clone https://github.com/crystal-ameba/ameba && cd ameba -$ make install -``` - -## Configuration - -Default configuration file is `.ameba.yml`. -It allows to configure rule properties, disable specific rules and exclude sources from the rules. - -Generate new file by running `ameba --gen-config`. - -### Sources - -**List of sources to run Ameba on can be configured globally via:** - -- `Globs` section - an array of wildcards (or paths) to include to the - inspection. Defaults to `%w[**/*.cr !lib]`, meaning it includes all project - files with `*.cr` extension except those which exist in `lib` folder. -- `Excluded` section - an array of wildcards (or paths) to exclude from the - source list defined by `Globs`. Defaults to an empty array. - -In this example we define default globs and exclude `src/compiler` folder: - -``` yaml -Globs: - - "**/*.cr" - - "!lib" - -Excluded: - - src/compiler -``` - -**Specific sources can be excluded at rule level**: - -``` yaml -Style/RedundantBegin: - Excluded: - - src/server/processor.cr - - src/server/api.cr -``` - -### Rules - -One or more rules, or a one or more group of rules can be included or excluded -via command line arguments: - -```sh -$ ameba --only Lint/Syntax # runs only Lint/Syntax rule -$ ameba --only Style,Lint # runs only rules from Style and Lint groups -$ ameba --except Lint/Syntax # runs all rules except Lint/Syntax -$ ameba --except Style,Lint # runs all rules except rules in Style and Lint groups -``` - -Or through the configuration file: - -``` yaml -Style/RedundantBegin: - Enabled: false -``` - -### Inline disabling - -One or more rules or one or more group of rules can be disabled using inline directives: - -```crystal -# ameba:disable Style/LargeNumbers -time = Time.epoch(1483859302) - -time = Time.epoch(1483859302) # ameba:disable Style/LargeNumbers, Lint/UselessAssign -time = Time.epoch(1483859302) # ameba:disable Style, Lint -``` - -## Editors & integrations - -- Vim: [vim-crystal](https://github.com/rhysd/vim-crystal), [Ale](https://github.com/w0rp/ale) -- Emacs: [ameba.el](https://github.com/crystal-ameba/ameba.el) -- Sublime Text: [Sublime Linter Ameba](https://github.com/epergo/SublimeLinter-contrib-ameba) -- VSCode: [vscode-crystal-ameba](https://github.com/crystal-ameba/vscode-crystal-ameba) -- Codacy: [codacy-ameba](https://github.com/codacy/codacy-ameba) -- GitHub Actions: [github-action](https://github.com/crystal-ameba/github-action) - -## Credits & inspirations - -- [Crystal Language](https://crystal-lang.org) -- [Rubocop](https://rubocop.readthedocs.io/en/latest/) -- [Credo](http://credo-ci.org/) -- [Dogma](https://github.com/lpil/dogma) - -## Contributors - -- [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer -- [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - contributor, maintainer diff --git a/lib/ameba/bench/check_sources.cr b/lib/ameba/bench/check_sources.cr deleted file mode 100644 index f5fbc2e..0000000 --- a/lib/ameba/bench/check_sources.cr +++ /dev/null @@ -1,30 +0,0 @@ -require "../src/ameba" -require "benchmark" - -private def get_files(n) - Dir["src/**/*.cr"].first(n) -end - -puts "== Compare:" -Benchmark.ips do |x| - [ - 1, - 3, - 5, - 10, - 20, - 30, - 40, - ].each do |n| # ameba:disable Naming/BlockParameterName - config = Ameba::Config.load - config.formatter = Ameba::Formatter::BaseFormatter.new - config.globs = get_files(n) - s = n == 1 ? "" : "s" - x.report("#{n} source#{s}") { Ameba.run config } - end -end - -puts "== Measure:" -config = Ameba::Config.load -config.formatter = Ameba::Formatter::BaseFormatter.new -puts Benchmark.measure { Ameba.run config } diff --git a/lib/ameba/lib b/lib/ameba/lib deleted file mode 120000 index a96aa0e..0000000 --- a/lib/ameba/lib +++ /dev/null @@ -1 +0,0 @@ -.. \ No newline at end of file diff --git a/lib/ameba/shard.yml b/lib/ameba/shard.yml deleted file mode 100644 index 74ce7c6..0000000 --- a/lib/ameba/shard.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: ameba -version: 1.6.1 - -authors: - - Vitalii Elenhaupt - - Sijawusz Pur Rahnama - -targets: - ameba: - main: src/cli.cr - -scripts: - postinstall: shards build -Dpreview_mt - -# TODO: remove pre-compiled executable in future releases -executables: - - ameba - - ameba.cr - -crystal: ~> 1.10 - -license: MIT diff --git a/lib/ameba/src/ameba.cr b/lib/ameba/src/ameba.cr deleted file mode 100644 index e675939..0000000 --- a/lib/ameba/src/ameba.cr +++ /dev/null @@ -1,43 +0,0 @@ -require "./ameba/*" -require "./ameba/ast/**" -require "./ameba/ext/**" -require "./ameba/rule/**" -require "./ameba/formatter/*" -require "./ameba/presenter/*" -require "./ameba/source/**" - -# Ameba's entry module. -# -# To run the linter with default parameters: -# -# ``` -# Ameba.run -# ``` -# -# To configure and run it: -# -# ``` -# config = Ameba::Config.load -# config.formatter = formatter -# config.files = file_paths -# -# Ameba.run config -# ``` -module Ameba - extend self - - VERSION = {{ `shards version "#{__DIR__}"`.chomp.stringify }} - - # Initializes `Ameba::Runner` and runs it. - # Can be configured via `config` parameter. - # - # Examples: - # - # ``` - # Ameba.run - # Ameba.run config - # ``` - def run(config = Config.load) - Runner.new(config).run - end -end diff --git a/lib/ameba/src/ameba/ast/branch.cr b/lib/ameba/src/ameba/ast/branch.cr deleted file mode 100644 index 3092988..0000000 --- a/lib/ameba/src/ameba/ast/branch.cr +++ /dev/null @@ -1,193 +0,0 @@ -require "./util" - -module Ameba::AST - # Represents the branch in Crystal code. - # Branch is a part of a branchable statement. - # For example, the branchable if statement contains 3 branches: - # - # ``` - # if a = something # --> Branch A - # a = 1 # --> Branch B - # put a if out # --> Branch C - # else - # do_something a # --> Branch D - # end - # ``` - class Branch - # The actual branch node. - getter node : Crystal::ASTNode - - # The parent branchable. - getter parent : Branchable - - delegate to_s, to: @node - delegate location, to: @node - delegate end_location, to: @node - - def_equals_and_hash node, location - - # Creates a new branch. - # - # ``` - # Branch.new(if_node) - # ``` - def initialize(@node, @parent) - end - - # Returns `true` if current branch is in a loop, `false` - otherwise. - # For example, this branch is in a loop: - # - # ``` - # while true - # handle_input # this branch is in a loop - # if wrong_input - # show_message # this branch is also in a loop. - # end - # end - # ``` - def in_loop? - @parent.loop? - end - - # Constructs a new branch based on the node in scope. - # - # ``` - # Branch.of(assign_node, scope) - # ``` - def self.of(node : Crystal::ASTNode, scope : Scope) - of(node, scope.node) - end - - # Constructs a new branch based on the node some parent scope. - # - # ``` - # Branch.of(assign_node, def_node) - # ``` - def self.of(node : Crystal::ASTNode, parent_node : Crystal::ASTNode) - BranchVisitor.new(node).tap(&.accept(parent_node)).branch - end - - # :nodoc: - private class BranchVisitor < Crystal::Visitor - include Util - - @current_branch : Crystal::ASTNode? - - property branchable : Branchable? - property branch : Branch? - - def initialize(@node : Crystal::ASTNode) - end - - private def on_branchable_start(node, *branches) - on_branchable_start(node, branches) - end - - private def on_branchable_start(node, branches : Enumerable) - @branchable = Branchable.new(node, @branchable) - - branches.each do |branch_node| - break if branch # branch found - - @current_branch = branch_node if branch_node && !branch_node.nop? - branch_node.try &.accept(self) - end - - false - end - - private def on_branchable_end(node) - @branchable = @branchable.try &.parent - end - - def visit(node : Crystal::ASTNode) - return false if branch - - if node.class == @node.class && - node.location == @node.location && - (branchable = @branchable) && - (branch = @current_branch) - @branch = Branch.new(branch, branchable) - end - - true - end - - def visit(node : Crystal::If | Crystal::Unless) - on_branchable_start node, node.cond, node.then, node.else - end - - def end_visit(node : Crystal::If | Crystal::Unless) - on_branchable_end node - end - - def visit(node : Crystal::BinaryOp) - on_branchable_start node, node.left, node.right - end - - def end_visit(node : Crystal::BinaryOp) - on_branchable_end node - end - - def visit(node : Crystal::Case) - on_branchable_start node, [node.cond, node.whens, node.else].flatten - end - - def end_visit(node : Crystal::Case) - on_branchable_end node - end - - def visit(node : Crystal::While | Crystal::Until) - on_branchable_start node, node.cond, node.body - end - - def end_visit(node : Crystal::While | Crystal::Until) - on_branchable_end node - end - - def visit(node : Crystal::ExceptionHandler) - on_branchable_start node, [node.body, node.rescues, node.else, node.ensure].flatten - end - - def end_visit(node : Crystal::ExceptionHandler) - on_branchable_end node - end - - def visit(node : Crystal::Rescue) - on_branchable_start node, node.body - end - - def end_visit(node : Crystal::Rescue) - on_branchable_end node - end - - def visit(node : Crystal::MacroIf) - on_branchable_start node, node.cond, node.then, node.else - end - - def end_visit(node : Crystal::MacroIf) - on_branchable_end node - end - - def visit(node : Crystal::MacroFor) - on_branchable_start node, node.exp, node.body - end - - def end_visit(node : Crystal::MacroFor) - on_branchable_end node - end - - def visit(node : Crystal::Call) - if loop?(node) && (block = node.block) - on_branchable_start node, block.body - end - end - - def end_visit(node : Crystal::Call) - if loop?(node) && node.block - on_branchable_end node - end - end - end - end -end diff --git a/lib/ameba/src/ameba/ast/branchable.cr b/lib/ameba/src/ameba/ast/branchable.cr deleted file mode 100644 index faeea67..0000000 --- a/lib/ameba/src/ameba/ast/branchable.cr +++ /dev/null @@ -1,45 +0,0 @@ -require "./util" - -module Ameba::AST - # A generic entity to represent a branchable Crystal node. - # For example, `Crystal::If`, `Crystal::Unless`, `Crystal::While` - # are branchables. - # - # ``` - # while a > 100 # Branchable A - # if b > 2 # Branchable B - # a += 1 - # end - # end - # ``` - class Branchable - include Util - - # Parent branchable (if any) - getter parent : Branchable? - - # Array of branches - getter branches = [] of Crystal::ASTNode - - # The actual Crystal node - getter node : Crystal::ASTNode - - delegate to_s, to: @node - delegate location, to: @node - delegate end_location, to: @node - - # Creates a new branchable - # - # ``` - # Branchable.new(node, parent_branchable) - # ``` - def initialize(@node, @parent = nil) - end - - # Returns `true` if this node or one of the parent branchables is a loop, - # `false` otherwise. - def loop? - loop?(node) || !!parent.try(&.loop?) - end - end -end diff --git a/lib/ameba/src/ameba/ast/flow_expression.cr b/lib/ameba/src/ameba/ast/flow_expression.cr deleted file mode 100644 index fe71c69..0000000 --- a/lib/ameba/src/ameba/ast/flow_expression.cr +++ /dev/null @@ -1,72 +0,0 @@ -require "./util" - -module Ameba::AST - # Represents a flow expression in Crystal code. - # For example, - # - # ``` - # def foobar - # a = 3 - # return 42 # => flow expression - # a + 1 - # end - # ``` - # - # Flow expression contains an actual node of a control expression and - # a parent node, which allows easily search through the related statement - # (i.e. find unreachable code) - class FlowExpression - include Util - - # Is true only if some of the nodes parents is a loop. - getter? in_loop : Bool - - # The actual node of the flow expression. - getter node : Crystal::ASTNode - - delegate to_s, to: @node - delegate location, to: @node - delegate end_location, to: @node - - # Creates a new flow expression. - # - # ``` - # FlowExpression.new(node, parent_node) - # ``` - def initialize(@node, @in_loop) - end - - # Returns nodes which can't be reached because of a flow command inside. - # For example: - # - # ``` - # def foobar - # a = 1 - # return 42 - # - # a + 2 # => unreachable assign node - # end - # ``` - def unreachable_nodes - unreachable_nodes = [] of Crystal::ASTNode - - case current_node = node - when Crystal::Expressions - control_flow_found = false - - current_node.expressions.each do |exp| - if control_flow_found - unreachable_nodes << exp - end - control_flow_found ||= !loop?(exp) && flow_expression?(exp, in_loop?) - end - when Crystal::BinaryOp - if flow_expression?(current_node.left, in_loop?) - unreachable_nodes << current_node.right - end - end - - unreachable_nodes - end - end -end diff --git a/lib/ameba/src/ameba/ast/scope.cr b/lib/ameba/src/ameba/ast/scope.cr deleted file mode 100644 index 3e0b457..0000000 --- a/lib/ameba/src/ameba/ast/scope.cr +++ /dev/null @@ -1,229 +0,0 @@ -require "./variabling/*" - -module Ameba::AST - # Represents a context of the local variable visibility. - # This is where the local variables belong to. - class Scope - # Whether the scope yields. - setter yields = false - - # Scope visibility level - setter visibility : Crystal::Visibility? - - # Link to local variables - getter variables = [] of Variable - - # Link to all variable references in currency scope - getter references = [] of Reference - - # Link to the arguments in current scope - getter arguments = [] of Argument - - # Link to the instance variables used in current scope - getter ivariables = [] of InstanceVariable - - # Link to the type declaration variables used in current scope - getter type_dec_variables = [] of TypeDecVariable - - # Link to the outer scope - getter outer_scope : Scope? - - # List of inner scopes - getter inner_scopes = [] of Scope - - # The actual AST node that represents a current scope. - getter node : Crystal::ASTNode - - delegate location, end_location, to_s, - to: @node - - def_equals_and_hash node, location - - # Creates a new scope. Accepts the AST node and the outer scope. - # - # ``` - # scope = Scope.new(class_node, nil) - # ``` - def initialize(@node, @outer_scope = nil) - @outer_scope.try &.inner_scopes.<< self - end - - # Creates a new variable in the current scope. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.add_variable(var_node) - # ``` - def add_variable(node) - variables << Variable.new(node, self) - end - - # Creates a new argument in the current scope. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.add_argument(arg_node) - # ``` - def add_argument(node) - add_variable Crystal::Var.new(node.name).at(node) - arguments << Argument.new(node, variables.last) - end - - # Adds a new instance variable to the current scope. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.add_ivariable(ivar_node) - # ``` - def add_ivariable(node) - ivariables << InstanceVariable.new(node) - end - - # Adds a new type declaration variable to the current scope. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.add_type_dec_variable(node) - # ``` - def add_type_dec_variable(node) - type_dec_variables << TypeDecVariable.new(node) - end - - # Returns variable by its name or `nil` if it does not exist. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.find_variable("foo") - # ``` - def find_variable(name : String) - variables.find(&.name.==(name)) || - outer_scope.try &.find_variable(name) - end - - # Creates a new assignment for the variable. - # - # ``` - # scope = Scope.new(class_node, nil) - # scope.assign_variable(var_name, assign_node) - # ``` - def assign_variable(name, node) - find_variable(name).try &.assign(node, self) - end - - # Returns `true` if current scope represents a block (or proc), - # `false` otherwise. - def block? - node.is_a?(Crystal::Block) || - node.is_a?(Crystal::ProcLiteral) - end - - # Returns `true` if current scope represents a spawn block, e. g. - # - # ``` - # spawn do - # # ... - # end - # ``` - def spawn_block? - node.as?(Crystal::Block).try(&.call).try(&.name) == "spawn" - end - - # Returns `true` if current scope sits inside a macro. - def in_macro? - (node.is_a?(Crystal::Macro) || - node.is_a?(Crystal::MacroIf) || - node.is_a?(Crystal::MacroFor)) || - !!outer_scope.try(&.in_macro?) - end - - # Returns `true` if instance variable is assigned in this scope. - def assigns_ivar?(name) - arguments.any?(&.name.== name) && - ivariables.any?(&.name.== "@#{name}") - end - - # Returns `true` if type declaration variable is assigned in this scope. - def assigns_type_dec?(name) - type_dec_variables.any?(&.name.== name) || - !!outer_scope.try(&.assigns_type_dec?(name)) - end - - # Returns `true` if and only if current scope represents some - # type definition, for example a class. - def type_definition? - node.is_a?(Crystal::ClassDef) || - node.is_a?(Crystal::ModuleDef) || - node.is_a?(Crystal::EnumDef) || - node.is_a?(Crystal::LibDef) || - node.is_a?(Crystal::FunDef) || - node.is_a?(Crystal::TypeDef) || - node.is_a?(Crystal::CStructOrUnionDef) - end - - # Returns `true` if current scope (or any of inner scopes) references variable, - # `false` otherwise. - def references?(variable : Variable, check_inner_scopes = true) - variable.references.any? do |reference| - (reference.scope == self) || - (check_inner_scopes && inner_scopes.any?(&.references?(variable))) - end || variable.used_in_macro? - end - - # Returns `true` if current scope (or any of inner scopes) yields, - # `false` otherwise. - def yields?(check_inner_scopes = true) - @yields || (check_inner_scopes && inner_scopes.any?(&.yields?)) - end - - # Returns visibility of the current scope (could be inherited from the outer scope). - def visibility - @visibility || outer_scope.try(&.visibility) - end - - # Returns `true` if current scope is a def, `false` otherwise. - def def? - node.is_a?(Crystal::Def) - end - - # Returns `true` if current scope is a class, `false` otherwise. - def class_def? - node.is_a?(Crystal::ClassDef) - end - - # Returns `true` if current scope is a module, `false` otherwise. - def module_def? - node.is_a?(Crystal::ModuleDef) - end - - # Returns `true` if this scope is a top level scope, `false` otherwise. - def top_level? - outer_scope.nil? - end - - # Returns `true` if var is an argument in current scope, `false` otherwise. - def arg?(var) - case current_node = node - when Crystal::Def - var.is_a?(Crystal::Arg) && any_arg?(current_node.args, var) - when Crystal::Block - var.is_a?(Crystal::Var) && any_arg?(current_node.args, var) - when Crystal::ProcLiteral - var.is_a?(Crystal::Var) && any_arg?(current_node.def.args, var) - else - false - end - end - - private def any_arg?(args, var) - args.any? { |arg| arg.name == var.name && arg.location == var.location } - end - - # Returns `true` if the *node* represents exactly - # the same Crystal node as `@node`. - def eql?(node) - node == @node && - node.location && - node.location == @node.location - end - end -end diff --git a/lib/ameba/src/ameba/ast/util.cr b/lib/ameba/src/ameba/ast/util.cr deleted file mode 100644 index ce71fc9..0000000 --- a/lib/ameba/src/ameba/ast/util.cr +++ /dev/null @@ -1,248 +0,0 @@ -# Utility module for Ameba's rules. -module Ameba::AST::Util - # Returns tuple with two bool flags: - # - # 1. is *node* a literal? - # 2. can *node* be proven static? - protected def literal_kind?(node) : {Bool, Bool} - case node - when Crystal::NilLiteral, - Crystal::BoolLiteral, - Crystal::NumberLiteral, - Crystal::CharLiteral, - Crystal::StringLiteral, - Crystal::SymbolLiteral, - Crystal::RegexLiteral, - Crystal::ProcLiteral, - Crystal::MacroLiteral - {true, true} - when Crystal::RangeLiteral - {true, static_literal?(node.from) && - static_literal?(node.to)} - when Crystal::ArrayLiteral, - Crystal::TupleLiteral - {true, node.elements.all? do |element| - static_literal?(element) - end} - when Crystal::HashLiteral - {true, node.entries.all? do |entry| - static_literal?(entry.key) && - static_literal?(entry.value) - end} - when Crystal::NamedTupleLiteral - {true, node.entries.all? do |entry| - static_literal?(entry.value) - end} - else - {false, false} - end - end - - # Returns `true` if current `node` is a static literal, `false` otherwise. - def static_literal?(node) : Bool - is_literal, is_static = literal_kind?(node) - is_literal && is_static - end - - # Returns `true` if current `node` is a dynamic literal, `false` otherwise. - def dynamic_literal?(node) : Bool - is_literal, is_static = literal_kind?(node) - is_literal && !is_static - end - - # Returns `true` if current `node` is a literal, `false` otherwise. - def literal?(node) : Bool - is_literal, _ = literal_kind?(node) - is_literal - end - - # Returns `true` if current `node` is a `Crystal::Path` - # matching given *name*, `false` otherwise. - def path_named?(node, name) : Bool - node.is_a?(Crystal::Path) && - name == node.names.join("::") - end - - # Returns a source code for the current node. - # This method uses `node.location` and `node.end_location` - # to determine and cut a piece of source of the node. - def node_source(node, code_lines) - loc, end_loc = node.location, node.end_location - return unless loc && end_loc - - source_between(loc, end_loc, code_lines) - end - - # Returns the source code from *loc* to *end_loc* (inclusive). - def source_between(loc, end_loc, code_lines) : String? - line, column = loc.line_number - 1, loc.column_number - 1 - end_line, end_column = end_loc.line_number - 1, end_loc.column_number - 1 - node_lines = code_lines[line..end_line] - first_line, last_line = node_lines[0]?, node_lines[-1]? - - return if first_line.nil? || last_line.nil? - return if first_line.size < column # compiler reports incorrect location - - node_lines[0] = first_line.sub(0...column, "") - - if line == end_line # one line - end_column = end_column - column - last_line = node_lines[0] - end - - return if last_line.size < end_column + 1 - - node_lines[-1] = last_line.sub(end_column + 1...last_line.size, "") - node_lines.join('\n') - end - - # Returns `true` if node is a flow command, `false` otherwise. - # Node represents a flow command if it is a control expression, - # or special call node that interrupts execution (i.e. raise, exit, abort). - def flow_command?(node, in_loop) - case node - when Crystal::Return - true - when Crystal::Break, Crystal::Next - in_loop - when Crystal::Call - raise?(node) || exit?(node) || abort?(node) - else - false - end - end - - # Returns `true` if node is a flow expression, `false` if not. - # Node represents a flow expression if it is full-filled by a flow command. - # - # For example, this node is a flow expression, because each branch contains - # a flow command `return`: - # - # ``` - # if a > 0 - # return :positive - # elsif a < 0 - # return :negative - # else - # return :zero - # end - # ``` - # - # This node is a not a flow expression: - # - # ``` - # if a > 0 - # return :positive - # end - # ``` - # - # That's because not all branches return(i.e. `else` is missing). - def flow_expression?(node, in_loop = false) - return true if flow_command? node, in_loop - - case node - when Crystal::If, Crystal::Unless - flow_expressions? [node.then, node.else], in_loop - when Crystal::BinaryOp - flow_expression? node.left, in_loop - when Crystal::Case - flow_expressions? [node.whens, node.else].flatten, in_loop - when Crystal::ExceptionHandler - flow_expressions? [node.else || node.body, node.rescues].flatten, in_loop - when Crystal::While, Crystal::Until - flow_expression? node.body, in_loop - when Crystal::Rescue, Crystal::When - flow_expression? node.body, in_loop - when Crystal::Expressions - node.expressions.any? { |exp| flow_expression? exp, in_loop } - else - false - end - end - - private def flow_expressions?(nodes, in_loop) - nodes.all? { |exp| flow_expression? exp, in_loop } - end - - # Returns `true` if node represents `raise` method call. - def raise?(node) - node.is_a?(Crystal::Call) && - node.name == "raise" && node.args.size == 1 && node.obj.nil? - end - - # Returns `true` if node represents `exit` method call. - def exit?(node) - node.is_a?(Crystal::Call) && - node.name == "exit" && node.args.size <= 1 && node.obj.nil? - end - - # Returns `true` if node represents `abort` method call. - def abort?(node) - node.is_a?(Crystal::Call) && - node.name == "abort" && node.args.size <= 2 && node.obj.nil? - end - - # Returns `true` if node represents a loop. - def loop?(node) - case node - when Crystal::While, Crystal::Until - true - when Crystal::Call - node.name == "loop" && node.args.size == 0 && node.obj.nil? - else - false - end - end - - # Returns the exp code of a control expression. - # Wraps implicit tuple literal with curly brackets (e.g. multi-return). - def control_exp_code(node : Crystal::ControlExpression, code_lines) - return unless exp = node.exp - return unless exp_code = node_source(exp, code_lines) - return exp_code unless exp.is_a?(Crystal::TupleLiteral) && exp_code[0] != '{' - return unless exp_start = exp.elements.first.location - return unless exp_end = exp.end_location - - "{#{source_between(exp_start, exp_end, code_lines)}}" - end - - # Returns `nil` if *node* does not contain a name. - def name_location(node) - if loc = node.name_location - return loc - end - - return node.var.location if node.is_a?(Crystal::TypeDeclaration) || - node.is_a?(Crystal::UninitializedVar) - return unless node.responds_to?(:name) && (name = node.name) - return unless name.is_a?(Crystal::ASTNode) - - name.location - end - - # Returns zero if *node* does not contain a name. - def name_size(node) - unless (size = node.name_size).zero? - return size - end - - return 0 unless node.responds_to?(:name) && (name = node.name) - - case name - when Crystal::ASTNode then name.name_size - when Crystal::Token::Kind then name.to_s.size # Crystal::MagicConstant - else name.size - end - end - - # Returns `nil` if *node* does not contain a name. - # - # NOTE: Use this instead of `Crystal::Call#name_end_location` to avoid an - # off-by-one error. - def name_end_location(node) - return unless loc = name_location(node) - return if (size = name_size(node)).zero? - - loc.adjust(column_number: size - 1) - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/argument.cr b/lib/ameba/src/ameba/ast/variabling/argument.cr deleted file mode 100644 index d080e9a..0000000 --- a/lib/ameba/src/ameba/ast/variabling/argument.cr +++ /dev/null @@ -1,53 +0,0 @@ -module Ameba::AST - # Represents the argument of some node. - # Holds the reference to the variable, thus to scope. - # - # For example, all these vars are arguments: - # - # ``` - # def method(a, b, c = 10, &block) - # 3.times do |i| - # end - # - # ->(x : Int32) {} - # end - # ``` - class Argument - # The actual node. - getter node : Crystal::Var | Crystal::Arg - - # Variable of this argument (may be the same node) - getter variable : Variable - - delegate location, end_location, to_s, - to: @node - - # Creates a new argument. - # - # ``` - # Argument.new(node, variable) - # ``` - def initialize(@node, @variable) - end - - # Returns `true` if the `name` is empty, `false` otherwise. - def anonymous? - name.blank? - end - - # Returns `true` if the `name` starts with '_', `false` otherwise. - def ignored? - name.starts_with? '_' - end - - # Name of the argument. - def name - case current_node = node - when Crystal::Var, Crystal::Arg - current_node.name - else - raise ArgumentError.new "Invalid node" - end - end - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/assignment.cr b/lib/ameba/src/ameba/ast/variabling/assignment.cr deleted file mode 100644 index ece19b8..0000000 --- a/lib/ameba/src/ameba/ast/variabling/assignment.cr +++ /dev/null @@ -1,76 +0,0 @@ -require "./reference" -require "./variable" - -module Ameba::AST - # Represents the assignment to the variable. - # Holds the assign node and the variable. - class Assignment - property? referenced = false - - # The actual assignment node. - getter node : Crystal::ASTNode - - # Variable of this assignment. - getter variable : Variable - - # Branch of this assignment. - getter branch : Branch? - - # A scope assignment belongs to - getter scope : Scope - - delegate location, end_location, to_s, - to: @node - - # Creates a new assignment. - # - # ``` - # Assignment.new(node, variable, scope) - # ``` - def initialize(@node, @variable, @scope) - return unless scope = @variable.scope - - @branch = Branch.of(@node, scope) - @referenced = true if @variable.special? || referenced_in_loop? - end - - def referenced_in_loop? - @variable.referenced? && !!@branch.try(&.in_loop?) - end - - # Returns `true` if this assignment is an op assign, `false` if not. - # For example, this is an op assign: - # - # ``` - # a ||= 1 - # ``` - def op_assign? - node.is_a?(Crystal::OpAssign) - end - - # Returns `true` if this assignment is in a branch, `false` if not. - # For example, this assignment is in a branch: - # - # ``` - # a = 1 if a.nil? - # ``` - def in_branch? - !branch.nil? - end - - # Returns the target node of the variable in this assignment. - def target_node - case assign = node - when Crystal::Assign then assign.target - when Crystal::OpAssign then assign.target - when Crystal::UninitializedVar then assign.var - when Crystal::MultiAssign - assign.targets.find(node) do |target| - target.is_a?(Crystal::Var) && target.name == variable.name - end - else - node - end - end - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/ivariable.cr b/lib/ameba/src/ameba/ast/variabling/ivariable.cr deleted file mode 100644 index db6f3bd..0000000 --- a/lib/ameba/src/ameba/ast/variabling/ivariable.cr +++ /dev/null @@ -1,11 +0,0 @@ -module Ameba::AST - class InstanceVariable - getter node : Crystal::InstanceVar - - delegate location, end_location, name, to_s, - to: @node - - def initialize(@node) - end - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/reference.cr b/lib/ameba/src/ameba/ast/variabling/reference.cr deleted file mode 100644 index 885234d..0000000 --- a/lib/ameba/src/ameba/ast/variabling/reference.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "./variable" - -module Ameba::AST - # Represents a reference to the variable. - # It behaves like a variable is used to distinguish a - # the variable from its reference. - class Reference < Variable - property? explicit = true - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/type_dec_variable.cr b/lib/ameba/src/ameba/ast/variabling/type_dec_variable.cr deleted file mode 100644 index 74cc3fb..0000000 --- a/lib/ameba/src/ameba/ast/variabling/type_dec_variable.cr +++ /dev/null @@ -1,20 +0,0 @@ -module Ameba::AST - class TypeDecVariable - getter node : Crystal::TypeDeclaration - - delegate location, end_location, to_s, - to: @node - - def initialize(@node) - end - - def name - case var = @node.var - when Crystal::Var, Crystal::InstanceVar, Crystal::ClassVar, Crystal::Global - var.name - else - raise "Unsupported var node type: #{var.class}" - end - end - end -end diff --git a/lib/ameba/src/ameba/ast/variabling/variable.cr b/lib/ameba/src/ameba/ast/variabling/variable.cr deleted file mode 100644 index 8b51394..0000000 --- a/lib/ameba/src/ameba/ast/variabling/variable.cr +++ /dev/null @@ -1,219 +0,0 @@ -module Ameba::AST - # Represents the existence of the local variable. - # Holds the var node and variable assignments. - class Variable - # List of the assignments of this variable. - getter assignments = [] of Assignment - - # List of the references of this variable. - getter references = [] of Reference - - # The actual var node. - getter node : Crystal::Var - - # Scope of this variable. - getter scope : Scope - - # Node of the first assignment which can be available before any reference. - getter assign_before_reference : Crystal::ASTNode? - - delegate location, end_location, name, to_s, - to: @node - - # Creates a new variable(in the scope). - # - # ``` - # Variable.new(node, scope) - # ``` - def initialize(@node, @scope) - end - - # Returns `true` if it is a special variable, i.e `$?`. - def special? - @node.special_var? - end - - # Assigns the variable (creates a new assignment). - # Variable may have multiple assignments. - # - # ``` - # variable = Variable.new(node, scope) - # variable.assign(node1) - # variable.assign(node2) - # variable.assignment.size # => 2 - # ``` - def assign(node, scope) - assignments << Assignment.new(node, self, scope) - - update_assign_reference! - end - - # Returns `true` if variable has any reference. - # - # ``` - # variable = Variable.new(node, scope) - # variable.reference(var_node, some_scope) - # variable.referenced? # => true - # ``` - def referenced? - !references.empty? - end - - # Creates a reference to this variable in some scope. - # - # ``` - # variable = Variable.new(node, scope) - # variable.reference(var_node, some_scope) - # ``` - def reference(node : Crystal::Var, scope : Scope) - Reference.new(node, scope).tap do |reference| - references << reference - scope.references << reference - end - end - - # :ditto: - def reference(scope : Scope) - reference(node, scope) - end - - # Reference variable's assignments. - # - # ``` - # variable = Variable.new(node, scope) - # variable.assign(assign_node) - # variable.reference_assignments! - # ``` - def reference_assignments! - consumed_branches = Set(Branch).new - - assignments.reverse_each do |assignment| - next if assignment.branch.in?(consumed_branches) - assignment.referenced = true - - break unless branch = assignment.branch - consumed_branches << branch - end - end - - # Returns `true` if the current var is referenced in - # in the block. For example this variable is captured - # by block: - # - # ``` - # a = 1 - # 3.times { |i| a = a + i } - # ``` - # - # And this variable is not captured by block. - # - # ``` - # i = 1 - # 3.times { |i| i + 1 } - # ``` - def captured_by_block?(scope = @scope) - scope.inner_scopes.each do |inner_scope| - return true if inner_scope.block? && - inner_scope.references?(self, check_inner_scopes: false) - return true if captured_by_block?(inner_scope) - end - - false - end - - # Returns `true` if current variable potentially referenced in a macro, - # `false` if not. - def used_in_macro?(scope = @scope) - scope.inner_scopes.each do |inner_scope| - return true if MacroReferenceFinder.new(inner_scope.node, node.name).references? - end - return true if MacroReferenceFinder.new(scope.node, node.name).references? - return true if (outer_scope = scope.outer_scope) && used_in_macro?(outer_scope) - - false - end - - # Returns `true` if the variable is a target (on the left) of the assignment, - # `false` otherwise. - def target_of?(assign) - case assign - when Crystal::Assign then eql?(assign.target) - when Crystal::OpAssign then eql?(assign.target) - when Crystal::MultiAssign then assign.targets.any? { |target| eql?(target) } - when Crystal::UninitializedVar then eql?(assign.var) - else - false - end - end - - # Returns `true` if the name starts with '_', `false` if not. - def ignored? - name.starts_with? '_' - end - - # Returns `true` if the `node` represents exactly - # the same Crystal node as `@node`. - def eql?(node) - node.is_a?(Crystal::Var) && - node.name == @node.name && - node.location == @node.location - end - - # Returns `true` if the variable is declared before the `node`. - def declared_before?(node) - var_location, node_location = location, node.location - - return unless var_location && node_location - - (var_location.line_number < node_location.line_number) || - (var_location.line_number == node_location.line_number && - var_location.column_number < node_location.column_number) - end - - private class MacroReferenceFinder < Crystal::Visitor - property? references = false - - def initialize(node, @reference : String) - node.accept self - end - - @[AlwaysInline] - private def includes_reference?(val) - val.to_s.includes?(@reference) - end - - def visit(node : Crystal::MacroLiteral) - !(@references ||= includes_reference?(node.value)) - end - - def visit(node : Crystal::MacroExpression) - !(@references ||= includes_reference?(node.exp)) - end - - def visit(node : Crystal::MacroFor) - !(@references ||= includes_reference?(node.exp) || - includes_reference?(node.body)) - end - - def visit(node : Crystal::MacroIf) - !(@references ||= includes_reference?(node.cond) || - includes_reference?(node.then) || - includes_reference?(node.else)) - end - - def visit(node : Crystal::ASTNode) - true - end - end - - private def update_assign_reference! - return if @assign_before_reference - return if references.size > assignments.size - return if assignments.any?(&.op_assign?) - - @assign_before_reference = assignments - .find(&.in_branch?.!) - .try(&.node) - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/base_visitor.cr b/lib/ameba/src/ameba/ast/visitors/base_visitor.cr deleted file mode 100644 index 0b2efc6..0000000 --- a/lib/ameba/src/ameba/ast/visitors/base_visitor.cr +++ /dev/null @@ -1,28 +0,0 @@ -require "compiler/crystal/syntax/*" - -# A module that helps to traverse Crystal AST using `Crystal::Visitor`. -module Ameba::AST - # An abstract base visitor that utilizes general logic for all visitors. - abstract class BaseVisitor < Crystal::Visitor - # A corresponding rule that uses this visitor. - @rule : Rule::Base - - # A source that needs to be traversed. - @source : Source - - # Creates instance of this visitor. - # - # ``` - # visitor = Ameba::AST::NodeVisitor.new(rule, source) - # ``` - def initialize(@rule, @source) - @source.ast.accept self - end - - # A main visit method that accepts `Crystal::ASTNode`. - # Returns `true`, meaning all child nodes will be traversed. - def visit(node : Crystal::ASTNode) - true - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/counting_visitor.cr b/lib/ameba/src/ameba/ast/visitors/counting_visitor.cr deleted file mode 100644 index 347e5cc..0000000 --- a/lib/ameba/src/ameba/ast/visitors/counting_visitor.cr +++ /dev/null @@ -1,52 +0,0 @@ -module Ameba::AST - # AST Visitor that counts occurrences of certain keywords - class CountingVisitor < Crystal::Visitor - DEFAULT_COMPLEXITY = 1 - - getter? macro_condition = false - - # Creates a new counting visitor - def initialize(@scope : Crystal::ASTNode) - @complexity = DEFAULT_COMPLEXITY - end - - # :nodoc: - def visit(node : Crystal::ASTNode) - true - end - - # Returns the number of keywords that were found in the node - def count - @scope.accept(self) - @complexity - end - - # Uses the same logic than rubocop. See - # https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21 - # Except "for", because crystal doesn't have a "for" loop. - {% for node in %i[if while until rescue or and] %} - # :nodoc: - def visit(node : Crystal::{{ node.id.capitalize }}) - @complexity += 1 unless macro_condition? - end - {% end %} - - # :nodoc: - def visit(node : Crystal::Case) - return true if macro_condition? - - # Count the complexity of an exhaustive `Case` as 1 - # Otherwise count the number of `When`s - @complexity += node.exhaustive? ? 1 : node.whens.size - - true - end - - def visit(node : Crystal::MacroIf | Crystal::MacroFor) - @macro_condition = true - @complexity = DEFAULT_COMPLEXITY - - false - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/flow_expression_visitor.cr b/lib/ameba/src/ameba/ast/visitors/flow_expression_visitor.cr deleted file mode 100644 index f23d8de..0000000 --- a/lib/ameba/src/ameba/ast/visitors/flow_expression_visitor.cr +++ /dev/null @@ -1,51 +0,0 @@ -require "../util" -require "./base_visitor" - -module Ameba::AST - # AST Visitor that traverses all the flow expressions. - class FlowExpressionVisitor < BaseVisitor - include Util - - @loop_stack = [] of Crystal::ASTNode - - # :nodoc: - def visit(node) - if flow_expression?(node, in_loop?) - @rule.test @source, node, FlowExpression.new(node, in_loop?) - end - true - end - - # :nodoc: - def visit(node : Crystal::While | Crystal::Until) - on_loop_started(node) - end - - # :nodoc: - def visit(node : Crystal::Call) - on_loop_started(node) if loop?(node) - end - - # :nodoc: - def end_visit(node : Crystal::While | Crystal::Until) - on_loop_ended(node) - end - - # :nodoc: - def end_visit(node : Crystal::Call) - on_loop_ended(node) if loop?(node) - end - - private def on_loop_started(node) - @loop_stack.push(node) - end - - private def on_loop_ended(node) - @loop_stack.pop? - end - - private def in_loop? - !@loop_stack.empty? - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/node_visitor.cr b/lib/ameba/src/ameba/ast/visitors/node_visitor.cr deleted file mode 100644 index 60617b5..0000000 --- a/lib/ameba/src/ameba/ast/visitors/node_visitor.cr +++ /dev/null @@ -1,94 +0,0 @@ -require "./base_visitor" - -module Ameba::AST - # An AST Visitor that traverses the source and allows all nodes - # to be inspected by rules. - # - # ``` - # visitor = Ameba::AST::NodeVisitor.new(rule, source) - # ``` - class NodeVisitor < BaseVisitor - @[Flags] - enum Category - Macro - end - - # List of nodes to be visited by Ameba's rules. - NODES = { - Alias, - Assign, - Block, - Call, - Case, - ClassDef, - ClassVar, - Def, - EnumDef, - ExceptionHandler, - Expressions, - HashLiteral, - If, - InstanceVar, - IsA, - LibDef, - ModuleDef, - MultiAssign, - NilLiteral, - StringInterpolation, - Unless, - Until, - Var, - When, - While, - } - - @skip : Array(Crystal::ASTNode.class)? - - def self.category_to_node_classes(category : Category) - ([] of Crystal::ASTNode.class).tap do |classes| - classes.push( - Crystal::Macro, - Crystal::MacroExpression, - Crystal::MacroIf, - Crystal::MacroFor, - ) if category.macro? - end - end - - def initialize(@rule, @source, *, skip : Category) - initialize @rule, @source, - skip: NodeVisitor.category_to_node_classes(skip) - end - - def initialize(@rule, @source, *, skip : Array? = nil) - @skip = skip.try &.map(&.as(Crystal::ASTNode.class)) - super @rule, @source - end - - def visit(node : Crystal::VisibilityModifier) - node.exp.visibility = node.modifier - true - end - - {% for name in NODES %} - # A visit callback for `Crystal::{{ name }}` node. - # - # Returns `true` if the child nodes should be traversed as well, - # `false` otherwise. - def visit(node : Crystal::{{ name }}) - return false if skip?(node) - - @rule.test @source, node - true - end - {% end %} - - def visit(node) - !skip?(node) - end - - private def skip?(node) - !!@skip.try(&.includes?(node.class)) - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/redundant_control_expression_visitor.cr b/lib/ameba/src/ameba/ast/visitors/redundant_control_expression_visitor.cr deleted file mode 100644 index 43f11fa..0000000 --- a/lib/ameba/src/ameba/ast/visitors/redundant_control_expression_visitor.cr +++ /dev/null @@ -1,60 +0,0 @@ -module Ameba::AST - # A class that utilizes a logic to traverse AST nodes and - # fire a source test callback if a redundant `Crystal::ControlExpression` - # is reached. - class RedundantControlExpressionVisitor - # A corresponding rule that uses this visitor. - getter rule : Rule::Base - - # A source that needs to be traversed. - getter source : Source - - # A node to run traversal on. - getter node : Crystal::ASTNode - - def initialize(@rule, @source, @node) - traverse_node node - end - - private def traverse_control_expression(node) - @rule.test(@source, node, self) - end - - private def traverse_node(node) - case node - when Crystal::ControlExpression then traverse_control_expression node - when Crystal::Expressions then traverse_expressions node - when Crystal::If, Crystal::Unless then traverse_condition node - when Crystal::Case then traverse_case node - when Crystal::BinaryOp then traverse_binary_op node - when Crystal::ExceptionHandler then traverse_exception_handler node - end - end - - private def traverse_expressions(node) - traverse_node node.expressions.last? - end - - private def traverse_condition(node) - return if node.else.nil? || node.else.nop? - - traverse_node(node.then) - traverse_node(node.else) - end - - private def traverse_case(node) - node.whens.each { |when_node| traverse_node when_node.body } - traverse_node(node.else) - end - - private def traverse_binary_op(node) - traverse_node(node.right) - end - - private def traverse_exception_handler(node) - traverse_node node.body - traverse_node node.else - node.rescues.try &.each { |rescue_node| traverse_node rescue_node.body } - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/scope_visitor.cr b/lib/ameba/src/ameba/ast/visitors/scope_visitor.cr deleted file mode 100644 index 53ea39e..0000000 --- a/lib/ameba/src/ameba/ast/visitors/scope_visitor.cr +++ /dev/null @@ -1,204 +0,0 @@ -require "./base_visitor" - -module Ameba::AST - # AST Visitor that traverses the source and constructs scopes. - class ScopeVisitor < BaseVisitor - # Non-exhaustive list of nodes to be visited by Ameba's rules. - NODES = { - ClassDef, - ModuleDef, - EnumDef, - LibDef, - FunDef, - TypeDef, - TypeOf, - CStructOrUnionDef, - ProcLiteral, - Block, - Macro, - MacroIf, - MacroFor, - } - - SPECIAL_NODE_NAMES = %w[super previous_def] - - @scope_queue = [] of Scope - @current_scope : Scope - @current_assign : Crystal::ASTNode? - @current_visibility : Crystal::Visibility? - @skip : Array(Crystal::ASTNode.class)? - - def initialize(@rule, @source, skip = nil) - @current_scope = Scope.new(@source.ast) # top level scope - @skip = skip.try &.map(&.as(Crystal::ASTNode.class)) - - super @rule, @source - - @scope_queue.each do |scope| - @rule.test @source, scope.node, scope - end - end - - private def on_scope_enter(node) - return if skip?(node) - - scope = Scope.new(node, @current_scope) - scope.visibility = @current_visibility - - @current_scope = scope - end - - private def on_scope_end(node) - @scope_queue << @current_scope - - @current_visibility = nil - - # go up if this is not a top level scope - if outer_scope = @current_scope.outer_scope - @current_scope = outer_scope - end - end - - private def on_assign_end(target, node) - target.is_a?(Crystal::Var) && - @current_scope.assign_variable(target.name, node) - end - - # :nodoc: - def end_visit(node : Crystal::ASTNode) - on_scope_end(node) if @current_scope.eql?(node) - end - - {% for name in NODES %} - # :nodoc: - def visit(node : Crystal::{{ name }}) - on_scope_enter(node) - end - {% end %} - - # :nodoc: - def visit(node : Crystal::VisibilityModifier) - @current_visibility = node.exp.visibility = node.modifier - true - end - - # :nodoc: - def visit(node : Crystal::Yield) - @current_scope.yields = true - end - - # :nodoc: - def visit(node : Crystal::Def) - node.name == "->" || on_scope_enter(node) - end - - # :nodoc: - def visit(node : Crystal::Assign | Crystal::OpAssign | Crystal::MultiAssign | Crystal::UninitializedVar) - @current_assign = node - end - - # :nodoc: - def end_visit(node : Crystal::Assign | Crystal::OpAssign) - on_assign_end(node.target, node) - @current_assign = nil - - on_scope_end(node) if @current_scope.eql?(node) - end - - # :nodoc: - def end_visit(node : Crystal::MultiAssign) - node.targets.each { |target| on_assign_end(target, node) } - @current_assign = nil - - on_scope_end(node) if @current_scope.eql?(node) - end - - # :nodoc: - def end_visit(node : Crystal::UninitializedVar) - on_assign_end(node.var, node) - @current_assign = nil - - on_scope_end(node) if @current_scope.eql?(node) - end - - # :nodoc: - def visit(node : Crystal::TypeDeclaration) - return unless (var = node.var).is_a?(Crystal::Var) - - @current_scope.add_variable(var) - @current_scope.add_type_dec_variable(node) - - @current_assign = node.value if node.value - end - - # :nodoc: - def end_visit(node : Crystal::TypeDeclaration) - return unless (var = node.var).is_a?(Crystal::Var) - - on_assign_end(var, node) - @current_assign = nil - - on_scope_end(node) if @current_scope.eql?(node) - end - - # :nodoc: - def visit(node : Crystal::Arg) - @current_scope.add_argument(node) - end - - # :nodoc: - def visit(node : Crystal::InstanceVar) - @current_scope.add_ivariable(node) - end - - # :nodoc: - def visit(node : Crystal::Var) - variable = @current_scope.find_variable(node.name) - - case - when @current_scope.arg?(node) # node is an argument - @current_scope.add_argument(node) - when variable.nil? && @current_assign # node is a variable - @current_scope.add_variable(node) - when variable # node is a reference - reference = variable.reference(node, @current_scope) - if @current_assign.is_a?(Crystal::OpAssign) || !reference.target_of?(@current_assign) - variable.reference_assignments! - end - end - end - - # :nodoc: - def visit(node : Crystal::Call) - scope = @current_scope - - case - when scope.top_level? && record_macro?(node) then return false - when scope.type_definition? && record_macro?(node) then return false - when scope.type_definition? && accessor_macro?(node) then return false - when scope.def? && special_node?(node) - scope.arguments.each do |arg| - ref = arg.variable.reference(scope) - ref.explicit = false - 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 - - private def record_macro?(node) - node.name == "record" && node.args.first?.is_a?(Crystal::Path) - end - - private def skip?(node) - !!@skip.try(&.includes?(node.class)) - end - end -end diff --git a/lib/ameba/src/ameba/ast/visitors/top_level_nodes_visitor.cr b/lib/ameba/src/ameba/ast/visitors/top_level_nodes_visitor.cr deleted file mode 100644 index 5e5ad91..0000000 --- a/lib/ameba/src/ameba/ast/visitors/top_level_nodes_visitor.cr +++ /dev/null @@ -1,30 +0,0 @@ -module Ameba::AST - # AST Visitor that visits certain nodes at a top level, which - # can characterize the source (i.e. require statements, modules etc.) - class TopLevelNodesVisitor < Crystal::Visitor - getter require_nodes = [] of Crystal::Require - - # Creates a new instance of visitor - def initialize(@scope : Crystal::ASTNode) - @scope.accept(self) - end - - # :nodoc: - def visit(node : Crystal::Require) - require_nodes << node - true - end - - # If a top level node is `Crystal::Expressions`, - # then always traverse the children. - def visit(node : Crystal::Expressions) - true - end - - # A general visit method for rest of the nodes. - # Returns `false`, meaning all child nodes will not be traversed. - def visit(node : Crystal::ASTNode) - false - end - end -end diff --git a/lib/ameba/src/ameba/cli/cmd.cr b/lib/ameba/src/ameba/cli/cmd.cr deleted file mode 100644 index 5cb9bc0..0000000 --- a/lib/ameba/src/ameba/cli/cmd.cr +++ /dev/null @@ -1,213 +0,0 @@ -require "../../ameba" -require "option_parser" - -# :nodoc: -module Ameba::Cli - extend self - - def run(args = ARGV) - opts = parse_args args - location_to_explain = opts.location_to_explain - autocorrect = opts.autocorrect? - - if location_to_explain && autocorrect - raise "Invalid usage: Cannot explain an issue and autocorrect at the same time." - end - - config = Config.load opts.config, opts.colors?, opts.skip_reading_config? - config.autocorrect = autocorrect - - if globs = opts.globs - config.globs = globs - end - if fail_level = opts.fail_level - config.severity = fail_level - end - - configure_formatter(config, opts) - configure_rules(config, opts) - - if opts.rules? - print_rules(config.rules) - end - - if describe_rule_name = opts.describe_rule - unless rule = config.rules.find(&.name.== describe_rule_name) - raise "Unknown rule" - end - describe_rule(rule) - end - - runner = Ameba.run(config) - - if location_to_explain - runner.explain(location_to_explain) - else - exit 1 unless runner.success? - end - rescue e - puts "Error: #{e.message}" - exit 255 - end - - private class Opts - property config : Path? - property formatter : Symbol | String | Nil - property globs : Array(String)? - property only : Array(String)? - property except : Array(String)? - property describe_rule : String? - property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)? - property fail_level : Severity? - property? skip_reading_config = false - property? rules = false - property? all = false - property? colors = true - property? without_affected_code = false - property? autocorrect = false - end - - def parse_args(args, opts = Opts.new) - OptionParser.parse(args) do |parser| - parser.banner = "Usage: ameba [options] [file1 file2 ...]" - - parser.on("-v", "--version", "Print version") { print_version } - parser.on("-h", "--help", "Show this help") { print_help(parser) } - parser.on("-r", "--rules", "Show all available rules") { opts.rules = true } - parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent } - parser.unknown_args do |arr| - if arr.size == 1 && arr.first.matches?(/.+:\d+:\d+/) - configure_explain_opts(arr.first, opts) - else - opts.globs = arr unless arr.empty? - end - end - - parser.on("-c", "--config PATH", - "Specify a configuration file") do |path| - opts.config = Path[path] unless path.empty? - end - - parser.on("-f", "--format FORMATTER", - "Choose an output formatter: #{Config.formatter_names}") do |formatter| - opts.formatter = formatter - end - - parser.on("--only RULE1,RULE2,...", - "Run only given rules (or groups)") do |rules| - opts.only = rules.split(',') - end - - parser.on("--except RULE1,RULE2,...", - "Disable the given rules (or groups)") do |rules| - opts.except = rules.split(',') - end - - parser.on("--all", "Enable all available rules") do - opts.all = true - end - - parser.on("--fix", "Autocorrect issues") do - opts.autocorrect = true - end - - parser.on("--gen-config", - "Generate a configuration file acting as a TODO list") do - opts.formatter = :todo - opts.skip_reading_config = true - end - - parser.on("--fail-level SEVERITY", - "Change the level of failure to exit. Defaults to Convention") do |level| - opts.fail_level = Severity.parse(level) - end - - parser.on("-e", "--explain PATH:line:column", - "Explain an issue at a specified location") do |loc| - configure_explain_opts(loc, opts) - end - - parser.on("-d", "--describe Category/Rule", - "Describe a rule with specified name") do |rule_name| - configure_describe_opts(rule_name, opts) - end - - parser.on("--without-affected-code", - "Stop showing affected code while using a default formatter") do - opts.without_affected_code = true - end - - parser.on("--no-color", "Disable colors") do - opts.colors = false - end - end - - opts - end - - private def configure_rules(config, opts) - case - when only = opts.only - config.rules.each(&.enabled = false) - config.update_rules(only, enabled: true) - when opts.all? - config.rules.each(&.enabled = true) - end - config.update_rules(opts.except, enabled: false) - end - - private def configure_formatter(config, opts) - if name = opts.formatter - config.formatter = name - end - config.formatter.config[:autocorrect] = opts.autocorrect? - config.formatter.config[:without_affected_code] = - opts.without_affected_code? - end - - private def configure_describe_opts(rule_name, opts) - opts.describe_rule = rule_name.presence - opts.formatter = :silent - end - - private def configure_explain_opts(loc, opts) - location_to_explain = parse_explain_location(loc) - opts.location_to_explain = location_to_explain - opts.globs = [location_to_explain[:file]] - opts.formatter = :silent - end - - private def parse_explain_location(arg) - location = arg.split(':', remove_empty: true).map! &.strip - raise ArgumentError.new unless location.size === 3 - - file, line, column = location - { - file: file, - line: line.to_i, - column: column.to_i, - } - rescue - raise "location should have PATH:line:column format" - end - - private def print_version - puts VERSION - exit 0 - end - - private def print_help(parser) - puts parser - exit 0 - end - - private def describe_rule(rule) - Presenter::RulePresenter.new.run(rule) - exit 0 - end - - private def print_rules(rules) - Presenter::RuleCollectionPresenter.new.run(rules) - exit 0 - end -end diff --git a/lib/ameba/src/ameba/config.cr b/lib/ameba/src/ameba/config.cr deleted file mode 100644 index 195c705..0000000 --- a/lib/ameba/src/ameba/config.cr +++ /dev/null @@ -1,350 +0,0 @@ -require "yaml" -require "./glob_utils" - -# A configuration entry for `Ameba::Runner`. -# -# Config can be loaded from configuration YAML file and adjusted. -# -# ``` -# config = Config.load -# config.formatter = my_formatter -# ``` -# -# By default config loads `.ameba.yml` file located in a current -# working directory. -# -# If it cannot be found until reaching the root directory, then it will be -# searched for in the userโ€™s global config locations, which consists of a -# dotfile or a config file inside the XDG Base Directory specification. -# -# - `~/.ameba.yml` -# - `$XDG_CONFIG_HOME/ameba/config.yml` (expands to `~/.config/ameba/config.yml` -# if `$XDG_CONFIG_HOME` is not set) -# -# If both files exist, the dotfile will be selected. -# -# As an example, if Ameba is invoked from inside `/path/to/project/lib/utils`, -# then it will use the config as specified inside the first of the following files: -# -# - `/path/to/project/lib/utils/.ameba.yml` -# - `/path/to/project/lib/.ameba.yml` -# - `/path/to/project/.ameba.yml` -# - `/path/to/.ameba.yml` -# - `/path/.ameba.yml` -# - `/.ameba.yml` -# - `~/.ameba.yml` -# - `~/.config/ameba/config.yml` -class Ameba::Config - include GlobUtils - - AVAILABLE_FORMATTERS = { - progress: Formatter::DotFormatter, - todo: Formatter::TODOFormatter, - flycheck: Formatter::FlycheckFormatter, - silent: Formatter::BaseFormatter, - disabled: Formatter::DisabledFormatter, - json: Formatter::JSONFormatter, - } - - XDG_CONFIG_HOME = ENV.fetch("XDG_CONFIG_HOME", "~/.config") - - FILENAME = ".ameba.yml" - DEFAULT_PATH = Path[Dir.current] / FILENAME - DEFAULT_PATHS = { - Path["~"] / FILENAME, - Path[XDG_CONFIG_HOME] / "ameba/config.yml", - } - - DEFAULT_GLOBS = %w( - **/*.cr - !lib - ) - - getter rules : Array(Rule::Base) - property severity = Severity::Convention - - # Returns a list of paths (with wildcards) to files. - # Represents a list of sources to be inspected. - # If globs are not set, it will return default list of files. - # - # ``` - # config = Ameba::Config.load - # config.globs = ["**/*.cr"] - # config.globs - # ``` - property globs : Array(String) - - # Represents a list of paths to exclude from globs. - # Can have wildcards. - # - # ``` - # config = Ameba::Config.load - # config.excluded = ["spec", "src/server/*.cr"] - # ``` - property excluded : Array(String) - - # Returns `true` if correctable issues should be autocorrected. - property? autocorrect = false - - @rule_groups : Hash(String, Array(Rule::Base)) - - # Creates a new instance of `Ameba::Config` based on YAML parameters. - # - # `Config.load` uses this constructor to instantiate new config by YAML file. - protected def initialize(config : YAML::Any) - @rules = Rule.rules.map &.new(config).as(Rule::Base) - @rule_groups = @rules.group_by &.group - @excluded = load_array_section(config, "Excluded") - @globs = load_array_section(config, "Globs", DEFAULT_GLOBS) - - if formatter_name = load_formatter_name(config) - self.formatter = formatter_name - end - end - - # Loads YAML configuration file by `path`. - # - # ``` - # config = Ameba::Config.load - # ``` - def self.load(path = nil, colors = true, skip_reading_config = false) - Colorize.enabled = colors - content = if skip_reading_config - "{}" - else - read_config(path) || "{}" - end - Config.new YAML.parse(content) - rescue e - raise "Unable to load config file: #{e.message}" - end - - protected def self.read_config(path = nil) - if path - return File.read(path) if File.exists?(path) - raise "Config file does not exist" - end - each_config_path do |config_path| - return File.read(config_path) if File.exists?(config_path) - end - end - - protected def self.each_config_path(&) - path = Path[DEFAULT_PATH].expand(home: true) - - search_paths = path.parents - search_paths.reverse_each do |search_path| - yield search_path / FILENAME - end - - DEFAULT_PATHS.each do |default_path| - yield default_path - end - end - - def self.formatter_names - AVAILABLE_FORMATTERS.keys.join('|') - end - - # Returns a list of sources matching globs and excluded sections. - # - # ``` - # config = Ameba::Config.load - # config.sources # => list of default sources - # config.globs = ["**/*.cr"] - # config.excluded = ["spec"] - # config.sources # => list of sources pointing to files found by the wildcards - # ``` - def sources - (find_files_by_globs(globs) - find_files_by_globs(excluded)) - .map { |path| Source.new File.read(path), path } - end - - # Returns a formatter to be used while inspecting files. - # If formatter is not set, it will return default formatter. - # - # ``` - # config = Ameba::Config.load - # config.formatter = custom_formatter - # config.formatter - # ``` - property formatter : Formatter::BaseFormatter do - Formatter::DotFormatter.new - end - - # Sets formatter by name. - # - # ``` - # config = Ameba::Config.load - # config.formatter = :progress - # ``` - def formatter=(name : String | Symbol) - unless formatter = AVAILABLE_FORMATTERS[name]? - raise "Unknown formatter `#{name}`. Use one of #{Config.formatter_names}." - end - @formatter = formatter.new - end - - # Updates rule properties. - # - # ``` - # config = Ameba::Config.load - # config.update_rule "MyRuleName", enabled: false - # ``` - def update_rule(name, enabled = true, excluded = nil) - rule = @rules.find(&.name.==(name)) - raise ArgumentError.new("Rule `#{name}` does not exist") unless rule - - rule - .tap(&.enabled = enabled) - .tap(&.excluded = excluded) - end - - # Updates rules properties. - # - # ``` - # config = Ameba::Config.load - # config.update_rules %w[Rule1 Rule2], enabled: true - # ``` - # - # also it allows to update groups of rules: - # - # ``` - # config.update_rules %w[Group1 Group2], enabled: true - # ``` - def update_rules(names, enabled = true, excluded = nil) - names.try &.each do |name| - if rules = @rule_groups[name]? - rules.each do |rule| - rule.enabled = enabled - rule.excluded = excluded - end - else - update_rule name, enabled, excluded - end - end - end - - private def load_formatter_name(config) - name = config["Formatter"]?.try &.["Name"]? - name.try(&.to_s) - end - - private def load_array_section(config, section_name, default = [] of String) - case value = config[section_name]? - when .nil? then default - when .as_s? then [value.to_s] - when .as_a? then value.as_a.map(&.as_s) - else - raise "Incorrect '#{section_name}' section in a config files" - end - end - - # :nodoc: - module RuleConfig - # Define rule properties - macro properties(&block) - {% definitions = [] of NamedTuple %} - {% if (prop = block.body).is_a? Call %} - {% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %} - {% definitions << {var: prop.name, value: prop.args.first, type: type.value} %} - {% else %} - {% definitions << {var: prop.name, value: prop.args.first} %} - {% end %} - {% elsif block.body.is_a? Expressions %} - {% for prop in block.body.expressions %} - {% if prop.is_a? Call %} - {% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %} - {% definitions << {var: prop.name, value: prop.args.first, type: type.value} %} - {% else %} - {% definitions << {var: prop.name, value: prop.args.first} %} - {% end %} - {% end %} - {% end %} - {% end %} - - {% properties = {} of MacroId => NamedTuple %} - {% for df in definitions %} - {% name = df[:var].id %} - {% key = name.camelcase.stringify %} - {% value = df[:value] %} - {% type = df[:type] %} - {% converter = nil %} - - {% if key == "Severity" %} - {% type = Severity %} - {% converter = SeverityYamlConverter %} - {% end %} - - {% unless type %} - {% if value.is_a? BoolLiteral %} - {% type = Bool %} - {% elsif value.is_a? StringLiteral %} - {% type = String %} - {% elsif value.is_a? NumberLiteral %} - {% if value.kind == :i32 %} - {% type = Int32 %} - {% elsif value.kind == :i64 %} - {% type = Int64 %} - {% elsif value.kind == :i128 %} - {% type = Int128 %} - {% elsif value.kind == :f32 %} - {% type = Float32 %} - {% elsif value.kind == :f64 %} - {% type = Float64 %} - {% end %} - {% end %} - {% end %} - - {% properties[name] = {key: key, default: value, type: type, converter: converter} %} - - @[YAML::Field(key: {{ key }}, converter: {{ converter }})] - {% if type == Bool %} - property? {{ name }}{{ " : #{type}".id if type }} = {{ value }} - {% else %} - property {{ name }}{{ " : #{type}".id if type }} = {{ value }} - {% end %} - {% end %} - - {% unless properties["enabled".id] %} - @[YAML::Field(key: "Enabled")] - property? enabled = true - {% end %} - - {% unless properties["severity".id] %} - @[YAML::Field(key: "Severity", converter: Ameba::SeverityYamlConverter)] - property severity = {{ @type }}.default_severity - {% end %} - - {% unless properties["excluded".id] %} - @[YAML::Field(key: "Excluded")] - property excluded : Array(String)? - {% end %} - end - - macro included - GROUP_SEVERITY = { - Documentation: Ameba::Severity::Warning, - Lint: Ameba::Severity::Warning, - Metrics: Ameba::Severity::Warning, - Performance: Ameba::Severity::Warning, - } - - class_getter default_severity : Ameba::Severity do - GROUP_SEVERITY[group_name]? || Ameba::Severity::Convention - end - - macro inherited - include YAML::Serializable - include YAML::Serializable::Strict - - def self.new(config = nil) - if (raw = config.try &.raw).is_a?(Hash) - yaml = raw[rule_name]?.try &.to_yaml - end - from_yaml yaml || "{}" - end - end - end - end -end diff --git a/lib/ameba/src/ameba/ext/location.cr b/lib/ameba/src/ameba/ext/location.cr deleted file mode 100644 index 88404b8..0000000 --- a/lib/ameba/src/ameba/ext/location.cr +++ /dev/null @@ -1,35 +0,0 @@ -# Extensions to Crystal::Location -module Ameba::Ext::Location - # Returns the same location as this location but with the line and/or column number(s) changed - # to the given value(s). - def with(line_number = @line_number, column_number = @column_number) : self - self.class.new(@filename, line_number, column_number) - end - - # Returns the same location as this location but with the line and/or column number(s) adjusted - # by the given amount(s). - def adjust(line_number = 0, column_number = 0) : self - self.class.new(@filename, @line_number + line_number, @column_number + column_number) - end - - # Seeks to a given *offset* relative to `self`. - def seek(offset : self) : self - if offset.filename.as?(String).presence && @filename != offset.filename - raise ArgumentError.new <<-MSG - Mismatching filenames: - #{@filename} - #{offset.filename} - MSG - end - - if offset.line_number == 1 - self.class.new(@filename, @line_number, @column_number + offset.column_number - 1) - else - self.class.new(@filename, @line_number + offset.line_number - 1, offset.column_number) - end - end -end - -class Crystal::Location - include Ameba::Ext::Location -end diff --git a/lib/ameba/src/ameba/formatter/base_formatter.cr b/lib/ameba/src/ameba/formatter/base_formatter.cr deleted file mode 100644 index a6ceafd..0000000 --- a/lib/ameba/src/ameba/formatter/base_formatter.cr +++ /dev/null @@ -1,32 +0,0 @@ -require "./util" - -# A module that utilizes Ameba's formatters. -module Ameba::Formatter - # A base formatter for all formatters. It uses `output` IO - # to report results and also implements stub methods for - # callbacks in `Ameba::Runner#run` method. - class BaseFormatter - # TODO: allow other IOs - getter output : IO::FileDescriptor | IO::Memory - getter config = {} of Symbol => String | Bool - - def initialize(@output = STDOUT) - end - - # Callback that indicates when inspecting is started. - # A list of sources to inspect is passed as an argument. - def started(sources); end - - # Callback that indicates when source inspection is started. - # A corresponding source is passed as an argument. - def source_started(source : Source); end - - # Callback that indicates when source inspection is finished. - # A corresponding source is passed as an argument. - def source_finished(source : Source); end - - # Callback that indicates when inspection is finished. - # A list of inspected sources is passed as an argument. - def finished(sources); end - end -end diff --git a/lib/ameba/src/ameba/formatter/disabled_formatter.cr b/lib/ameba/src/ameba/formatter/disabled_formatter.cr deleted file mode 100644 index cf777ee..0000000 --- a/lib/ameba/src/ameba/formatter/disabled_formatter.cr +++ /dev/null @@ -1,17 +0,0 @@ -module Ameba::Formatter - # A formatter that shows all disabled lines by inline directives. - class DisabledFormatter < BaseFormatter - def finished(sources) - output << "Disabled rules using inline directives:\n\n" - - sources.each do |source| - source.issues.select(&.disabled?).each do |issue| - next unless loc = issue.location - - output << "#{source.path}:#{loc.line_number}".colorize(:cyan) - output << " #{issue.rule.name}\n" - end - end - end - end -end diff --git a/lib/ameba/src/ameba/formatter/dot_formatter.cr b/lib/ameba/src/ameba/formatter/dot_formatter.cr deleted file mode 100644 index 9798262..0000000 --- a/lib/ameba/src/ameba/formatter/dot_formatter.cr +++ /dev/null @@ -1,110 +0,0 @@ -require "./util" - -module Ameba::Formatter - # A formatter that shows a progress of inspection in a terminal using dots. - # It is similar to Crystal's dot formatter for specs. - class DotFormatter < BaseFormatter - include Util - - @started_at : Time::Span? - @mutex = Thread::Mutex.new - - # Reports a message when inspection is started. - def started(sources) - @started_at = Time.monotonic - - output.puts started_message(sources.size) - output.puts - end - - # Reports a result of the inspection of a corresponding source. - def source_finished(source : Source) - sym = source.valid? ? ".".colorize(:green) : "F".colorize(:red) - @mutex.synchronize { output << sym } - end - - # Reports a message when inspection is finished. - def finished(sources) - output.flush - output << "\n\n" - - show_affected_code = !config[:without_affected_code]? - failed_sources = sources.reject &.valid? - - failed_sources.each do |source| - source.issues.each do |issue| - next if issue.disabled? - next if (location = issue.location).nil? - - output.print location.colorize(:cyan) - if issue.correctable? - if config[:autocorrect]? - output.print " [Corrected]".colorize(:green) - else - output.print " [Correctable]".colorize(:yellow) - end - end - output.puts - output.puts ("[%s] %s: %s" % { - issue.rule.severity.symbol, - issue.rule.name, - issue.message, - }).colorize(issue.rule.severity.color) - - if show_affected_code && (code = affected_code(issue)) - output << code.colorize(:default) - end - - output.puts - end - end - - output.puts finished_in_message(@started_at, Time.monotonic) - output.puts final_message(sources, failed_sources) - end - - private def started_message(size) - if size == 1 - "Inspecting 1 file".colorize(:default) - else - "Inspecting #{size} files".colorize(:default) - end - end - - private def finished_in_message(started, finished) - return unless started && finished - - "Finished in #{to_human(finished - started)}".colorize(:default) - end - - private def to_human(span : Time::Span) - total_milliseconds = span.total_milliseconds - if total_milliseconds < 1 - return "#{(span.total_milliseconds * 1_000).round.to_i} microseconds" - end - - total_seconds = span.total_seconds - if total_seconds < 1 - return "#{span.total_milliseconds.round(2)} milliseconds" - end - - if total_seconds < 60 - return "#{total_seconds.round(2)} seconds" - end - - minutes = span.minutes - seconds = span.seconds - - "#{minutes}:#{seconds < 10 ? "0" : ""}#{seconds} minutes" - end - - private def final_message(sources, failed_sources) - total = sources.size - failures = failed_sources.sum(&.issues.size) - color = failures == 0 ? :green : :red - s = failures != 1 ? "s" : "" - - "#{total} inspected, #{failures} failure#{s}".colorize(color) - end - end -end diff --git a/lib/ameba/src/ameba/formatter/explain_formatter.cr b/lib/ameba/src/ameba/formatter/explain_formatter.cr deleted file mode 100644 index e963d98..0000000 --- a/lib/ameba/src/ameba/formatter/explain_formatter.cr +++ /dev/null @@ -1,100 +0,0 @@ -require "./util" - -module Ameba::Formatter - # A formatter that shows the detailed explanation of the issue at - # a specific location. - class ExplainFormatter - include Util - - getter output : IO::FileDescriptor | IO::Memory - getter location : Crystal::Location - - # Creates a new instance of `ExplainFormatter`. - # - # Accepts *output* which indicates the io where the explanation will be written to. - # Second argument is *location* which indicates the location to explain. - # - # ``` - # ExplainFormatter.new output, { - # file: path, - # line: line_number, - # column: column_number, - # } - # ``` - def initialize(@output, location) - @location = Crystal::Location.new( - location[:file], - location[:line], - location[:column] - ) - end - - # Reports the explanations at the *@location*. - def finished(sources) - source = sources.find(&.path.==(@location.filename)) - return unless source - - issue = source.issues.find(&.location.==(@location)) - return unless issue - - explain(source, issue) - end - - private def explain(source, issue) - return unless location = issue.location - - output << '\n' - output_title "Issue info" - output_paragraph [ - issue.message.colorize(:red), - location.to_s.colorize(:cyan), - ] - - if affected_code = affected_code(issue, context_lines: 3) - output_title "Affected code" - output_paragraph affected_code - end - - rule = issue.rule - - output_title "Rule info" - output_paragraph "%s of a %s severity" % { - rule.name.colorize(:magenta), - rule.severity.to_s.colorize(rule.severity.color), - } - if rule_description = colorize_code_fences(rule.description) - output_paragraph rule_description - end - - rule_doc = colorize_code_fences(rule.class.parsed_doc) - return unless rule_doc - - output_title "Detailed description" - output_paragraph rule_doc - end - - private def colorize_code_fences(string) - return unless string - string - .gsub(/```(.+?)```/m, &.colorize(:dark_gray)) - .gsub(/`(?!`)(.+?)`/, &.colorize(:dark_gray)) - end - - private def output_title(title) - output << "### ".colorize(:yellow) - output << title.upcase.colorize(:yellow) - output << "\n\n" - end - - private def output_paragraph(paragraph : String) - output_paragraph(paragraph.lines) - end - - private def output_paragraph(paragraph : Array) - paragraph.each do |line| - output << " " << line << '\n' - end - output << '\n' - end - end -end diff --git a/lib/ameba/src/ameba/formatter/flycheck_formatter.cr b/lib/ameba/src/ameba/formatter/flycheck_formatter.cr deleted file mode 100644 index 491567e..0000000 --- a/lib/ameba/src/ameba/formatter/flycheck_formatter.cr +++ /dev/null @@ -1,20 +0,0 @@ -module Ameba::Formatter - class FlycheckFormatter < BaseFormatter - @mutex = Mutex.new - - def source_finished(source : Source) - source.issues.each do |issue| - next if issue.disabled? - next if issue.correctable? && config[:autocorrect]? - - next unless loc = issue.location - - @mutex.synchronize do - output.printf "%s:%d:%d: %s: [%s] %s\n", - source.path, loc.line_number, loc.column_number, issue.rule.severity.symbol, - issue.rule.name, issue.message.gsub('\n', " ") - end - end - end - end -end diff --git a/lib/ameba/src/ameba/formatter/json_formatter.cr b/lib/ameba/src/ameba/formatter/json_formatter.cr deleted file mode 100644 index 6cd3a32..0000000 --- a/lib/ameba/src/ameba/formatter/json_formatter.cr +++ /dev/null @@ -1,170 +0,0 @@ -require "json" - -module Ameba::Formatter - # A formatter that produces the result in a json format. - # - # Example: - # - # ``` - # { - # "metadata": { - # "ameba_version": "x.x.x", - # "crystal_version": "x.x.x", - # }, - # "sources": [ - # { - # "issues": [ - # { - # "location": { - # "column": 7, - # "line": 17, - # }, - # "end_location": { - # "column": 20, - # "line": 17, - # }, - # "message": "Useless assignment to variable `a`", - # "rule_name": "UselessAssign", - # "severity": "Convention", - # }, - # { - # "location": { - # "column": 7, - # "line": 18, - # }, - # "end_location": { - # "column": 8, - # "line": 18, - # }, - # "message": "Useless assignment to variable `a`", - # "rule_name": "UselessAssign", - # }, - # { - # "location": { - # "column": 7, - # "line": 19, - # }, - # "end_location": { - # "column": 9, - # "line": 19, - # }, - # "message": "Useless assignment to variable `a`", - # "rule_name": "UselessAssign", - # "severity": "Convention", - # }, - # ], - # "path": "src/ameba/formatter/json_formatter.cr", - # }, - # ], - # "summary": { - # "issues_count": 3, - # "target_sources_count": 1, - # }, - # } - # ``` - class JSONFormatter < BaseFormatter - def initialize(@output = STDOUT) - @result = AsJSON::Result.new - end - - def started(sources) - @result.summary.target_sources_count = sources.size - end - - def source_finished(source : Source) - json_source = AsJSON::Source.new source.path - - source.issues.each do |issue| - next if issue.disabled? - next if issue.correctable? && config[:autocorrect]? - - json_source.issues << AsJSON::Issue.new( - issue.rule.name, - issue.rule.severity.to_s, - issue.location, - issue.end_location, - issue.message - ) - @result.summary.issues_count += 1 - end - - @result.sources << json_source - end - - def finished(sources) - @result.to_json @output - end - end - - private module AsJSON - record Result, - sources = [] of Source, - metadata = Metadata.new, - summary = Summary.new do - def to_json(json) - { - sources: sources, - metadata: metadata, - summary: summary, - }.to_json(json) - end - end - - record Source, - path : String, - issues = [] of Issue do - def to_json(json) - { - path: path, - issues: issues, - }.to_json(json) - end - end - - record Issue, - rule_name : String, - severity : String, - location : Crystal::Location?, - end_location : Crystal::Location?, - message : String do - def to_json(json) - { - rule_name: rule_name, - severity: severity, - message: message, - location: { - line: location.try &.line_number, - column: location.try &.column_number, - }, - end_location: { - line: end_location.try &.line_number, - column: end_location.try &.column_number, - }, - }.to_json(json) - end - end - - record Metadata, - ameba_version : String = Ameba::VERSION, - crystal_version : String = Crystal::VERSION do - def to_json(json) - { - ameba_version: ameba_version, - crystal_version: crystal_version, - }.to_json(json) - end - end - - class Summary - property target_sources_count = 0 - property issues_count = 0 - - def to_json(json) - { - target_sources_count: target_sources_count, - issues_count: issues_count, - }.to_json(json) - end - end - end -end diff --git a/lib/ameba/src/ameba/formatter/todo_formatter.cr b/lib/ameba/src/ameba/formatter/todo_formatter.cr deleted file mode 100644 index 00a2d28..0000000 --- a/lib/ameba/src/ameba/formatter/todo_formatter.cr +++ /dev/null @@ -1,75 +0,0 @@ -module Ameba::Formatter - # A formatter that creates a todo config. - # Basically, it takes all issues reported and disables corresponding rules - # or excludes failed sources from these rules. - class TODOFormatter < DotFormatter - def initialize(@output = STDOUT, @config_path : Path = Config::DEFAULT_PATH) - end - - def finished(sources) - super - - issues = sources.flat_map(&.issues) - unless issues.any? { |issue| !issue.disabled? } - @output.puts "No issues found. File is not generated." - return - end - - if issues.any?(&.syntax?) - @output.puts "Unable to generate TODO file. Please fix syntax issues." - return - end - - generate_todo_config(issues).tap do |file| - @output.puts "Created #{file.path}" - end - end - - private def generate_todo_config(issues) - File.open(@config_path, mode: "w") do |file| - file << header - - rule_issues_map(issues).each do |rule, rule_issues| - rule_todo = rule_todo(rule, rule_issues) - rule_todo = - {rule_todo.name => rule_todo} - .to_yaml.gsub("---", "") - - file << "\n# Problems found: #{rule_issues.size}" - file << "\n# Run `ameba --only #{rule.name}` for details" - file << rule_todo - end - file - end - end - - private def rule_issues_map(issues) - Hash(Rule::Base, Array(Issue)).new.tap do |hash| - issues.each do |issue| - next if issue.disabled? || issue.rule.is_a?(Rule::Lint::Syntax) - next if issue.correctable? && config[:autocorrect]? - - (hash[issue.rule] ||= Array(Issue).new) << issue - end - end - end - - private def header - <<-HEADER - # This configuration file was generated by `ameba --gen-config` - # on #{Time.utc} using Ameba version #{VERSION}. - # The point is for the user to remove these configuration records - # one by one as the reported problems are removed from the code base. - - HEADER - end - - private def rule_todo(rule, issues) - rule.dup.tap do |rule_todo| - rule_todo.excluded = issues - .compact_map(&.location.try &.filename.try &.to_s) - .uniq! - end - end - end -end diff --git a/lib/ameba/src/ameba/formatter/util.cr b/lib/ameba/src/ameba/formatter/util.cr deleted file mode 100644 index da8554a..0000000 --- a/lib/ameba/src/ameba/formatter/util.cr +++ /dev/null @@ -1,118 +0,0 @@ -module Ameba::Formatter - module Util - extend self - - def deansify(message : String?) : String? - message.try &.gsub(/\x1b[^m]*m/, "").presence - end - - def trim(str, max_length = 120, ellipsis = " ...") - if (str.size - ellipsis.size) > max_length - str = str[0, max_length] - if str.size > ellipsis.size - str = str[0...-ellipsis.size] + ellipsis - end - end - str - end - - def context(lines, lineno, context_lines = 3, remove_empty = true) - pre_context, post_context = %w[], %w[] - - lines.each_with_index do |line, i| - case i + 1 - when lineno - context_lines...lineno - pre_context << line - when lineno + 1..lineno + context_lines - post_context << line - end - end - - if remove_empty - # remove empty lines at the beginning ... - while pre_context.first?.try(&.blank?) - pre_context.shift - end - # ... and the end - while post_context.last?.try(&.blank?) - post_context.pop - end - end - - {pre_context, post_context} - end - - def affected_code(issue : Issue, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ") - return unless location = issue.location - - affected_code(issue.code, location, issue.end_location, context_lines, max_length, ellipsis, prompt) - end - - def affected_code(code, location, end_location = nil, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ") - lines = code.split('\n') # must preserve trailing newline - lineno, column = - location.line_number, location.column_number - - return unless affected_line = lines[lineno - 1]?.presence - - if column < max_length - affected_line = trim(affected_line, max_length, ellipsis) - end - - show_context = context_lines > 0 - - if show_context - pre_context, post_context = - context(lines, lineno, context_lines) - - position = prompt.size + column - position -= 1 - else - affected_line_size, affected_line = - affected_line.size, affected_line.lstrip - - position = column - (affected_line_size - affected_line.size) + prompt.size - position -= 1 - end - - String.build do |str| - if show_context - pre_context.try &.each do |line| - line = trim(line, max_length, ellipsis) - str << prompt - str.puts(line.colorize(:dark_gray)) - end - end - - str << prompt - str.puts(affected_line.colorize(:white)) - - str << (" " * position) - str << "^".colorize(:yellow) - - if end_location - end_lineno = end_location.line_number - end_column = end_location.column_number - - if end_lineno == lineno && end_column > column - end_position = end_column - column - end_position -= 1 - - str << ("-" * end_position).colorize(:dark_gray) - str << "^".colorize(:yellow) - end - end - - str.puts - - if show_context - post_context.try &.each do |line| - line = trim(line, max_length, ellipsis) - str << prompt - str.puts(line.colorize(:dark_gray)) - end - end - end - end - end -end diff --git a/lib/ameba/src/ameba/glob_utils.cr b/lib/ameba/src/ameba/glob_utils.cr deleted file mode 100644 index 71cd0cb..0000000 --- a/lib/ameba/src/ameba/glob_utils.cr +++ /dev/null @@ -1,35 +0,0 @@ -module Ameba - # Helper module that is utilizes helpers for working with globs. - module GlobUtils - # Returns all files that match specified globs. - # Globs can have wildcards or be rejected: - # - # ``` - # find_files_by_globs(["**/*.cr", "!lib"]) - # ``` - def find_files_by_globs(globs) - rejected = rejected_globs(globs) - selected = globs - rejected - - expand(selected) - expand(rejected.map!(&.[1..-1])) - end - - # Expands globs. Globs can point to files or even directories. - # - # ``` - # expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs - # ``` - def expand(globs) - globs.flat_map do |glob| - glob += "/**/*.cr" if File.directory?(glob) - Dir[glob] - end.uniq! - end - - private def rejected_globs(globs) - globs.select do |glob| - glob.starts_with?('!') && !File.exists?(glob) - end - end - end -end diff --git a/lib/ameba/src/ameba/inline_comments.cr b/lib/ameba/src/ameba/inline_comments.cr deleted file mode 100644 index 3dce089..0000000 --- a/lib/ameba/src/ameba/inline_comments.cr +++ /dev/null @@ -1,105 +0,0 @@ -module Ameba - # A module that utilizes inline comments parsing and processing logic. - module InlineComments - COMMENT_DIRECTIVE_REGEX = - /# ameba:(?\w+) (?\w+(?:\/\w+)?(?:,? \w+(?:\/\w+)?)*)/ - - # Available actions in the inline comments - enum Action - Disable - Enable - end - - # Returns `true` if current location is disabled for a particular rule, - # `false` otherwise. - # - # Location is disabled in two cases: - # 1. The line of the location ends with a comment directive. - # 2. The line above the location is a comment directive. - # - # For example, here are two examples of disabled location: - # - # ``` - # # ameba:disable Style/LargeNumbers - # Time.epoch(1483859302) - # - # Time.epoch(1483859302) # ameba:disable Style/LargeNumbers - # ``` - # - # But here are examples which are not considered as disabled location: - # - # ``` - # # ameba:disable Style/LargeNumbers - # # - # Time.epoch(1483859302) - # - # if use_epoch? # ameba:disable Style/LargeNumbers - # Time.epoch(1483859302) - # end - # ``` - def location_disabled?(location : Crystal::Location?, rule) - return false if rule.name.in?(Rule::SPECIAL) - return false unless line_number = location.try &.line_number.try &.- 1 - return false unless line = lines[line_number]? - - line_disabled?(line, rule) || - (line_number > 0 && - (prev_line = lines[line_number - 1]) && - comment?(prev_line) && - line_disabled?(prev_line, rule)) - end - - # Parses inline comment directive. Returns a tuple that consists of - # an action and parsed rules if directive found, nil otherwise. - # - # ``` - # line = "# ameba:disable Rule1, Rule2" - # directive = parse_inline_directive(line) - # directive[:action] # => "disable" - # directive[:rules] # => ["Rule1", "Rule2"] - # ``` - # - # It ignores the directive if it is commented out. - # - # ``` - # line = "# # ameba:disable Rule1, Rule2" - # parse_inline_directive(line) # => nil - # ``` - def parse_inline_directive(line) - return unless directive = COMMENT_DIRECTIVE_REGEX.match(line) - return if commented_out?(line.gsub(directive[0], "")) - { - action: directive["action"], - rules: directive["rules"].split(/[\s,]/, remove_empty: true), - } - end - - # Returns `true` if the line at the given `line_number` is a comment. - def comment?(line_number : Int32) - return unless line = lines[line_number]? - comment?(line) - end - - private def comment?(line : String) - line.lstrip.starts_with? '#' - end - - private def line_disabled?(line, rule) - return false unless directive = parse_inline_directive(line) - return false unless Action.parse?(directive[:action]).try(&.disable?) - - rules = directive[:rules] - rules.includes?(rule.name) || rules.includes?(rule.group) - end - - private def commented_out?(line) - commented = false - - lexer = Crystal::Lexer.new(line).tap(&.comments_enabled = true) - Tokenizer.new(lexer).run do |token| - commented = true if token.type.comment? - end - commented - end - end -end diff --git a/lib/ameba/src/ameba/issue.cr b/lib/ameba/src/ameba/issue.cr deleted file mode 100644 index b6fa55e..0000000 --- a/lib/ameba/src/ameba/issue.cr +++ /dev/null @@ -1,46 +0,0 @@ -module Ameba - # Represents an issue reported by Ameba. - struct Issue - enum Status - Enabled - Disabled - end - - # The source code that triggered this issue. - getter code : String - - # A rule that triggers this issue. - getter rule : Rule::Base - - # Location of the issue. - getter location : Crystal::Location? - - # End location of the issue. - getter end_location : Crystal::Location? - - # Issue message. - getter message : String - - # Issue status. - getter status : Status - - delegate :enabled?, :disabled?, - to: status - - def initialize(@code, @rule, @location, @end_location, @message, status : Status? = nil, @block : (Source::Corrector ->)? = nil) - @status = status || Status::Enabled - end - - def syntax? - rule.is_a?(Rule::Lint::Syntax) - end - - def correctable? - !@block.nil? - end - - def correct(corrector) - @block.try &.call(corrector) - end - end -end diff --git a/lib/ameba/src/ameba/presenter/base_presenter.cr b/lib/ameba/src/ameba/presenter/base_presenter.cr deleted file mode 100644 index 73f9ad0..0000000 --- a/lib/ameba/src/ameba/presenter/base_presenter.cr +++ /dev/null @@ -1,12 +0,0 @@ -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 diff --git a/lib/ameba/src/ameba/presenter/rule_collection_presenter.cr b/lib/ameba/src/ameba/presenter/rule_collection_presenter.cr deleted file mode 100644 index e833df1..0000000 --- a/lib/ameba/src/ameba/presenter/rule_collection_presenter.cr +++ /dev/null @@ -1,34 +0,0 @@ -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 diff --git a/lib/ameba/src/ameba/presenter/rule_presenter.cr b/lib/ameba/src/ameba/presenter/rule_presenter.cr deleted file mode 100644 index a790ac4..0000000 --- a/lib/ameba/src/ameba/presenter/rule_presenter.cr +++ /dev/null @@ -1,43 +0,0 @@ -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 diff --git a/lib/ameba/src/ameba/reportable.cr b/lib/ameba/src/ameba/reportable.cr deleted file mode 100644 index b60f970..0000000 --- a/lib/ameba/src/ameba/reportable.cr +++ /dev/null @@ -1,105 +0,0 @@ -require "./ast/util" - -module Ameba - # Represents a module used to report issues. - module Reportable - include AST::Util - - # List of reported issues. - getter issues = [] of Issue - - # Adds a new issue to the list of issues. - def add_issue(rule, - location : Crystal::Location?, - end_location : Crystal::Location?, - message : String, - status : Issue::Status? = nil, - block : (Source::Corrector ->)? = nil) : Issue - status ||= - Issue::Status::Disabled if location_disabled?(location, rule) - - Issue.new(code, rule, location, end_location, message, status, block).tap do |issue| - issues << issue - end - end - - # :ditto: - def add_issue(rule, - location : Crystal::Location?, - end_location : Crystal::Location?, - message : String, - status : Issue::Status? = nil, - &block : Source::Corrector ->) : Issue - add_issue rule, location, end_location, message, status, block - end - - # Adds a new issue for Crystal AST *node*. - def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil, *, prefer_name_location = false) : Issue - location = name_location(node) if prefer_name_location - location ||= node.location - - end_location = name_end_location(node) if prefer_name_location - end_location ||= node.end_location - - add_issue rule, location, end_location, message, status, block - end - - # :ditto: - def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, *, prefer_name_location = false, &block : Source::Corrector ->) : Issue - add_issue rule, node, message, status, block, prefer_name_location: prefer_name_location - end - - # Adds a new issue for Crystal *token*. - def add_issue(rule, token : Crystal::Token, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil) : Issue - add_issue rule, token.location, nil, message, status, block - end - - # :ditto: - def add_issue(rule, token : Crystal::Token, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue - add_issue rule, token, message, status, block - end - - # Adds a new issue for *location* defined by line and column numbers. - def add_issue(rule, location : {Int32, Int32}, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil) : Issue - location = - Crystal::Location.new(path, *location) - - add_issue rule, location, nil, message, status, block - end - - # :ditto: - def add_issue(rule, location : {Int32, Int32}, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue - add_issue rule, location, message, status, block - end - - # Adds a new issue for *location* and *end_location* defined by line and column numbers. - def add_issue(rule, - location : {Int32, Int32}, - end_location : {Int32, Int32}, - message, - status : Issue::Status? = nil, - block : (Source::Corrector ->)? = nil) : Issue - location = - Crystal::Location.new(path, *location) - end_location = - Crystal::Location.new(path, *end_location) - - add_issue rule, location, end_location, message, status, block - end - - # :ditto: - def add_issue(rule, - location : {Int32, Int32}, - end_location : {Int32, Int32}, - message, - status : Issue::Status? = nil, - &block : Source::Corrector ->) : Issue - add_issue rule, location, end_location, message, status, block - end - - # Returns `true` if the list of not disabled issues is empty, `false` otherwise. - def valid? - issues.none?(&.enabled?) - end - end -end diff --git a/lib/ameba/src/ameba/rule/base.cr b/lib/ameba/src/ameba/rule/base.cr deleted file mode 100644 index 242c86f..0000000 --- a/lib/ameba/src/ameba/rule/base.cr +++ /dev/null @@ -1,177 +0,0 @@ -module Ameba::Rule - # List of names of the special rules, which - # behave differently than usual rules. - SPECIAL = { - Lint::Syntax.rule_name, - Lint::UnneededDisableDirective.rule_name, - } - - # Represents a base of all rules. In other words, all rules - # inherits from this struct: - # - # ``` - # class MyRule < Ameba::Rule::Base - # def test(source) - # if invalid?(source) - # issue_for line, column, "Something wrong." - # end - # end - # - # private def invalid?(source) - # # ... - # end - # end - # ``` - # - # Enforces rules to implement an abstract `#test` method which - # is designed to test the source passed in. If source has issues - # that are tested by this rule, it should add an issue. - abstract class Base - include Config::RuleConfig - - # 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. - # - # By default it uses a node visitor to traverse all the nodes in the source. - # - # NOTE: Must be overridden for other type of rules. - def test(source : Source) - AST::NodeVisitor.new self, source - end - - # NOTE: Can't be abstract - def test(source : Source, node : Crystal::ASTNode, *opts) - end - - # A convenient addition to `#test` method that does the same - # but returns a passed in `source` as an addition. - # - # ``` - # source = MyRule.new.catch(source) - # source.valid? - # ``` - def catch(source : Source) - source.tap { test source } - end - - # Returns a name of this rule, which is basically a class name. - # - # ``` - # class MyRule < Ameba::Rule::Base - # def test(source) - # end - # end - # - # MyRule.new.name # => "MyRule" - # ``` - def name - {{ @type }}.rule_name - end - - # Returns a group this rule belong to. - # - # ``` - # class MyGroup::MyRule < Ameba::Rule::Base - # # ... - # end - # - # MyGroup::MyRule.new.group # => "MyGroup" - # ``` - def group - {{ @type }}.group_name - end - - # Checks whether the source is excluded from this rule. - # It searches for a path in `excluded` property which matches - # the one of the given source. - # - # ``` - # my_rule.excluded?(source) # => true or false - # ``` - def excluded?(source) - !!excluded.try &.any? do |path| - source.matches_path?(path) || - Dir.glob(path).any? { |glob| source.matches_path?(glob) } - end - end - - # Returns `true` if this rule is special and behaves differently than - # usual rules. - # - # ``` - # my_rule.special? # => true or false - # ``` - def special? - name.in?(SPECIAL) - end - - def ==(other) - name == other.try(&.name) - end - - def hash - name.hash - end - - # Adds an issue to the *source* - macro issue_for(*args, **kwargs, &block) - source.add_issue(self, {{ args.splat }}, {{ kwargs.double_splat }}) {{ block }} - end - - protected def self.rule_name - name.gsub("Ameba::Rule::", "").gsub("::", '/') - end - - protected def self.group_name - rule_name.split('/')[0...-1].join('/') - end - - protected def self.subclasses - {{ @type.subclasses }} - end - - protected def self.abstract? - {{ @type.abstract? }} - end - - protected def self.inherited_rules - subclasses.each_with_object([] of Base.class) do |klass, obj| - klass.abstract? ? obj.concat(klass.inherited_rules) : (obj << klass) - end - end - - private macro read_type_doc(filepath = __FILE__) - {{ run("../../contrib/read_type_doc", - @type.name.split("::").last, - filepath - ).chomp.stringify }}.presence - end - - macro inherited - # Returns documentation for this rule, if there is any. - # - # ``` - # module Ameba - # # This is a test rule. - # # Does nothing. - # class MyRule < Ameba::Rule::Base - # def test(source) - # end - # end - # end - # - # MyRule.parsed_doc # => "This is a test rule.\nDoes nothing." - # ``` - class_getter parsed_doc : String? = read_type_doc - end - end - - # Returns a list of all available rules. - # - # ``` - # Ameba::Rule.rules # => [Rule1, Rule2, ....] - # ``` - def self.rules - Base.inherited_rules - end -end diff --git a/lib/ameba/src/ameba/rule/documentation/documentation.cr b/lib/ameba/src/ameba/rule/documentation/documentation.cr deleted file mode 100644 index 45cbfc3..0000000 --- a/lib/ameba/src/ameba/rule/documentation/documentation.cr +++ /dev/null @@ -1,80 +0,0 @@ -module Ameba::Rule::Documentation - # A rule that enforces documentation for public types: - # modules, classes, enums, methods and macros. - # - # YAML configuration example: - # - # ``` - # Documentation/Documentation: - # Enabled: true - # IgnoreClasses: false - # IgnoreModules: true - # IgnoreEnums: false - # IgnoreDefs: true - # IgnoreMacros: false - # IgnoreMacroHooks: true - # ``` - class Documentation < Base - properties do - enabled false - description "Enforces public types to be documented" - - ignore_classes false - ignore_modules true - ignore_enums false - ignore_defs true - ignore_macros false - ignore_macro_hooks true - end - - MSG = "Missing documentation" - - MACRO_HOOK_NAMES = %w[ - inherited - included extended - method_missing method_added - finished - ] - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node : Crystal::ClassDef, scope : AST::Scope) - ignore_classes? || check_missing_doc(source, node, scope) - end - - def test(source, node : Crystal::ModuleDef, scope : AST::Scope) - ignore_modules? || check_missing_doc(source, node, scope) - end - - def test(source, node : Crystal::EnumDef, scope : AST::Scope) - ignore_enums? || check_missing_doc(source, node, scope) - end - - def test(source, node : Crystal::Def, scope : AST::Scope) - ignore_defs? || check_missing_doc(source, node, scope) - end - - def test(source, node : Crystal::Macro, scope : AST::Scope) - return if ignore_macro_hooks? && node.name.in?(MACRO_HOOK_NAMES) - - ignore_macros? || check_missing_doc(source, node, scope) - end - - private def check_missing_doc(source, node, scope) - # bail out if the node is not public, - # i.e. `private def foo` - return if !node.visibility.public? - - # bail out if the scope is not public, - # i.e. `def bar` inside `private class Foo` - return if (visibility = scope.visibility) && !visibility.public? - - # bail out if the node has the documentation present - return if node.doc.presence - - issue_for(node, MSG) - end - end -end diff --git a/lib/ameba/src/ameba/rule/documentation/documentation_admonition.cr b/lib/ameba/src/ameba/rule/documentation/documentation_admonition.cr deleted file mode 100644 index 7554775..0000000 --- a/lib/ameba/src/ameba/rule/documentation/documentation_admonition.cr +++ /dev/null @@ -1,96 +0,0 @@ -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*(?#{Regex.union(admonitions)})(?:\((?.+?)\))?(?:\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 diff --git a/lib/ameba/src/ameba/rule/layout/line_length.cr b/lib/ameba/src/ameba/rule/layout/line_length.cr deleted file mode 100644 index 41eb76f..0000000 --- a/lib/ameba/src/ameba/rule/layout/line_length.cr +++ /dev/null @@ -1,26 +0,0 @@ -module Ameba::Rule::Layout - # A rule that disallows lines longer than `max_length` number of symbols. - # - # YAML configuration example: - # - # ``` - # Layout/LineLength: - # Enabled: true - # MaxLength: 100 - # ``` - class LineLength < Base - properties do - enabled false - description "Disallows lines longer than `MaxLength` number of symbols" - max_length 140 - end - - MSG = "Line too long" - - def test(source) - source.lines.each_with_index do |line, index| - issue_for({index + 1, max_length + 1}, MSG) if line.size > max_length - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/layout/trailing_blank_lines.cr b/lib/ameba/src/ameba/rule/layout/trailing_blank_lines.cr deleted file mode 100644 index dbfa3aa..0000000 --- a/lib/ameba/src/ameba/rule/layout/trailing_blank_lines.cr +++ /dev/null @@ -1,39 +0,0 @@ -module Ameba::Rule::Layout - # A rule that disallows trailing blank lines at the end of the source file. - # - # YAML configuration example: - # - # ``` - # Layout/TrailingBlankLines: - # Enabled: true - # ``` - class TrailingBlankLines < Base - properties do - description "Disallows trailing blank lines" - end - - MSG = "Excessive trailing newline detected" - MSG_FINAL_NEWLINE = "Trailing newline missing" - - def test(source) - source_lines = source.lines - return if source_lines.empty? - - last_source_line = source_lines.last - source_lines_size = source_lines.size - return if source_lines_size == 1 && last_source_line.empty? - - last_line_empty = last_source_line.empty? - return if source_lines_size.zero? || - (source_lines.last(2).join.presence && last_line_empty) - - if last_line_empty - issue_for({source_lines_size, 1}, MSG) - else - issue_for({source_lines_size, 1}, MSG_FINAL_NEWLINE) do |corrector| - corrector.insert_before({source_lines_size + 1, 1}, '\n') - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/layout/trailing_whitespace.cr b/lib/ameba/src/ameba/rule/layout/trailing_whitespace.cr deleted file mode 100644 index 48561a9..0000000 --- a/lib/ameba/src/ameba/rule/layout/trailing_whitespace.cr +++ /dev/null @@ -1,30 +0,0 @@ -module Ameba::Rule::Layout - # A rule that disallows trailing whitespace. - # - # YAML configuration example: - # - # ``` - # Layout/TrailingWhitespace: - # Enabled: true - # ``` - class TrailingWhitespace < Base - properties do - description "Disallows trailing whitespace" - end - - MSG = "Trailing whitespace detected" - - def test(source) - source.lines.each_with_index do |line, index| - next unless ws_index = line =~ /\s+$/ - - location = {index + 1, ws_index + 1} - end_location = {index + 1, line.size} - - issue_for location, end_location, MSG do |corrector| - corrector.remove(location, end_location) - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/ambiguous_assignment.cr b/lib/ameba/src/ameba/rule/lint/ambiguous_assignment.cr deleted file mode 100644 index f0f1bd7..0000000 --- a/lib/ameba/src/ameba/rule/lint/ambiguous_assignment.cr +++ /dev/null @@ -1,57 +0,0 @@ -module Ameba::Rule::Lint - # This rule checks for mistyped shorthand assignments. - # - # This is considered invalid: - # - # ``` - # x = -y - # x = +y - # x = !y - # ``` - # - # And this is valid: - # - # ``` - # x -= y # or x = -y - # x += y # or x = +y - # x != y # or x = !y - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/AmbiguousAssignment: - # Enabled: true - # ``` - class AmbiguousAssignment < Base - include AST::Util - - properties do - description "Disallows ambiguous `=-/=+/=!`" - end - - MSG = "Suspicious assignment detected. Did you mean `%s`?" - - MISTAKES = { - "=-": "-=", - "=+": "+=", - "=!": "!=", - } - - def test(source, node : Crystal::Assign) - return unless op_end_location = node.value.location - - op_location = Crystal::Location.new( - op_end_location.filename, - op_end_location.line_number, - op_end_location.column_number - 1 - ) - op_text = source_between(op_location, op_end_location, source.lines) - - return unless op_text - return unless suggestion = MISTAKES[op_text]? - - issue_for op_location, op_end_location, MSG % suggestion - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/bad_directive.cr b/lib/ameba/src/ameba/rule/lint/bad_directive.cr deleted file mode 100644 index e0ff30f..0000000 --- a/lib/ameba/src/ameba/rule/lint/bad_directive.cr +++ /dev/null @@ -1,55 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports incorrect comment directives for Ameba. - # - # For example, the user can mistakenly add a directive - # to disable a rule that even doesn't exist: - # - # ``` - # # ameba:disable BadRuleName - # def foo - # :bar - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/BadDirective: - # Enabled: true - # ``` - class BadDirective < Base - properties do - description "Reports bad comment directives" - end - - AVAILABLE_ACTIONS = InlineComments::Action.names.map(&.downcase) - ALL_RULE_NAMES = Rule.rules.map(&.rule_name) - ALL_GROUP_NAMES = Rule.rules.map(&.group_name).uniq! - - def test(source) - Tokenizer.new(source).run do |token| - next unless token.type.comment? - next unless directive = source.parse_inline_directive(token.value.to_s) - - check_action source, token, directive[:action] - check_rules source, token, directive[:rules] - end - end - - private def check_action(source, token, action) - return if InlineComments::Action.parse?(action) - - issue_for token, - "Bad action in comment directive: '%s'. Possible values: %s" % { - action, AVAILABLE_ACTIONS.join(", "), - } - end - - private def check_rules(source, token, rules) - bad_names = rules - ALL_RULE_NAMES - ALL_GROUP_NAMES - return if bad_names.empty? - - issue_for token, "Such rules do not exist: %s" % bad_names.join(", ") - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/comparison_to_boolean.cr b/lib/ameba/src/ameba/rule/lint/comparison_to_boolean.cr deleted file mode 100644 index abd4efc..0000000 --- a/lib/ameba/src/ameba/rule/lint/comparison_to_boolean.cr +++ /dev/null @@ -1,61 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows comparison to booleans. - # - # For example, these are considered invalid: - # - # ``` - # foo == true - # bar != false - # false === baz - # ``` - # - # This is because these expressions evaluate to `true` or `false`, so you - # could get the same result by using either the variable directly, - # or negating the variable. - # - # YAML configuration example: - # - # ``` - # Lint/ComparisonToBoolean: - # Enabled: true - # ``` - class ComparisonToBoolean < Base - include AST::Util - - properties do - enabled false - description "Disallows comparison to booleans" - end - - MSG = "Comparison to a boolean is pointless" - OP_NAMES = %w[== != ===] - - def test(source, node : Crystal::Call) - return unless node.name.in?(OP_NAMES) - return unless node.args.size == 1 - - arg, obj = node.args.first, node.obj - case - when arg.is_a?(Crystal::BoolLiteral) - bool, exp = arg, obj - when obj.is_a?(Crystal::BoolLiteral) - bool, exp = obj, arg - end - - return unless bool && exp - return unless exp_code = node_source(exp, source.lines) - - not = - case node.name - when "==", "===" then !bool.value # foo == false - when "!=" then bool.value # foo != true - end - - exp_code = "!#{exp_code}" if not - - issue_for node, MSG do |corrector| - corrector.replace(node, exp_code) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/debug_calls.cr b/lib/ameba/src/ameba/rule/lint/debug_calls.cr deleted file mode 100644 index b295829..0000000 --- a/lib/ameba/src/ameba/rule/lint/debug_calls.cr +++ /dev/null @@ -1,32 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows calls to debug-related methods. - # - # This is because we don't want debug calls accidentally being - # committed into our codebase. - # - # YAML configuration example: - # - # ``` - # Lint/DebugCalls: - # Enabled: true - # MethodNames: - # - p - # - p! - # - pp - # - pp! - # ``` - class DebugCalls < Base - properties do - description "Disallows debug-related calls" - method_names %w[p p! pp pp!] - end - - MSG = "Possibly forgotten debug-related `%s` call detected" - - def test(source, node : Crystal::Call) - return unless node.name.in?(method_names) && node.obj.nil? - - issue_for node, MSG % node.name - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/debugger_statement.cr b/lib/ameba/src/ameba/rule/lint/debugger_statement.cr deleted file mode 100644 index da478cb..0000000 --- a/lib/ameba/src/ameba/rule/lint/debugger_statement.cr +++ /dev/null @@ -1,28 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows calls to debugger. - # - # This is because we don't want debugger breakpoints accidentally being - # committed into our codebase. - # - # YAML configuration example: - # - # ``` - # Lint/DebuggerStatement: - # Enabled: true - # ``` - class DebuggerStatement < Base - properties do - description "Disallows calls to debugger" - end - - MSG = "Possible forgotten debugger statement detected" - - def test(source, node : Crystal::Call) - return unless node.name == "debugger" && - node.args.empty? && - node.obj.nil? - - issue_for node, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/duplicated_require.cr b/lib/ameba/src/ameba/rule/lint/duplicated_require.cr deleted file mode 100644 index d1fe129..0000000 --- a/lib/ameba/src/ameba/rule/lint/duplicated_require.cr +++ /dev/null @@ -1,31 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports duplicated require statements. - # - # ``` - # require "./thing" - # require "./stuff" - # require "./thing" # duplicated require - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/DuplicatedRequire: - # Enabled: true - # ``` - class DuplicatedRequire < Base - properties do - description "Reports duplicated require statements" - end - - MSG = "Duplicated require of `%s`" - - def test(source) - nodes = AST::TopLevelNodesVisitor.new(source.ast).require_nodes - nodes.each_with_object([] of String) do |node, processed_require_strings| - issue_for(node, MSG % node.string) if node.string.in?(processed_require_strings) - processed_require_strings << node.string - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/empty_ensure.cr b/lib/ameba/src/ameba/rule/lint/empty_ensure.cr deleted file mode 100644 index dce5dfa..0000000 --- a/lib/ameba/src/ameba/rule/lint/empty_ensure.cr +++ /dev/null @@ -1,54 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows empty ensure statement. - # - # For example, this is considered invalid: - # - # ``` - # def some_method - # do_some_stuff - # ensure - # end - # - # begin - # do_some_stuff - # ensure - # end - # ``` - # - # And it should be written as this: - # - # ``` - # def some_method - # do_some_stuff - # ensure - # do_something_else - # end - # - # begin - # do_some_stuff - # ensure - # do_something_else - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/EmptyEnsure - # Enabled: true - # ``` - class EmptyEnsure < Base - properties do - description "Disallows empty ensure statement" - end - - MSG = "Empty `ensure` block detected" - - def test(source, node : Crystal::ExceptionHandler) - node_ensure = node.ensure - return if node_ensure.nil? || !node_ensure.nop? - - issue_for node.ensure_location, node.end_location, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/empty_expression.cr b/lib/ameba/src/ameba/rule/lint/empty_expression.cr deleted file mode 100644 index bac570c..0000000 --- a/lib/ameba/src/ameba/rule/lint/empty_expression.cr +++ /dev/null @@ -1,42 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows empty expressions. - # - # This is considered invalid: - # - # ``` - # foo = () - # - # if () - # bar - # end - # ``` - # - # And this is valid: - # - # ``` - # foo = (some_expression) - # - # if (some_expression) - # bar - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/EmptyExpression: - # Enabled: true - # ``` - class EmptyExpression < Base - properties do - description "Disallows empty expressions" - end - - MSG = "Avoid empty expressions" - - def test(source, node : Crystal::Expressions) - return unless node.expressions.size == 1 && node.expressions.first.nop? - issue_for node, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/empty_loop.cr b/lib/ameba/src/ameba/rule/lint/empty_loop.cr deleted file mode 100644 index 32d9b5a..0000000 --- a/lib/ameba/src/ameba/rule/lint/empty_loop.cr +++ /dev/null @@ -1,64 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows empty loops. - # - # This is considered invalid: - # - # ``` - # while false - # end - # - # until 10 - # end - # - # loop do - # # nothing here - # end - # ``` - # - # And this is valid: - # - # ``` - # a = 1 - # while a < 10 - # a += 1 - # end - # - # until socket_opened? - # end - # - # loop do - # do_something_here - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/EmptyLoop: - # Enabled: true - # ``` - class EmptyLoop < Base - include AST::Util - - properties do - description "Disallows empty loops" - end - - MSG = "Empty loop detected" - - def test(source, node : Crystal::Call) - check_node(source, node, node.block) if loop?(node) - end - - def test(source, node : Crystal::While | Crystal::Until) - check_node(source, node, node.body) if literal?(node.cond) - end - - private def check_node(source, node, loop_body) - body = loop_body.is_a?(Crystal::Block) ? loop_body.body : loop_body - return unless body.nil? || body.nop? - - issue_for node, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/formatting.cr b/lib/ameba/src/ameba/rule/lint/formatting.cr deleted file mode 100644 index ea848ad..0000000 --- a/lib/ameba/src/ameba/rule/lint/formatting.cr +++ /dev/null @@ -1,65 +0,0 @@ -require "compiler/crystal/formatter" - -module Ameba::Rule::Lint - # A rule that verifies syntax formatting according to the - # Crystal's built-in formatter. - # - # For example, this syntax is invalid: - # - # def foo(a,b,c=0) - # #foobar - # a+b+c - # end - # - # And should be properly written: - # - # def foo(a, b, c = 0) - # # foobar - # a + b + c - # end - # - # YAML configuration example: - # - # ``` - # Lint/Formatting: - # Enabled: true - # FailOnError: false - # ``` - class Formatting < Base - properties do - description "Reports not formatted sources" - fail_on_error false - end - - MSG = "Use built-in formatter to format this source" - MSG_ERROR = "Error while formatting: %s" - - private LOCATION = {1, 1} - - def test(source) - source_code = source.code - result = Crystal.format(source_code, source.path) - return if result == source_code - - source_lines = source_code.lines - return if source_lines.empty? - - end_location = { - source_lines.size, - source_lines.last.size + 1, - } - - issue_for LOCATION, MSG do |corrector| - corrector.replace(LOCATION, end_location, result) - end - rescue ex : Crystal::SyntaxException - if fail_on_error? - issue_for({ex.line_number, ex.column_number}, MSG_ERROR % ex.message) - end - rescue ex - if fail_on_error? - issue_for(LOCATION, MSG_ERROR % ex.message) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/hash_duplicated_key.cr b/lib/ameba/src/ameba/rule/lint/hash_duplicated_key.cr deleted file mode 100644 index 5b335ed..0000000 --- a/lib/ameba/src/ameba/rule/lint/hash_duplicated_key.cr +++ /dev/null @@ -1,42 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows duplicated keys in hash literals. - # - # This is considered invalid: - # - # ``` - # h = {"foo" => 1, "bar" => 2, "foo" => 3} - # ``` - # - # And it has to written as this instead: - # - # ``` - # h = {"foo" => 1, "bar" => 2} - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/HashDuplicatedKey: - # Enabled: true - # ``` - class HashDuplicatedKey < Base - properties do - description "Disallows duplicated keys in hash literals" - end - - MSG = "Duplicated keys in hash literal: %s" - - def test(source, node : Crystal::HashLiteral) - return if (keys = duplicated_keys(node.entries)).empty? - - issue_for node, MSG % keys.join(", ") - end - - private def duplicated_keys(entries) - entries.map(&.key) - .group_by(&.itself) - .select! { |_, v| v.size > 1 } - .map { |k, _| k } - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/literal_assignments_in_expressions.cr b/lib/ameba/src/ameba/rule/lint/literal_assignments_in_expressions.cr deleted file mode 100644 index 1851bb0..0000000 --- a/lib/ameba/src/ameba/rule/lint/literal_assignments_in_expressions.cr +++ /dev/null @@ -1,43 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows assignments with literal values - # in control expressions. - # - # For example, this is considered invalid: - # - # ``` - # if foo = 42 - # do_something - # end - # ``` - # - # And most likely should be replaced by the following: - # - # ``` - # if foo == 42 - # do_something - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/LiteralAssignmentsInExpressions: - # Enabled: true - # ``` - class LiteralAssignmentsInExpressions < Base - include AST::Util - - properties do - description "Disallows assignments with literal values in control expressions" - end - - MSG = "Detected assignment with a literal value in control expression" - - def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until) - return unless (cond = node.cond).is_a?(Crystal::Assign) - return unless literal?(cond.value) - - issue_for cond, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/literal_in_condition.cr b/lib/ameba/src/ameba/rule/lint/literal_in_condition.cr deleted file mode 100644 index 3be1c15..0000000 --- a/lib/ameba/src/ameba/rule/lint/literal_in_condition.cr +++ /dev/null @@ -1,37 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows useless conditional statements that contain a literal - # in place of a variable or predicate function. - # - # This is because a conditional construct with a literal predicate will - # always result in the same behaviour at run time, meaning it can be - # replaced with either the body of the construct, or deleted entirely. - # - # This is considered invalid: - # - # ``` - # if "something" - # :ok - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/LiteralInCondition: - # Enabled: true - # ``` - class LiteralInCondition < Base - include AST::Util - - properties do - description "Disallows useless conditional statements that contain \ - a literal in place of a variable or predicate function" - end - - MSG = "Literal value found in conditional" - - def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case) - issue_for node, MSG if static_literal?(node.cond) - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/literal_in_interpolation.cr b/lib/ameba/src/ameba/rule/lint/literal_in_interpolation.cr deleted file mode 100644 index fce2150..0000000 --- a/lib/ameba/src/ameba/rule/lint/literal_in_interpolation.cr +++ /dev/null @@ -1,33 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows useless string interpolations - # that contain a literal value instead of a variable or function. - # - # For example: - # - # ``` - # "Hello, #{:Ary}" - # "There are #{4} cats" - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/LiteralInInterpolation - # Enabled: true - # ``` - class LiteralInInterpolation < Base - include AST::Util - - properties do - description "Disallows useless string interpolations" - end - - MSG = "Literal value found in interpolation" - - def test(source, node : Crystal::StringInterpolation) - node.expressions - .select { |exp| !exp.is_a?(Crystal::StringLiteral) && literal?(exp) } - .each { |exp| issue_for exp, MSG } - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/literals_comparison.cr b/lib/ameba/src/ameba/rule/lint/literals_comparison.cr deleted file mode 100644 index 9f9a7fe..0000000 --- a/lib/ameba/src/ameba/rule/lint/literals_comparison.cr +++ /dev/null @@ -1,53 +0,0 @@ -module Ameba::Rule::Lint - # This rule is used to identify comparisons between two literals. - # - # They usually have the same result - except for non-primitive - # types like containers, range or regex. - # - # For example, this will be always false: - # - # ``` - # "foo" == 42 - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/LiteralsComparison: - # Enabled: true - # ``` - class LiteralsComparison < Base - include AST::Util - - properties do - description "Identifies comparisons between literals" - end - - OP_NAMES = %w[=== == !=] - - MSG = "Comparison always evaluates to %s" - MSG_LIKELY = "Comparison most likely evaluates to %s" - - def test(source, node : Crystal::Call) - return unless node.name.in?(OP_NAMES) - return unless (obj = node.obj) && (arg = node.args.first?) - - obj_is_literal, obj_is_static = literal_kind?(obj) - arg_is_literal, arg_is_static = literal_kind?(arg) - - return unless obj_is_literal && arg_is_literal - return unless obj.to_s == arg.to_s - - is_dynamic = !obj_is_static || !arg_is_static - - what = - case node.name - when "===" then "the same" - when "==" then "true" - when "!=" then "false" - end - - issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/missing_block_argument.cr b/lib/ameba/src/ameba/rule/lint/missing_block_argument.cr deleted file mode 100644 index 5fd6176..0000000 --- a/lib/ameba/src/ameba/rule/lint/missing_block_argument.cr +++ /dev/null @@ -1,40 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows yielding method definitions without block argument. - # - # For example, this is considered invalid: - # - # def foo - # yield 42 - # end - # - # And has to be written as the following: - # - # def foo(&) - # yield 42 - # end - # - # YAML configuration example: - # - # ``` - # Lint/MissingBlockArgument: - # Enabled: true - # ``` - class MissingBlockArgument < Base - properties do - description "Disallows yielding method definitions without block argument" - end - - MSG = "Missing anonymous block argument. Use `&` as an argument " \ - "name to indicate yielding method." - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node : Crystal::Def, scope : AST::Scope) - return if !scope.yields? || node.block_arg - - issue_for node, MSG, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/not_nil.cr b/lib/ameba/src/ameba/rule/lint/not_nil.cr deleted file mode 100644 index 1ab405e..0000000 --- a/lib/ameba/src/ameba/rule/lint/not_nil.cr +++ /dev/null @@ -1,46 +0,0 @@ -module Ameba::Rule::Lint - # This rule is used to identify usages of `not_nil!` calls. - # - # For example, this is considered a code smell: - # - # ``` - # names = %w[Alice Bob] - # alice = names.find { |name| name == "Alice" }.not_nil! - # ``` - # - # And can be written as this: - # - # ``` - # names = %w[Alice Bob] - # alice = names.find { |name| name == "Alice" } - # - # if alice - # # ... - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/NotNil: - # Enabled: true - # ``` - class NotNil < Base - properties do - description "Identifies usage of `not_nil!` calls" - end - - MSG = "Avoid using `not_nil!`" - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "not_nil!" - return unless node.obj && node.args.empty? - - issue_for node, MSG, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/not_nil_after_no_bang.cr b/lib/ameba/src/ameba/rule/lint/not_nil_after_no_bang.cr deleted file mode 100644 index 1f9f339..0000000 --- a/lib/ameba/src/ameba/rule/lint/not_nil_after_no_bang.cr +++ /dev/null @@ -1,56 +0,0 @@ -module Ameba::Rule::Lint - # This rule is used to identify usage of `index/rindex/find/match` calls - # followed by a call to `not_nil!`. - # - # For example, this is considered a code smell: - # - # ``` - # %w[Alice Bob].find(&.chars.any?(&.in?('o', 'b'))).not_nil! - # ``` - # - # And can be written as this: - # - # ``` - # %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b'))) - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/NotNilAfterNoBang: - # Enabled: true - # ``` - class NotNilAfterNoBang < Base - include AST::Util - - properties do - description "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`" - end - - MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`" - - BLOCK_CALL_NAMES = %w[index rindex find] - CALL_NAMES = %w[index rindex match] - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "not_nil!" && node.args.empty? - return unless (obj = node.obj).is_a?(Crystal::Call) - return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES) - - return unless name_location = name_location(obj) - return unless name_location_end = name_end_location(obj) - return unless end_location = name_end_location(node) - - msg = MSG % {obj.name, obj.name} - - issue_for name_location, end_location, msg do |corrector| - corrector.insert_after(name_location_end, '!') - corrector.remove_trailing(node, {{ ".not_nil!".size }}) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/percent_array.cr b/lib/ameba/src/ameba/rule/lint/percent_array.cr deleted file mode 100644 index 987d87d..0000000 --- a/lib/ameba/src/ameba/rule/lint/percent_array.cr +++ /dev/null @@ -1,69 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows some unwanted symbols in percent array literals. - # - # For example, this is usually written by mistake: - # - # ``` - # %i[:one, :two] - # %w["one", "two"] - # ``` - # - # And the expected example is: - # - # ``` - # %i[one two] - # %w[one two] - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/PercentArrays: - # Enabled: true - # StringArrayUnwantedSymbols: ',"' - # SymbolArrayUnwantedSymbols: ',:' - # ``` - class PercentArrays < Base - properties do - description "Disallows some unwanted symbols in percent array literals" - - string_array_unwanted_symbols %(,") - symbol_array_unwanted_symbols %(,:) - end - - MSG = "Symbols `%s` may be unwanted in %s array literals" - - def test(source) - issue = start_token = nil - - Tokenizer.new(source).run do |token| - case token.type - when .string_array_start?, .symbol_array_start? - start_token = token.dup - when .string? - if (_start = start_token) && !issue - issue = array_entry_invalid?(token.value.to_s, _start.raw) - end - when .string_array_end? - if (_start = start_token) && (_issue = issue) - issue_for _start, _issue - end - issue = start_token = nil - end - end - end - - private def array_entry_invalid?(entry, array_type) - case array_type - when .starts_with? "%w" - check_array_entry entry, string_array_unwanted_symbols, "%w" - when .starts_with? "%i" - check_array_entry entry, symbol_array_unwanted_symbols, "%i" - end - end - - private def check_array_entry(entry, symbols, literal) - MSG % {symbols, literal} if entry.matches?(/[#{Regex.escape(symbols)}]/) - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/rand_zero.cr b/lib/ameba/src/ameba/rule/lint/rand_zero.cr deleted file mode 100644 index 5293704..0000000 --- a/lib/ameba/src/ameba/rule/lint/rand_zero.cr +++ /dev/null @@ -1,41 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows `rand(0)` and `rand(1)` calls. - # Such calls always return `0`. - # - # For example: - # - # ``` - # rand(1) - # ``` - # - # Should be written as: - # - # ``` - # rand - # # or - # rand(2) - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/RandZero: - # Enabled: true - # ``` - class RandZero < Base - properties do - description "Disallows rand zero calls" - end - - MSG = "%s always returns 0" - - def test(source, node : Crystal::Call) - return unless node.name == "rand" && - node.args.size == 1 && - (arg = node.args.first).is_a?(Crystal::NumberLiteral) && - arg.value.in?("0", "1") - - issue_for node, MSG % node - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/redundant_string_coercion.cr b/lib/ameba/src/ameba/rule/lint/redundant_string_coercion.cr deleted file mode 100644 index 0a48608..0000000 --- a/lib/ameba/src/ameba/rule/lint/redundant_string_coercion.cr +++ /dev/null @@ -1,48 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows string conversion in string interpolation, - # which is redundant. - # - # For example, this is considered invalid: - # - # ``` - # "Hello, #{name.to_s}" - # ``` - # - # And this is valid: - # - # ``` - # "Hello, #{name}" - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/RedundantStringCoercion - # Enabled: true - # ``` - class RedundantStringCoercion < Base - include AST::Util - - properties do - description "Disallows redundant string conversions in interpolation" - end - - MSG = "Redundant use of `Object#to_s` in interpolation" - - def test(source, node : Crystal::StringInterpolation) - string_coercion_nodes(node).each do |expr| - issue_for name_location(expr), expr.end_location, MSG - end - end - - private def string_coercion_nodes(node) - node.expressions.select do |exp| - exp.is_a?(Crystal::Call) && - exp.name == "to_s" && - exp.args.size.zero? && - exp.named_args.nil? && - exp.obj - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/redundant_with_index.cr b/lib/ameba/src/ameba/rule/lint/redundant_with_index.cr deleted file mode 100644 index ac42e4f..0000000 --- a/lib/ameba/src/ameba/rule/lint/redundant_with_index.cr +++ /dev/null @@ -1,57 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows redundant `with_index` calls. - # - # For example, this is considered invalid: - # - # ``` - # collection.each.with_index do |e| - # # ... - # end - # - # collection.each_with_index do |e, _| - # # ... - # end - # ``` - # - # and it should be written as follows: - # - # ``` - # collection.each do |e| - # # ... - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/RedundantWithIndex: - # Enabled: true - # ``` - class RedundantWithIndex < Base - properties do - description "Disallows redundant `with_index` calls" - end - - def test(source, node : Crystal::Call) - args, block = node.args, node.block - - return if block.nil? || args.size > 1 - return if with_index_arg?(block) - - case node.name - when "with_index" - report source, node, "Remove redundant with_index" - when "each_with_index" - report source, node, "Use each instead of each_with_index" - end - end - - private def with_index_arg?(block : Crystal::Block) - block.args.size >= 2 && block.args.last.name != "_" - end - - private def report(source, node, msg) - issue_for node, msg, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/redundant_with_object.cr b/lib/ameba/src/ameba/rule/lint/redundant_with_object.cr deleted file mode 100644 index 7dfbb96..0000000 --- a/lib/ameba/src/ameba/rule/lint/redundant_with_object.cr +++ /dev/null @@ -1,50 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows redundant `each_with_object` calls. - # - # For example, this is considered invalid: - # - # ``` - # collection.each_with_object(0) do |e| - # # ... - # end - # - # collection.each_with_object(0) do |e, _| - # # ... - # end - # ``` - # - # and it should be written as follows: - # - # ``` - # collection.each do |e| - # # ... - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/RedundantWithObject: - # Enabled: true - # ``` - class RedundantWithObject < Base - properties do - description "Disallows redundant `with_object` calls" - end - - MSG = "Use `each` instead of `each_with_object`" - - def test(source, node : Crystal::Call) - return if node.name != "each_with_object" || - node.args.size != 1 || - !(block = node.block) || - with_index_arg?(block) - - issue_for node, MSG, prefer_name_location: true - end - - private def with_index_arg?(block : Crystal::Block) - block.args.size >= 2 && block.args.last.name != "_" - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/shadowed_argument.cr b/lib/ameba/src/ameba/rule/lint/shadowed_argument.cr deleted file mode 100644 index c21c1da..0000000 --- a/lib/ameba/src/ameba/rule/lint/shadowed_argument.cr +++ /dev/null @@ -1,57 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows shadowed arguments. - # - # For example, this is considered invalid: - # - # ``` - # do_something do |foo| - # foo = 1 # shadows block argument - # foo - # end - # - # def do_something(foo) - # foo = 1 # shadows method argument - # foo - # end - # ``` - # - # and it should be written as follows: - # - # ``` - # do_something do |foo| - # foo = foo + 42 - # foo - # end - # - # def do_something(foo) - # foo = foo + 42 - # foo - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/ShadowedArgument: - # Enabled: true - # ``` - class ShadowedArgument < Base - properties do - description "Disallows shadowed arguments" - end - - MSG = "Argument `%s` is assigned before it is used" - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node, scope : AST::Scope) - scope.arguments.each do |arg| - next unless assign = arg.variable.assign_before_reference - - issue_for assign, MSG % arg.name - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/shadowed_exception.cr b/lib/ameba/src/ameba/rule/lint/shadowed_exception.cr deleted file mode 100644 index b6708a2..0000000 --- a/lib/ameba/src/ameba/rule/lint/shadowed_exception.cr +++ /dev/null @@ -1,83 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows a rescued exception that get shadowed by a - # less specific exception being rescued before a more specific - # exception is rescued. - # - # For example, this is invalid: - # - # ``` - # begin - # do_something - # rescue Exception - # handle_exception - # rescue ArgumentError - # handle_argument_error_exception - # end - # ``` - # - # And it has to be written as follows: - # - # ``` - # begin - # do_something - # rescue ArgumentError - # handle_argument_error_exception - # rescue Exception - # handle_exception - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/ShadowedException: - # Enabled: true - # ``` - class ShadowedException < Base - properties do - description "Disallows rescued exception that get shadowed" - end - - MSG = "Shadowed exception found: %s" - - def test(source, node : Crystal::ExceptionHandler) - rescues = node.rescues - return if rescues.nil? - - shadowed(rescues).each do |path| - issue_for path, MSG % path.names.join("::") - end - end - - private def shadowed(rescues, catch_all = false) - traversed_types = Set(String).new - - rescues = filter_rescues(rescues) - rescues.each_with_object([] of Crystal::Path) do |types, shadowed| - case - when catch_all - shadowed.concat(types) - next - when types.any?(&.single?("Exception")) - nodes = types.reject(&.single?("Exception")) - shadowed.concat(nodes) unless nodes.empty? - catch_all = true - next - else - nodes = types.select { |path| traverse(path.to_s, traversed_types) } - shadowed.concat(nodes) unless nodes.empty? - end - end - end - - private def filter_rescues(rescues) - rescues.compact_map(&.types.try &.select(Crystal::Path)) - end - - private def traverse(path, traversed_types) - dup = traversed_types.includes?(path) - dup || (traversed_types << path) - dup - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/shadowing_outer_local_var.cr b/lib/ameba/src/ameba/rule/lint/shadowing_outer_local_var.cr deleted file mode 100644 index 1ac1c23..0000000 --- a/lib/ameba/src/ameba/rule/lint/shadowing_outer_local_var.cr +++ /dev/null @@ -1,69 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows the usage of the same name as outer local variables - # for block or proc arguments. - # - # For example, this is considered incorrect: - # - # ``` - # def some_method - # foo = 1 - # - # 3.times do |foo| # shadowing outer `foo` - # end - # end - # ``` - # - # and should be written as: - # - # ``` - # def some_method - # foo = 1 - # - # 3.times do |bar| - # end - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/ShadowingOuterLocalVar: - # Enabled: true - # ``` - class ShadowingOuterLocalVar < Base - properties do - description "Disallows the usage of the same name as outer local variables " \ - "for block or proc arguments" - end - - MSG = "Shadowing outer local variable `%s`" - - def test(source) - AST::ScopeVisitor.new self, source, skip: [ - Crystal::Macro, - Crystal::MacroFor, - ] - end - - def test(source, node : Crystal::ProcLiteral | Crystal::Block, scope : AST::Scope) - find_shadowing source, scope - end - - private def find_shadowing(source, scope) - return unless outer_scope = scope.outer_scope - - scope.arguments.reject(&.ignored?).each do |arg| - # TODO: handle unpacked variables from `Block#unpacks` - next unless name = arg.name.presence - - variable = outer_scope.find_variable(name) - - next if variable.nil? || !variable.declared_before?(arg) - next if outer_scope.assigns_ivar?(name) - next if outer_scope.assigns_type_dec?(name) - - issue_for arg.node, MSG % name - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/shared_var_in_fiber.cr b/lib/ameba/src/ameba/rule/lint/shared_var_in_fiber.cr deleted file mode 100644 index 2a2bd16..0000000 --- a/lib/ameba/src/ameba/rule/lint/shared_var_in_fiber.cr +++ /dev/null @@ -1,85 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows using shared variables in fibers, - # which are mutated during iterations. - # - # In most cases it leads to unexpected behaviour and is undesired. - # - # For example, having this example: - # - # ``` - # n = 0 - # channel = Channel(Int32).new - # - # while n < 3 - # n = n + 1 - # spawn { channel.send n } - # end - # - # 3.times { puts channel.receive } # => # 3, 3, 3 - # ``` - # - # The problem is there is only one shared between fibers variable `n` - # and when `channel.receive` is executed its value is `3`. - # - # To solve this, the code above needs to be rewritten to the following: - # - # ``` - # n = 0 - # channel = Channel(Int32).new - # - # while n < 3 - # n = n + 1 - # m = n - # spawn do { channel.send m } - # end - # - # 3.times { puts channel.receive } # => # 1, 2, 3 - # ``` - # - # This rule is able to find the shared variables between fibers, which are mutated - # during iterations. So it reports the issue on the first sample and passes on - # the second one. - # - # There are also other techniques to solve the problem above which are - # [officially documented](https://crystal-lang.org/reference/guides/concurrency.html) - # - # YAML configuration example: - # - # ``` - # Lint/SharedVarInFiber: - # Enabled: true - # ``` - class SharedVarInFiber < Base - properties do - description "Disallows shared variables in fibers" - end - - MSG = "Shared variable `%s` is used in fiber" - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node, scope : AST::Scope) - return unless scope.spawn_block? - - scope.references.each do |ref| - next if (variable = scope.find_variable(ref.name)).nil? - next if variable.scope == scope || !mutated_in_loop?(variable) - - issue_for ref.node, MSG % variable.name - end - end - - # Variable is mutated in loop if it was declared above the loop and assigned inside. - private def mutated_in_loop?(variable) - declared_in = variable.assignments.first?.try &.branch - - variable.assignments - .reject(&.scope.spawn_block?) - .any? do |assign| - assign.branch.try(&.in_loop?) && assign.branch != declared_in - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/spec_filename.cr b/lib/ameba/src/ameba/rule/lint/spec_filename.cr deleted file mode 100644 index 9711806..0000000 --- a/lib/ameba/src/ameba/rule/lint/spec_filename.cr +++ /dev/null @@ -1,50 +0,0 @@ -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 diff --git a/lib/ameba/src/ameba/rule/lint/spec_focus.cr b/lib/ameba/src/ameba/rule/lint/spec_focus.cr deleted file mode 100644 index b165145..0000000 --- a/lib/ameba/src/ameba/rule/lint/spec_focus.cr +++ /dev/null @@ -1,72 +0,0 @@ -module Ameba::Rule::Lint - # Checks if specs are focused. - # - # In specs `focus: true` is mainly used to focus on a spec - # item locally during development. However, if such change - # is committed, it silently runs only focused spec on all - # other environment, which is undesired. - # - # This is considered bad: - # - # ``` - # describe MyClass, focus: true do - # end - # - # describe ".new", focus: true do - # end - # - # context "my context", focus: true do - # end - # - # it "works", focus: true do - # end - # ``` - # - # And it should be written as the following: - # - # ``` - # describe MyClass do - # end - # - # describe ".new" do - # end - # - # context "my context" do - # end - # - # it "works" do - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/SpecFocus: - # Enabled: true - # ``` - class SpecFocus < Base - properties do - description "Reports focused spec items" - end - - MSG = "Focused spec item detected" - - SPEC_ITEM_NAMES = %w[describe context it pending] - - def test(source) - return unless source.spec? - - AST::NodeVisitor.new self, source - end - - def test(source, node : Crystal::Call) - return unless node.name.in?(SPEC_ITEM_NAMES) - return unless node.block - - arg = node.named_args.try &.find(&.name.== "focus") - return unless arg - - issue_for arg, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/syntax.cr b/lib/ameba/src/ameba/rule/lint/syntax.cr deleted file mode 100644 index 14e7ba4..0000000 --- a/lib/ameba/src/ameba/rule/lint/syntax.cr +++ /dev/null @@ -1,33 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports invalid Crystal syntax. - # - # For example, this syntax is invalid: - # - # ``` - # def hello - # do_something - # rescue Exception => e - # end - # ``` - # - # And should be properly written: - # - # ``` - # def hello - # do_something - # rescue e : Exception - # end - # ``` - class Syntax < Base - properties do - description "Reports invalid Crystal syntax" - severity :error - end - - def test(source) - source.ast - rescue e : Crystal::SyntaxException - issue_for({e.line_number, e.column_number}, e.message.to_s) - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/typos.cr b/lib/ameba/src/ameba/rule/lint/typos.cr deleted file mode 100644 index 2371aff..0000000 --- a/lib/ameba/src/ameba/rule/lint/typos.cr +++ /dev/null @@ -1,97 +0,0 @@ -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 diff --git a/lib/ameba/src/ameba/rule/lint/unneeded_disable_directive.cr b/lib/ameba/src/ameba/rule/lint/unneeded_disable_directive.cr deleted file mode 100644 index a32215a..0000000 --- a/lib/ameba/src/ameba/rule/lint/unneeded_disable_directive.cr +++ /dev/null @@ -1,67 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports unneeded disable directives. - # For example, this is considered invalid: - # - # ``` - # # ameba:disable Style/PredicateName - # def comment? - # do_something - # end - # ``` - # - # as the predicate name is correct and the comment directive does not - # have any effect, the snippet should be written as the following: - # - # ``` - # def comment? - # do_something - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UnneededDisableDirective - # Enabled: true - # ``` - class UnneededDisableDirective < Base - properties do - description "Reports unneeded disable directives in comments" - end - - MSG = "Unnecessary disabling of %s" - - def test(source) - Tokenizer.new(source).run do |token| - next unless token.type.comment? - next unless directive = source.parse_inline_directive(token.value.to_s) - next unless names = unneeded_disables(source, directive, token.location) - next if names.empty? - - issue_for token, MSG % names.join(", ") - end - end - - private def unneeded_disables(source, directive, location) - return unless directive[:action] == "disable" - - directive[:rules].reject do |rule_name| - next if rule_name == self.name - source.issues.any? do |issue| - issue.rule.name == rule_name && - issue.disabled? && - issue_at_location?(source, issue, location) - end - end - end - - private def issue_at_location?(source, issue, location) - return false unless issue_line_number = issue.location.try(&.line_number) - - issue_line_number == location.line_number || - ((prev_line_number = issue_line_number - 1) && - prev_line_number == location.line_number && - source.comment?(prev_line_number - 1)) - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/unreachable_code.cr b/lib/ameba/src/ameba/rule/lint/unreachable_code.cr deleted file mode 100644 index f3c2843..0000000 --- a/lib/ameba/src/ameba/rule/lint/unreachable_code.cr +++ /dev/null @@ -1,60 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports unreachable code. - # - # For example, this is considered invalid: - # - # ``` - # def method(a) - # return 42 - # a + 1 - # end - # ``` - # - # ``` - # a = 1 - # loop do - # break - # a += 1 - # end - # ``` - # - # And has to be written as the following: - # - # ``` - # def method(a) - # return 42 if a == 0 - # a + 1 - # end - # ``` - # - # ``` - # a = 1 - # loop do - # break a > 3 - # a += 1 - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UnreachableCode: - # Enabled: true - # ``` - class UnreachableCode < Base - properties do - description "Reports unreachable code" - end - - MSG = "Unreachable code detected" - - def test(source) - AST::FlowExpressionVisitor.new self, source - end - - def test(source, node, flow_expression : AST::FlowExpression) - return unless unreachable_node = flow_expression.unreachable_nodes.first? - issue_for unreachable_node, MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/unused_argument.cr b/lib/ameba/src/ameba/rule/lint/unused_argument.cr deleted file mode 100644 index d001a1a..0000000 --- a/lib/ameba/src/ameba/rule/lint/unused_argument.cr +++ /dev/null @@ -1,84 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports unused arguments. - # For example, this is considered invalid: - # - # ``` - # def method(a, b, c) - # a + b - # end - # ``` - # - # and should be written as: - # - # ``` - # def method(a, b) - # a + b - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UnusedArgument: - # Enabled: true - # IgnoreDefs: true - # IgnoreBlocks: false - # IgnoreProcs: false - # ``` - class UnusedArgument < Base - properties do - description "Disallows unused arguments" - - ignore_defs true - ignore_blocks false - ignore_procs false - end - - MSG = "Unused argument `%s`. If it's necessary, use `%s` " \ - "as an argument name to indicate that it won't be used." - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node : Crystal::ProcLiteral, scope : AST::Scope) - ignore_procs? || find_unused_arguments(source, scope) - end - - def test(source, node : Crystal::Block, scope : AST::Scope) - ignore_blocks? || find_unused_arguments(source, scope) - end - - def test(source, node : Crystal::Def, scope : AST::Scope) - arguments = scope.arguments.dup - - # `Lint/UnusedBlockArgument` rule covers this case explicitly - if block_arg = node.block_arg - arguments.reject!(&.node.== block_arg) - end - - ignore_defs? || find_unused_arguments(source, scope, arguments) - end - - private def find_unused_arguments(source, scope, arguments = scope.arguments) - arguments.each do |argument| - next if argument.anonymous? || argument.ignored? - next if scope.references?(argument.variable) - - name_suggestion = scope.node.is_a?(Crystal::Block) ? '_' : "_#{argument.name}" - message = MSG % {argument.name, name_suggestion} - - location = argument.node.location - end_location = location.try &.adjust(column_number: argument.name.size - 1) - - if location && end_location - issue_for argument.node, message do |corrector| - corrector.replace(location, end_location, name_suggestion) - end - else - issue_for argument.node, message - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/unused_block_argument.cr b/lib/ameba/src/ameba/rule/lint/unused_block_argument.cr deleted file mode 100644 index 65a7255..0000000 --- a/lib/ameba/src/ameba/rule/lint/unused_block_argument.cr +++ /dev/null @@ -1,79 +0,0 @@ -module Ameba::Rule::Lint - # A rule that reports unused block arguments. - # For example, this is considered invalid: - # - # ``` - # def foo(a, b, &block) - # a + b - # end - # - # def bar(&block) - # yield 42 - # end - # ``` - # - # and should be written as: - # - # ``` - # def foo(a, b, &_block) - # a + b - # end - # - # def bar(&) - # yield 42 - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UnusedBlockArgument: - # Enabled: true - # ``` - class UnusedBlockArgument < Base - properties do - description "Disallows unused block arguments" - end - - MSG_UNUSED = "Unused block argument `%1$s`. If it's necessary, use `_%1$s` " \ - "as an argument name to indicate that it won't be used." - - MSG_YIELDED = "Use `&` as an argument name to indicate that it won't be referenced." - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node : Crystal::Def, scope : AST::Scope) - return if node.abstract? - - return unless block_arg = node.block_arg - return unless block_arg = scope.arguments.find(&.node.== block_arg) - - return if block_arg.anonymous? - return if scope.references?(block_arg.variable) - - location = block_arg.node.location - end_location = location.try &.adjust(column_number: block_arg.name.size - 1) - - case - when scope.yields? - if location && end_location - issue_for location, end_location, MSG_YIELDED do |corrector| - corrector.remove(location, end_location) - end - else - issue_for block_arg.node, MSG_YIELDED - end - when !block_arg.ignored? - if location && end_location - issue_for location, end_location, MSG_UNUSED % block_arg.name do |corrector| - corrector.insert_before(location, '_') - end - else - issue_for block_arg.node, MSG_UNUSED % block_arg.name - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/useless_assign.cr b/lib/ameba/src/ameba/rule/lint/useless_assign.cr deleted file mode 100644 index ed7d536..0000000 --- a/lib/ameba/src/ameba/rule/lint/useless_assign.cr +++ /dev/null @@ -1,53 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows useless assignments. - # - # For example, this is considered invalid: - # - # ``` - # def method - # var = 1 - # do_something - # end - # ``` - # - # And has to be written as the following: - # - # ``` - # def method - # var = 1 - # do_something(var) - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UselessAssign: - # Enabled: true - # ExcludeTypeDeclarations: false - # ``` - class UselessAssign < Base - properties do - description "Disallows useless variable assignments" - exclude_type_declarations false - end - - MSG = "Useless assignment to variable `%s`" - - def test(source) - AST::ScopeVisitor.new self, source - end - - def test(source, node, scope : AST::Scope) - scope.variables.each do |var| - next if var.ignored? || var.used_in_macro? || var.captured_by_block? - next if exclude_type_declarations? && scope.assigns_type_dec?(var.name) - - var.assignments.each do |assign| - next if assign.referenced? - issue_for assign.target_node, MSG % var.name - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/lint/useless_condition_in_when.cr b/lib/ameba/src/ameba/rule/lint/useless_condition_in_when.cr deleted file mode 100644 index da3f47a..0000000 --- a/lib/ameba/src/ameba/rule/lint/useless_condition_in_when.cr +++ /dev/null @@ -1,74 +0,0 @@ -module Ameba::Rule::Lint - # A rule that disallows useless conditions in when clause - # where it is guaranteed to always return the same result. - # - # For example, this is considered invalid: - # - # ``` - # case - # when utc? - # io << " UTC" - # when local? - # Format.new(" %:z").format(self, io) if local? - # end - # ``` - # - # And has to be written as the following: - # - # ``` - # case - # when utc? - # io << " UTC" - # when local? - # Format.new(" %:z").format(self, io) - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Lint/UselessConditionInWhen: - # Enabled: true - # ``` - class UselessConditionInWhen < Base - properties do - description "Disallows useless conditions in when" - end - - MSG = "Useless condition in when detected" - - # TODO: condition *cond* may be a complex ASTNode with - # useless inner conditions. We might need to improve this - # simple implementation in future. - protected def check_node(source, when_node, cond) - return unless cond_s = cond.to_s.presence - return if when_node.conds.none?(&.to_s.==(cond_s)) - - issue_for cond, MSG - end - - def test(source, node : Crystal::When) - ConditionInWhenVisitor.new self, source, node - end - - # :nodoc: - private class ConditionInWhenVisitor < Crystal::Visitor - @source : Source - @rule : UselessConditionInWhen - @parent : Crystal::When - - def initialize(@rule, @source, @parent) - @parent.accept self - end - - def visit(node : Crystal::If | Crystal::Unless) - @rule.check_node(@source, @parent, node.cond) - true - end - - def visit(node : Crystal::ASTNode) - true - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/metrics/cyclomatic_complexity.cr b/lib/ameba/src/ameba/rule/metrics/cyclomatic_complexity.cr deleted file mode 100644 index 51d6abc..0000000 --- a/lib/ameba/src/ameba/rule/metrics/cyclomatic_complexity.cr +++ /dev/null @@ -1,26 +0,0 @@ -module Ameba::Rule::Metrics - # A rule that disallows methods with a cyclomatic complexity higher than `MaxComplexity` - # - # YAML configuration example: - # - # ``` - # Metrics/CyclomaticComplexity: - # Enabled: true - # MaxComplexity: 10 - # ``` - class CyclomaticComplexity < Base - properties do - description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`" - max_complexity 10 - end - - MSG = "Cyclomatic complexity too high [%d/%d]" - - def test(source, node : Crystal::Def) - complexity = AST::CountingVisitor.new(node).count - return unless complexity > max_complexity - - issue_for node, MSG % {complexity, max_complexity}, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/accessor_method_name.cr b/lib/ameba/src/ameba/rule/naming/accessor_method_name.cr deleted file mode 100644 index 55b9cbd..0000000 --- a/lib/ameba/src/ameba/rule/naming/accessor_method_name.cr +++ /dev/null @@ -1,77 +0,0 @@ -module Ameba::Rule::Naming - # A rule that makes sure that accessor methods are named properly. - # - # Favour this: - # - # ``` - # class Foo - # def user - # @user - # end - # - # def user=(value) - # @user = value - # end - # end - # ``` - # - # Over this: - # - # ``` - # class Foo - # def get_user - # @user - # end - # - # def set_user(value) - # @user = value - # end - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/AccessorMethodName: - # Enabled: true - # ``` - class AccessorMethodName < Base - properties do - description "Makes sure that accessor methods are named properly" - end - - MSG = "Favour method name '%s' over '%s'" - - def test(source, node : Crystal::ClassDef | Crystal::ModuleDef) - defs = - case body = node.body - when Crystal::Def - [body] - when Crystal::Expressions - body.expressions.select(Crystal::Def) - end - - defs.try &.each do |def_node| - # skip defs with explicit receiver, as they'll be handled - # by the `test(source, node : Crystal::Def)` overload - check_issue(source, def_node) unless def_node.receiver - end - end - - def test(source, node : Crystal::Def) - # check only defs with explicit receiver (`def self.foo`) - check_issue(source, node) if node.receiver - end - - private def check_issue(source, node : Crystal::Def) - case node.name - when /^get_([a-z]\w*)$/ - return unless node.args.empty? - issue_for node, MSG % {$1, node.name}, prefer_name_location: true - when /^set_([a-z]\w*)$/ - return unless node.args.size == 1 - issue_for node, MSG % {"#{$1}=", node.name}, prefer_name_location: true - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/ascii_identifiers.cr b/lib/ameba/src/ameba/rule/naming/ascii_identifiers.cr deleted file mode 100644 index f2739b4..0000000 --- a/lib/ameba/src/ameba/rule/naming/ascii_identifiers.cr +++ /dev/null @@ -1,87 +0,0 @@ -module Ameba::Rule::Naming - # A rule that reports non-ascii characters in identifiers. - # - # Favour this: - # - # ``` - # class BigAwesomeWolf - # end - # ``` - # - # Over this: - # - # ``` - # class BigAwesome๐Ÿบ - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/AsciiIdentifiers: - # Enabled: true - # IgnoreSymbols: false - # ``` - class AsciiIdentifiers < Base - properties do - description "Disallows non-ascii characters in identifiers" - ignore_symbols false - end - - MSG = "Identifier contains non-ascii characters" - - def test(source, node : Crystal::Assign) - if (target = node.target).is_a?(Crystal::Path) - check_issue(source, target, target) - end - check_symbol_literal(source, node.value) - end - - def test(source, node : Crystal::MultiAssign) - node.values.each do |value| - check_symbol_literal(source, value) - end - end - - def test(source, node : Crystal::Call) - node.args.each do |arg| - check_symbol_literal(source, arg) - end - node.named_args.try &.each do |arg| - check_symbol_literal(source, arg.value) - end - end - - def test(source, node : Crystal::Def) - check_issue(source, node, prefer_name_location: true) - - node.args.each do |arg| - check_issue(source, arg, prefer_name_location: true) - check_symbol_literal(source, arg.default_value) - end - end - - def test(source, node : Crystal::ClassVar | Crystal::InstanceVar | Crystal::Var | Crystal::Alias) - check_issue(source, node, prefer_name_location: true) - end - - def test(source, node : Crystal::ClassDef | Crystal::ModuleDef | Crystal::EnumDef | Crystal::LibDef) - check_issue(source, node.name, node.name) - end - - private def check_symbol_literal(source, node) - return if ignore_symbols? - return unless node.is_a?(Crystal::SymbolLiteral) - - check_issue(source, node, node.value) - end - - private def check_issue(source, location, end_location, name) - issue_for location, end_location, MSG unless name.to_s.ascii_only? - end - - private def check_issue(source, node, name = node.name, *, prefer_name_location = false) - issue_for node, MSG, prefer_name_location: prefer_name_location unless name.to_s.ascii_only? - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/binary_operator_parameter_name.cr b/lib/ameba/src/ameba/rule/naming/binary_operator_parameter_name.cr deleted file mode 100644 index 16cc1ed..0000000 --- a/lib/ameba/src/ameba/rule/naming/binary_operator_parameter_name.cr +++ /dev/null @@ -1,50 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces that certain binary operator methods have - # their sole parameter named `other`. - # - # For example, this is considered valid: - # - # ``` - # class Money - # def +(other) - # end - # end - # ``` - # - # And this is invalid parameter name: - # - # ``` - # class Money - # def +(amount) - # end - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/BinaryOperatorParameterName: - # Enabled: true - # ExcludedOperators: ["[]", "[]?", "[]=", "<<", ">>", "=~", "!~"] - # ``` - class BinaryOperatorParameterName < Base - properties do - description "Enforces that certain binary operator methods have " \ - "their sole parameter named `other`" - excluded_operators %w[[] []? []= << >> ` =~ !~] - end - - MSG = "When defining the `%s` operator, name its argument `other`" - - def test(source, node : Crystal::Def) - name = node.name - - return if name == "->" || name.in?(excluded_operators) - return if name.chars.any?(&.alphanumeric?) - return unless node.args.size == 1 - return if (arg = node.args.first).name == "other" - - issue_for arg, MSG % name - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/block_parameter_name.cr b/lib/ameba/src/ameba/rule/naming/block_parameter_name.cr deleted file mode 100644 index 71e6a6a..0000000 --- a/lib/ameba/src/ameba/rule/naming/block_parameter_name.cr +++ /dev/null @@ -1,54 +0,0 @@ -module Ameba::Rule::Naming - # A rule that reports non-descriptive block parameter names. - # - # Favour this: - # - # ``` - # tokens.each { |token| token.last_accessed_at = Time.utc } - # ``` - # - # Over this: - # - # ``` - # tokens.each { |t| t.last_accessed_at = Time.utc } - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/BlockParameterName: - # Enabled: true - # MinNameLength: 3 - # AllowNamesEndingInNumbers: true - # AllowedNames: [_, e, i, j, k, v, x, y, ex, io, ws, op, tx, id, ip, k1, k2, v1, v2] - # ForbiddenNames: [] - # ``` - class BlockParameterName < Base - properties do - description "Disallows non-descriptive block parameter names" - min_name_length 3 - allow_names_ending_in_numbers true - allowed_names %w[_ e i j k v x y ex io ws op tx id ip k1 k2 v1 v2] - forbidden_names %w[] - end - - MSG = "Disallowed block parameter name found" - - def test(source, node : Crystal::Call) - node.try(&.block).try(&.args).try &.each do |arg| - issue_for arg, MSG unless valid_name?(arg.name) - end - end - - private def valid_name?(name) - return true if name.blank? # TODO: handle unpacked variables - return true if name.in?(allowed_names) - - return false if name.in?(forbidden_names) - return false if name.size < min_name_length - return false if name[-1].ascii_number? && !allow_names_ending_in_numbers? - - true - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/constant_names.cr b/lib/ameba/src/ameba/rule/naming/constant_names.cr deleted file mode 100644 index 88f815f..0000000 --- a/lib/ameba/src/ameba/rule/naming/constant_names.cr +++ /dev/null @@ -1,42 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces constant names to be in screaming case. - # - # For example, these constant names are considered valid: - # - # ``` - # LUCKY_NUMBERS = [3, 7, 11] - # DOCUMENTATION_URL = "http://crystal-lang.org/docs" - # ``` - # - # And these are invalid names: - # - # ``` - # myBadConstant = 1 - # Wrong_NAME = 2 - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/ConstantNames: - # Enabled: true - # ``` - class ConstantNames < Base - properties do - description "Enforces constant names to be in screaming case" - end - - MSG = "Constant name should be screaming-cased: %s, not %s" - - def test(source, node : Crystal::Assign) - return unless (target = node.target).is_a?(Crystal::Path) - - name = target.to_s - expected = name.upcase - - return if name.in?(expected, name.camelcase) - - issue_for target, MSG % {expected, name} - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/filename.cr b/lib/ameba/src/ameba/rule/naming/filename.cr deleted file mode 100644 index 1c73b76..0000000 --- a/lib/ameba/src/ameba/rule/naming/filename.cr +++ /dev/null @@ -1,28 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces file names to be in underscored case. - # - # YAML configuration example: - # - # ``` - # Naming/Filename: - # Enabled: true - # ``` - class Filename < Base - properties do - description "Enforces file names to be in underscored case" - end - - MSG = "Filename should be underscore-cased: %s, not %s" - - private LOCATION = {1, 1} - - def test(source : Source) - path = Path[source.path] - name = path.basename - - return if (expected = name.underscore) == name - - issue_for LOCATION, MSG % {expected, name} - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/method_names.cr b/lib/ameba/src/ameba/rule/naming/method_names.cr deleted file mode 100644 index d434d9a..0000000 --- a/lib/ameba/src/ameba/rule/naming/method_names.cr +++ /dev/null @@ -1,55 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces method names to be in underscored case. - # - # For example, these are considered valid: - # - # ``` - # class Person - # def first_name - # end - # - # def date_of_birth - # end - # - # def homepage_url - # end - # end - # ``` - # - # And these are invalid method names: - # - # ``` - # class Person - # def firstName - # end - # - # def date_of_Birth - # end - # - # def homepageURL - # end - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/MethodNames: - # Enabled: true - # ``` - class MethodNames < Base - properties do - description "Enforces method names to be in underscored case" - end - - MSG = "Method name should be underscore-cased: %s, not %s" - - def test(source, node : Crystal::Def) - name = node.name.to_s - - return if (expected = name.underscore) == name - - issue_for node, MSG % {expected, name}, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/predicate_name.cr b/lib/ameba/src/ameba/rule/naming/predicate_name.cr deleted file mode 100644 index b3935f2..0000000 --- a/lib/ameba/src/ameba/rule/naming/predicate_name.cr +++ /dev/null @@ -1,40 +0,0 @@ -module Ameba::Rule::Naming - # A rule that disallows tautological predicate names - - # meaning those that start with the prefix `is_`, except for - # the ones that are not valid Crystal code (e.g. `is_404?`). - # - # Favour this: - # - # ``` - # def valid?(x) - # end - # ``` - # - # Over this: - # - # ``` - # def is_valid?(x) - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/PredicateName: - # Enabled: true - # ``` - class PredicateName < Base - properties do - description "Disallows tautological predicate names" - end - - MSG = "Favour method name '%s?' over '%s'" - - def test(source, node : Crystal::Def) - return unless node.name =~ /^is_([a-z]\w*)\??$/ - alternative = $1 - - issue_for node, MSG % {alternative, node.name}, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/query_bool_methods.cr b/lib/ameba/src/ameba/rule/naming/query_bool_methods.cr deleted file mode 100644 index a0ecaeb..0000000 --- a/lib/ameba/src/ameba/rule/naming/query_bool_methods.cr +++ /dev/null @@ -1,70 +0,0 @@ -module Ameba::Rule::Naming - # A rule that disallows boolean properties without the `?` suffix - defined - # using `Object#(class_)property` or `Object#(class_)getter` macros. - # - # Favour this: - # - # ``` - # class Person - # property? deceased = false - # getter? witty = true - # end - # ``` - # - # Over this: - # - # ``` - # class Person - # property deceased = false - # getter witty = true - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/QueryBoolMethods: - # Enabled: true - # ``` - class QueryBoolMethods < Base - include AST::Util - - properties do - description "Reports boolean properties without the `?` suffix" - end - - MSG = "Consider using '%s?' for '%s'" - - CALL_NAMES = %w[getter class_getter property class_property] - - def test(source, node : Crystal::ClassDef | Crystal::ModuleDef) - calls = - case body = node.body - when Crystal::Call - [body] if body.name.in?(CALL_NAMES) - when Crystal::Expressions - body.expressions - .select(Crystal::Call) - .select!(&.name.in?(CALL_NAMES)) - end - - calls.try &.each do |exp| - exp.args.each do |arg| - name_node, is_bool = - case arg - when Crystal::Assign - {arg.target, arg.value.is_a?(Crystal::BoolLiteral)} - when Crystal::TypeDeclaration - {arg.var, path_named?(arg.declared_type, "Bool")} - else - {nil, false} - end - - if name_node && is_bool - issue_for name_node, MSG % {exp.name, name_node} - end - end - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/rescued_exceptions_variable_name.cr b/lib/ameba/src/ameba/rule/naming/rescued_exceptions_variable_name.cr deleted file mode 100644 index f1230c5..0000000 --- a/lib/ameba/src/ameba/rule/naming/rescued_exceptions_variable_name.cr +++ /dev/null @@ -1,51 +0,0 @@ -module Ameba::Rule::Naming - # A rule that makes sure that rescued exceptions variables are named as expected. - # - # For example, these are considered valid: - # - # def foo - # # potentially raising computations - # rescue e - # Log.error(exception: e) { "Error" } - # end - # - # And these are invalid variable names: - # - # def foo - # # potentially raising computations - # rescue wtf - # Log.error(exception: wtf) { "Error" } - # end - # - # YAML configuration example: - # - # ``` - # Naming/RescuedExceptionsVariableName: - # Enabled: true - # AllowedNames: [e, ex, exception, error] - # ``` - class RescuedExceptionsVariableName < Base - properties do - description "Makes sure that rescued exceptions variables are named as expected" - allowed_names %w[e ex exception error] - end - - MSG = "Disallowed variable name, use one of these instead: '%s'" - MSG_SINGULAR = "Disallowed variable name, use '%s' instead" - - def test(source, node : Crystal::ExceptionHandler) - node.rescues.try &.each do |rescue_node| - next if valid_name?(rescue_node.name) - - message = - allowed_names.size == 1 ? MSG_SINGULAR : MSG - - issue_for rescue_node, message % allowed_names.join("', '") - end - end - - private def valid_name?(name) - !name || name.in?(allowed_names) - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/type_names.cr b/lib/ameba/src/ameba/rule/naming/type_names.cr deleted file mode 100644 index fb47247..0000000 --- a/lib/ameba/src/ameba/rule/naming/type_names.cr +++ /dev/null @@ -1,69 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces type names in camelcase manner. - # - # For example, these are considered valid: - # - # ``` - # class ParseError < Exception - # end - # - # module HTTP - # class RequestHandler - # end - # end - # - # alias NumericValue = Float32 | Float64 | Int32 | Int64 - # - # lib LibYAML - # end - # - # struct TagDirective - # end - # - # enum Time::DayOfWeek - # end - # ``` - # - # And these are invalid type names - # - # ``` - # class My_class - # end - # - # module HTT_p - # end - # - # alias Numeric_value = Int32 - # - # lib Lib_YAML - # end - # - # struct Tag_directive - # end - # - # enum Time_enum::Day_of_week - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/TypeNames: - # Enabled: true - # ``` - class TypeNames < Base - properties do - description "Enforces type names in camelcase manner" - end - - MSG = "Type name should be camelcased: %s, but it was %s" - - def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef) - name = node.name.to_s - - return if (expected = name.camelcase) == name - - issue_for node.name, MSG % {expected, name} - end - end -end diff --git a/lib/ameba/src/ameba/rule/naming/variable_names.cr b/lib/ameba/src/ameba/rule/naming/variable_names.cr deleted file mode 100644 index fe28b3e..0000000 --- a/lib/ameba/src/ameba/rule/naming/variable_names.cr +++ /dev/null @@ -1,59 +0,0 @@ -module Ameba::Rule::Naming - # A rule that enforces variable names to be in underscored case. - # - # For example, these variable names are considered valid: - # - # ``` - # var_name = 1 - # name = 2 - # _another_good_name = 3 - # ``` - # - # And these are invalid variable names: - # - # ``` - # myBadNamedVar = 1 - # wrong_Name = 2 - # ``` - # - # YAML configuration example: - # - # ``` - # Naming/VariableNames: - # Enabled: true - # ``` - class VariableNames < Base - properties do - description "Enforces variable names to be in underscored case" - end - - MSG = "Var name should be underscore-cased: %s, not %s" - - def test(source : Source) - VarVisitor.new self, source - end - - def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar) - name = node.name.to_s - - return if (expected = name.underscore) == name - - issue_for node, MSG % {expected, name} - end - - private class VarVisitor < AST::NodeVisitor - private getter var_locations = [] of Crystal::Location - - def visit(node : Crystal::Var) - !node.location.in?(var_locations) && super - end - - def visit(node : Crystal::InstanceVar | Crystal::ClassVar) - if location = node.location - var_locations << location - end - super - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/any_after_filter.cr b/lib/ameba/src/ameba/rule/performance/any_after_filter.cr deleted file mode 100644 index c51f6b2..0000000 --- a/lib/ameba/src/ameba/rule/performance/any_after_filter.cr +++ /dev/null @@ -1,47 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `any?` calls that follow filters. - # - # For example, this is considered invalid: - # - # ``` - # [1, 2, 3].select { |e| e > 2 }.any? - # [1, 2, 3].reject { |e| e >= 2 }.any? - # ``` - # - # And it should be written as this: - # - # ``` - # [1, 2, 3].any? { |e| e > 2 } - # [1, 2, 3].any? { |e| e < 2 } - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/AnyAfterFilter: - # Enabled: true - # FilterNames: - # - select - # - reject - # ``` - class AnyAfterFilter < Base - include AST::Util - - properties do - description "Identifies usage of `any?` calls that follow filters" - filter_names %w[select reject] - end - - MSG = "Use `any? {...}` instead of `%s {...}.any?`" - - def test(source, node : Crystal::Call) - return unless node.name == "any?" && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block && node.block.nil? - return unless obj.name.in?(filter_names) - - issue_for name_location(obj), name_end_location(node), MSG % obj.name - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/any_instead_of_empty.cr b/lib/ameba/src/ameba/rule/performance/any_instead_of_empty.cr deleted file mode 100644 index 88ecc10..0000000 --- a/lib/ameba/src/ameba/rule/performance/any_instead_of_empty.cr +++ /dev/null @@ -1,45 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of arg-less `Enumerable#any?` calls. - # - # Using `Enumerable#any?` instead of `Enumerable#empty?` might lead to an - # unexpected results (like `[nil, false].any? # => false`). In some cases - # it also might be less efficient, since it iterates until the block will - # return a _truthy_ value, instead of just checking if there's at least - # one value present. - # - # For example, this is considered invalid: - # - # ``` - # [1, 2, 3].any? - # ``` - # - # And it should be written as this: - # - # ``` - # ![1, 2, 3].empty? - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/AnyInsteadOfEmpty: - # Enabled: true - # ``` - class AnyInsteadOfEmpty < Base - properties do - description "Identifies usage of arg-less `any?` calls" - end - - MSG = "Use `!{...}.empty?` instead of `{...}.any?`" - - def test(source, node : Crystal::Call) - return unless node.name == "any?" - return unless node.block.nil? && node.args.empty? - return unless node.obj - - issue_for node, MSG, prefer_name_location: true - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/base.cr b/lib/ameba/src/ameba/rule/performance/base.cr deleted file mode 100644 index 405cf97..0000000 --- a/lib/ameba/src/ameba/rule/performance/base.cr +++ /dev/null @@ -1,10 +0,0 @@ -require "../base" - -module Ameba::Rule::Performance - # A general base class for performance rules. - abstract class Base < Ameba::Rule::Base - def catch(source : Source) - source.spec? ? source : super - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/chained_call_with_no_bang.cr b/lib/ameba/src/ameba/rule/performance/chained_call_with_no_bang.cr deleted file mode 100644 index a5d2d0e..0000000 --- a/lib/ameba/src/ameba/rule/performance/chained_call_with_no_bang.cr +++ /dev/null @@ -1,79 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of chained calls not utilizing - # the bang method variants. - # - # For example, this is considered inefficient: - # - # ``` - # names = %w[Alice Bob] - # chars = names - # .flat_map(&.chars) - # .uniq - # .sort - # ``` - # - # And can be written as this: - # - # ``` - # names = %w[Alice Bob] - # chars = names - # .flat_map(&.chars) - # .uniq! - # .sort! - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/ChainedCallWithNoBang: - # Enabled: true - # CallNames: - # - uniq - # - sort - # - sort_by - # - shuffle - # - reverse - # ``` - class ChainedCallWithNoBang < Base - include AST::Util - - properties do - description "Identifies usage of chained calls not utilizing the bang method variants" - - # All of those have bang method variants returning `self` - # and are not modifying the receiver type (like `compact` does), - # thus are safe to switch to the bang variant. - call_names %w[uniq sort sort_by shuffle reverse] - end - - MSG = "Use bang method variant `%s!` after chained `%s` call" - - # All these methods allocate a new object - ALLOCATING_METHOD_NAMES = %w[ - keys values values_at map map_with_index flat_map compact_map - flatten compact select reject sample group_by chunks tally merge - combinations repeated_combinations permutations repeated_permutations - transpose invert chars captures named_captures clone - ] - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless (obj = node.obj).is_a?(Crystal::Call) - return unless node.name.in?(call_names) - return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES) - - if end_location = name_end_location(node) - issue_for node, MSG % {node.name, obj.name}, prefer_name_location: true do |corrector| - corrector.insert_after(end_location, '!') - end - else - issue_for node, MSG % {node.name, obj.name}, prefer_name_location: true - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/compact_after_map.cr b/lib/ameba/src/ameba/rule/performance/compact_after_map.cr deleted file mode 100644 index 7ec44a1..0000000 --- a/lib/ameba/src/ameba/rule/performance/compact_after_map.cr +++ /dev/null @@ -1,45 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `compact` calls that follow `map`. - # - # For example, this is considered inefficient: - # - # ``` - # %w[Alice Bob].map(&.match(/^A./)).compact - # ``` - # - # And can be written as this: - # - # ``` - # %w[Alice Bob].compact_map(&.match(/^A./)) - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/CompactAfterMap: - # Enabled: true - # ``` - class CompactAfterMap < Base - include AST::Util - - properties do - description "Identifies usage of `compact` calls that follow `map`" - end - - MSG = "Use `compact_map {...}` instead of `map {...}.compact`" - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "compact" && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block - return unless obj.name == "map" - - issue_for name_location(obj), name_end_location(node), MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/excessive_allocations.cr b/lib/ameba/src/ameba/rule/performance/excessive_allocations.cr deleted file mode 100644 index e8677d7..0000000 --- a/lib/ameba/src/ameba/rule/performance/excessive_allocations.cr +++ /dev/null @@ -1,70 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify excessive collection allocations, - # that can be avoided by using `each_` instead of `.each`. - # - # For example, this is considered inefficient: - # - # ``` - # "Alice".chars.each { |c| puts c } - # "Alice\nBob".lines.each { |l| puts l } - # ``` - # - # And can be written as this: - # - # ``` - # "Alice".each_char { |c| puts c } - # "Alice\nBob".each_line { |l| puts l } - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/ExcessiveAllocations: - # Enabled: true - # CallNames: - # codepoints: each_codepoint - # graphemes: each_grapheme - # chars: each_char - # lines: each_line - # ``` - class ExcessiveAllocations < Base - include AST::Util - - properties do - description "Identifies usage of excessive collection allocations" - call_names({ - "codepoints" => "each_codepoint", - "graphemes" => "each_grapheme", - "chars" => "each_char", - "lines" => "each_line", - # "keys" => "each_key", - # "values" => "each_value", - # "children" => "each_child", - }) - end - - MSG = "Use `%s {...}` instead of `%s.each {...}` to avoid excessive allocation" - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "each" && node.args.empty? - return unless (obj = node.obj).is_a?(Crystal::Call) - return unless obj.args.empty? && obj.block.nil? - return unless method = call_names[obj.name]? - - return unless name_location = name_location(obj) - return unless end_location = name_end_location(node) - - msg = MSG % {method, obj.name} - - issue_for name_location, end_location, msg do |corrector| - corrector.replace(name_location, end_location, method) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/first_last_after_filter.cr b/lib/ameba/src/ameba/rule/performance/first_last_after_filter.cr deleted file mode 100644 index 4290ba4..0000000 --- a/lib/ameba/src/ameba/rule/performance/first_last_after_filter.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `first/last/first?/last?` calls that follow filters. - # - # For example, this is considered inefficient: - # - # ``` - # [-1, 0, 1, 2].select { |e| e > 0 }.first? - # [-1, 0, 1, 2].select { |e| e > 0 }.last? - # ``` - # - # And can be written as this: - # - # ``` - # [-1, 0, 1, 2].find { |e| e > 0 } - # [-1, 0, 1, 2].reverse_each.find { |e| e > 0 } - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/FirstLastAfterFilter - # Enabled: true - # FilterNames: - # - select - # ``` - class FirstLastAfterFilter < Base - include AST::Util - - properties do - description "Identifies usage of `first/last/first?/last?` calls that follow filters" - filter_names %w[select] - end - - MSG = "Use `find {...}` instead of `%s {...}.%s`" - MSG_REVERSE = "Use `reverse_each.find {...}` instead of `%s {...}.%s`" - - CALL_NAMES = %w[first last first? last?] - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name.in?(CALL_NAMES) && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block - return unless node.block.nil? && node.args.empty? - return unless obj.name.in?(filter_names) - - message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE - - issue_for name_location(obj), name_end_location(node), - message % {obj.name, node.name} - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/flatten_after_map.cr b/lib/ameba/src/ameba/rule/performance/flatten_after_map.cr deleted file mode 100644 index cbdd4f1..0000000 --- a/lib/ameba/src/ameba/rule/performance/flatten_after_map.cr +++ /dev/null @@ -1,45 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `flatten` calls that follow `map`. - # - # For example, this is considered inefficient: - # - # ``` - # %w[Alice Bob].map(&.chars).flatten - # ``` - # - # And can be written as this: - # - # ``` - # %w[Alice Bob].flat_map(&.chars) - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/FlattenAfterMap: - # Enabled: true - # ``` - class FlattenAfterMap < Base - include AST::Util - - properties do - description "Identifies usage of `flatten` calls that follow `map`" - end - - MSG = "Use `flat_map {...}` instead of `map {...}.flatten`" - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "flatten" && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block - return unless obj.name == "map" - - issue_for name_location(obj), name_end_location(node), MSG - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/map_instead_of_block.cr b/lib/ameba/src/ameba/rule/performance/map_instead_of_block.cr deleted file mode 100644 index cb4f6fd..0000000 --- a/lib/ameba/src/ameba/rule/performance/map_instead_of_block.cr +++ /dev/null @@ -1,49 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `sum/product` calls - # that follow `map`. - # - # For example, this is considered inefficient: - # - # ``` - # (1..3).map(&.*(2)).sum - # ``` - # - # And can be written as this: - # - # ``` - # (1..3).sum(&.*(2)) - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/MapInsteadOfBlock: - # Enabled: true - # ``` - class MapInsteadOfBlock < Base - include AST::Util - - properties do - description "Identifies usage of `sum/product` calls that follow `map`" - end - - MSG = "Use `%s {...}` instead of `map {...}.%s`" - - CALL_NAMES = %w[sum product] - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name.in?(CALL_NAMES) && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block - return unless obj.name == "map" - - issue_for name_location(obj), name_end_location(node), - MSG % {node.name, node.name} - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/minmax_after_map.cr b/lib/ameba/src/ameba/rule/performance/minmax_after_map.cr deleted file mode 100644 index 803c84e..0000000 --- a/lib/ameba/src/ameba/rule/performance/minmax_after_map.cr +++ /dev/null @@ -1,69 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `min/max/minmax` calls that follow `map`. - # - # For example, this is considered invalid: - # - # ``` - # %w[Alice Bob].map(&.size).min - # %w[Alice Bob].map(&.size).max - # %w[Alice Bob].map(&.size).minmax - # ``` - # - # And it should be written as this: - # - # ``` - # %w[Alice Bob].min_of(&.size) - # %w[Alice Bob].max_of(&.size) - # %w[Alice Bob].minmax_of(&.size) - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/MinMaxAfterMap: - # Enabled: true - # ``` - class MinMaxAfterMap < Base - include AST::Util - - properties do - description "Identifies usage of `min/max/minmax` calls that follow `map`" - end - - MSG = "Use `%s {...}` instead of `map {...}.%s`." - CALL_NAMES = %w[min min? max max? minmax minmax?] - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name.in?(CALL_NAMES) && node.block.nil? && node.args.empty? - return unless (obj = node.obj) && obj.is_a?(Crystal::Call) - return unless obj.name == "map" && obj.block && obj.args.empty? - - return unless name_location = name_location(obj) - return unless end_location = name_end_location(node) - - of_name = node.name.sub(/(.+?)(\?)?$/, "\\1_of\\2") - message = MSG % {of_name, node.name} - - issue_for name_location, end_location, message do |corrector| - next unless node_name_location = name_location(node) - - # TODO: switching the order of the below calls breaks the corrector - corrector.replace( - name_location, - name_location.adjust(column_number: {{ "map".size - 1 }}), - of_name - ) - corrector.remove( - node_name_location.adjust(column_number: -1), - end_location - ) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/performance/size_after_filter.cr b/lib/ameba/src/ameba/rule/performance/size_after_filter.cr deleted file mode 100644 index 254576d..0000000 --- a/lib/ameba/src/ameba/rule/performance/size_after_filter.cr +++ /dev/null @@ -1,57 +0,0 @@ -require "./base" - -module Ameba::Rule::Performance - # This rule is used to identify usage of `size` calls that follow filter. - # - # For example, this is considered invalid: - # - # ``` - # [1, 2, 3].select { |e| e > 2 }.size - # [1, 2, 3].reject { |e| e < 2 }.size - # [1, 2, 3].select(&.< 2).size - # [0, 1, 2].select(&.zero?).size - # [0, 1, 2].reject(&.zero?).size - # ``` - # - # And it should be written as this: - # - # ``` - # [1, 2, 3].count { |e| e > 2 } - # [1, 2, 3].count { |e| e >= 2 } - # [1, 2, 3].count(&.< 2) - # [0, 1, 2].count(&.zero?) - # [0, 1, 2].count(&.!= 0) - # ``` - # - # YAML configuration example: - # - # ``` - # Performance/SizeAfterFilter: - # Enabled: true - # FilterNames: - # - select - # - reject - # ``` - class SizeAfterFilter < Base - include AST::Util - - properties do - description "Identifies usage of `size` calls that follow filter" - filter_names %w[select reject] - end - - MSG = "Use `count {...}` instead of `%s {...}.size`." - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name == "size" && (obj = node.obj) - return unless obj.is_a?(Crystal::Call) && obj.block - return unless obj.name.in?(filter_names) - - issue_for name_location(obj), name_end_location(node), MSG % obj.name - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/guard_clause.cr b/lib/ameba/src/ameba/rule/style/guard_clause.cr deleted file mode 100644 index 348fd85..0000000 --- a/lib/ameba/src/ameba/rule/style/guard_clause.cr +++ /dev/null @@ -1,186 +0,0 @@ -module Ameba::Rule::Style - # Use a guard clause instead of wrapping the code inside a conditional - # expression - # - # ``` - # # bad - # def test - # if something - # work - # end - # end - # - # # good - # def test - # return unless something - # - # work - # end - # - # # also good - # def test - # work if something - # end - # - # # bad - # if something - # raise "exception" - # else - # ok - # end - # - # # good - # raise "exception" if something - # ok - # - # # bad - # if something - # foo || raise("exception") - # else - # ok - # end - # - # # good - # foo || raise("exception") if something - # ok - # ``` - # - # YAML configuration example: - # - # ``` - # Style/GuardClause: - # Enabled: true - # ``` - class GuardClause < Base - include AST::Util - - properties do - enabled false - description "Check for conditionals that can be replaced with guard clauses" - end - - MSG = "Use a guard clause (`%s`) instead of wrapping the " \ - "code inside a conditional expression." - - def test(source) - AST::NodeVisitor.new self, source, skip: [ - Crystal::Assign, - ] - end - - def test(source, node : Crystal::Def) - final_expression = - if (body = node.body).is_a?(Crystal::Expressions) - body.last - else - body - end - - case final_expression - when Crystal::If, Crystal::Unless - check_ending_if(source, final_expression) - end - end - - def test(source, node : Crystal::If | Crystal::Unless) - return if accepted_form?(source, node, ending: false) - - case - when guard_clause = guard_clause(node.then) - parent, conditional_keyword = node.then, keyword(node) - when guard_clause = guard_clause(node.else) - parent, conditional_keyword = node.else, opposite_keyword(node) - end - - return unless guard_clause && parent && conditional_keyword - - guard_clause_source = guard_clause_source(source, guard_clause, parent) - report_issue(source, node, guard_clause_source, conditional_keyword) - end - - private def check_ending_if(source, node) - return if accepted_form?(source, node, ending: true) - - report_issue(source, node, "return", opposite_keyword(node)) - end - - private def report_issue(source, node, scope_exiting_keyword, conditional_keyword) - return unless keyword_loc = node.location - return unless cond_code = node_source(node.cond, source.lines) - - keyword_end_loc = keyword_loc.adjust(column_number: keyword(node).size - 1) - - example = "#{scope_exiting_keyword} #{conditional_keyword} #{cond_code}" - # TODO: check if example is too long for single line - - if node.else.is_a?(Crystal::Nop) - return unless end_end_loc = node.end_location - - end_loc = end_end_loc.adjust(column_number: {{ 1 - "end".size }}) - - issue_for keyword_loc, keyword_end_loc, MSG % example do |corrector| - replacement = "#{scope_exiting_keyword} #{conditional_keyword}" - - corrector.replace(keyword_loc, keyword_end_loc, replacement) - corrector.remove(end_loc, end_end_loc) - end - else - issue_for keyword_loc, keyword_end_loc, MSG % example - end - end - - private def keyword(node : Crystal::If) - "if" - end - - private def keyword(node : Crystal::Unless) - "unless" - end - - private def opposite_keyword(node : Crystal::If) - "unless" - end - - private def opposite_keyword(node : Crystal::Unless) - "if" - end - - private def accepted_form?(source, node, ending) - return true if node.is_a?(Crystal::If) && node.ternary? - return true unless cond_loc = node.cond.location - return true unless cond_end_loc = node.cond.end_location - return true unless cond_loc.line_number == cond_end_loc.line_number - return true unless (then_loc = node.then.location).nil? || cond_loc < then_loc - - if ending - !node.else.is_a?(Crystal::Nop) - else - return true if node.else.is_a?(Crystal::Nop) - return true unless code = node_source(node, source.lines) - - code.starts_with?("elsif") - end - end - - private def guard_clause(node) - node = node.right if node.is_a?(Crystal::BinaryOp) - - return unless location = node.location - return unless end_location = node.end_location - return unless location.line_number == end_location.line_number - - case node - when Crystal::Call - node if node.obj.nil? && node.name == "raise" - when Crystal::Return, Crystal::Break, Crystal::Next - node - end - end - - def guard_clause_source(source, guard_clause, parent) - node = parent.is_a?(Crystal::BinaryOp) ? parent : guard_clause - - node_source(node, source.lines) - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/is_a_filter.cr b/lib/ameba/src/ameba/rule/style/is_a_filter.cr deleted file mode 100644 index 1a252fc..0000000 --- a/lib/ameba/src/ameba/rule/style/is_a_filter.cr +++ /dev/null @@ -1,90 +0,0 @@ -module Ameba::Rule::Style - # This rule is used to identify usage of `is_a?/nil?` calls within filters. - # - # For example, this is considered invalid: - # - # ``` - # matches = %w[Alice Bob].map(&.match(/^A./)) - # - # matches.any?(&.is_a?(Regex::MatchData)) # => true - # matches.one?(&.nil?) # => true - # - # typeof(matches.reject(&.nil?)) # => Array(Regex::MatchData | Nil) - # typeof(matches.select(&.is_a?(Regex::MatchData))) # => Array(Regex::MatchData | Nil) - # ``` - # - # And it should be written as this: - # - # ``` - # matches = %w[Alice Bob].map(&.match(/^A./)) - # - # matches.any?(Regex::MatchData) # => true - # matches.one?(Nil) # => true - # - # typeof(matches.reject(Nil)) # => Array(Regex::MatchData) - # typeof(matches.select(Regex::MatchData)) # => Array(Regex::MatchData) - # ``` - # - # YAML configuration example: - # - # ``` - # Style/IsAFilter: - # Enabled: true - # FilterNames: - # - select - # - reject - # - any? - # - all? - # - none? - # - one? - # ``` - class IsAFilter < Base - include AST::Util - - properties do - description "Identifies usage of `is_a?/nil?` calls within filters" - filter_names %w[select reject any? all? none? one?] - end - - MSG = "Use `%s` instead of `%s`" - - OLD = "%s {...}" - NEW = "%s(%s)" - - def test(source) - AST::NodeVisitor.new self, source, skip: :macro - end - - def test(source, node : Crystal::Call) - return unless node.name.in?(filter_names) - return unless filter_location = name_location(node) - return unless block = node.block - return unless (body = block.body).is_a?(Crystal::IsA) - return unless (path = body.const).is_a?(Crystal::Path) - return unless body.obj.is_a?(Crystal::Var) - return if block.args.size > 1 - - name = path.names.join("::") - name = "::#{name}" if path.global? && !body.nil_check? - - end_location = node.end_location - if !end_location || end_location.try(&.column_number.zero?) - if end_location = path.end_location - end_location = end_location.adjust(column_number: 1) - end - end - - old = OLD % node.name - new = NEW % {node.name, name} - msg = MSG % {new, old} - - if end_location - issue_for(filter_location, end_location, msg) do |corrector| - corrector.replace(filter_location, end_location, new) - end - else - issue_for(filter_location, nil, msg) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/is_a_nil.cr b/lib/ameba/src/ameba/rule/style/is_a_nil.cr deleted file mode 100644 index 0668126..0000000 --- a/lib/ameba/src/ameba/rule/style/is_a_nil.cr +++ /dev/null @@ -1,42 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows calls to `is_a?(Nil)` in favor of `nil?`. - # - # This is considered bad: - # - # ``` - # var.is_a?(Nil) - # ``` - # - # And needs to be written as: - # - # ``` - # var.nil? - # ``` - # - # YAML configuration example: - # - # ``` - # Style/IsANil: - # Enabled: true - # ``` - class IsANil < Base - include AST::Util - - properties do - description "Disallows calls to `is_a?(Nil)` in favor of `nil?`" - end - - MSG = "Use `nil?` instead of `is_a?(Nil)`" - - def test(source, node : Crystal::IsA) - return if node.nil_check? - - const = node.const - return unless path_named?(const, "Nil") - - issue_for const, MSG do |corrector| - corrector.replace(node, "#{node.obj}.nil?") - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/large_numbers.cr b/lib/ameba/src/ameba/rule/style/large_numbers.cr deleted file mode 100644 index b439ba6..0000000 --- a/lib/ameba/src/ameba/rule/style/large_numbers.cr +++ /dev/null @@ -1,113 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows usage of large numbers without underscore. - # These do not affect the value of the number, but can help read - # large numbers more easily. - # - # For example, these are considered invalid: - # - # ``` - # 100000 - # 141592654 - # 5.123456 - # ``` - # - # And has to be rewritten as the following: - # - # ``` - # 100_000 - # 141_592_654 - # 5.123_456 - # ``` - # - # YAML configuration example: - # - # ``` - # Style/LargeNumbers: - # Enabled: true - # IntMinDigits: 6 # i.e. integers higher than 99999 - # ``` - class LargeNumbers < Base - properties do - enabled false - description "Disallows usage of large numbers without underscore" - int_min_digits 6 - end - - MSG = "Large numbers should be written with underscores: %s" - - def test(source) - Tokenizer.new(source).run do |token| - next unless token.type.number? && decimal?(token.raw) - - parsed = parse_number(token.raw) - - if allowed?(*parsed) && (expected = underscored *parsed) != token.raw - location = token.location - end_location = location.adjust(column_number: token.raw.size - 1) - - issue_for location, end_location, MSG % expected do |corrector| - corrector.replace(location, end_location, expected) - end - end - end - end - - private def decimal?(value) - value !~ /^0(x|b|o)/ - end - - private def allowed?(_sign, value, fraction, _suffix) - return true if fraction && fraction.size > 3 - - digits = value.chars.select!(&.number?) - digits.size >= int_min_digits - end - - private def underscored(sign, value, fraction, suffix) - value = slice_digits(value.reverse).reverse - fraction = ".#{slice_digits(fraction)}" if fraction - - "#{sign}#{value}#{fraction}#{suffix}" - end - - private def slice_digits(value, by = 3) - %w[].tap do |slices| - value.chars.reject!(&.== '_').each_slice(by) do |slice| - slices << slice.join - end - end.join('_') - end - - private def parse_number(value) - value, sign = parse_sign(value) - value, suffix = parse_suffix(value) - value, fraction = parse_fraction(value) - - {sign, value, fraction, suffix} - end - - private def parse_sign(value) - if value[0].in?('+', '-') - sign = value[0] - value = value[1..-1] - end - {value, sign} - end - - private def parse_suffix(value) - if pos = (value =~ /(e|_?(i|u|f))/) - suffix = value[pos..-1] - value = value[0..pos - 1] - end - {value, suffix} - end - - private def parse_fraction(value) - if comma = value.index('.') - fraction = value[comma + 1..-1] - value = value[0..comma - 1] - end - {value, fraction} - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/negated_conditions_in_unless.cr b/lib/ameba/src/ameba/rule/style/negated_conditions_in_unless.cr deleted file mode 100644 index 129ba27..0000000 --- a/lib/ameba/src/ameba/rule/style/negated_conditions_in_unless.cr +++ /dev/null @@ -1,53 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows negated conditions in unless. - # - # For example, this is considered invalid: - # - # ``` - # unless !s.empty? - # :ok - # end - # ``` - # - # And should be rewritten to the following: - # - # ``` - # if s.empty? - # :ok - # end - # ``` - # - # It is pretty difficult to wrap your head around a block of code - # that is executed if a negated condition is NOT met. - # - # YAML configuration example: - # - # ``` - # Style/NegatedConditionsInUnless: - # Enabled: true - # ``` - class NegatedConditionsInUnless < Base - properties do - description "Disallows negated conditions in unless" - end - - MSG = "Avoid negated conditions in unless blocks" - - def test(source, node : Crystal::Unless) - issue_for node, MSG if negated_condition?(node.cond) - end - - private def negated_condition?(node) - case node - when Crystal::BinaryOp - negated_condition?(node.left) || negated_condition?(node.right) - when Crystal::Expressions - node.expressions.any? { |exp| negated_condition?(exp) } - when Crystal::Not - true - else - false - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/parentheses_around_condition.cr b/lib/ameba/src/ameba/rule/style/parentheses_around_condition.cr deleted file mode 100644 index 112d3a6..0000000 --- a/lib/ameba/src/ameba/rule/style/parentheses_around_condition.cr +++ /dev/null @@ -1,81 +0,0 @@ -module Ameba::Rule::Style - # A rule that checks for the presence of superfluous parentheses - # around the condition of `if`, `unless`, `case`, `while` and `until`. - # - # For example, this is considered invalid: - # - # ``` - # if (foo == 42) - # do_something - # end - # ``` - # - # And should be replaced by the following: - # - # ``` - # if foo == 42 - # do_something - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Style/ParenthesesAroundCondition: - # Enabled: true - # ExcludeTernary: false - # AllowSafeAssignment: false - # ``` - class ParenthesesAroundCondition < Base - properties do - description "Disallows redundant parentheses around control expressions" - - exclude_ternary false - allow_safe_assignment false - end - - MSG_REDUNDANT = "Redundant parentheses" - MSG_MISSING = "Missing parentheses" - - protected def strip_parentheses?(node, in_ternary) : Bool - case node - when Crystal::BinaryOp, Crystal::ExceptionHandler - !in_ternary - when Crystal::Call - !in_ternary || node.has_parentheses? || node.args.empty? - when Crystal::Yield - !in_ternary || node.has_parentheses? || node.exps.empty? - when Crystal::Assign, Crystal::OpAssign, Crystal::MultiAssign - !in_ternary && !allow_safe_assignment? - else - true - end - end - - def test(source, node : Crystal::If | Crystal::Unless | Crystal::Case | Crystal::While | Crystal::Until) - cond = node.cond - - if cond.is_a?(Crystal::Assign) && allow_safe_assignment? - issue_for cond, MSG_MISSING do |corrector| - corrector.wrap(cond, '(', ')') - end - return - end - - is_ternary = node.is_a?(Crystal::If) && node.ternary? - - return if is_ternary && exclude_ternary? - - return unless cond.is_a?(Crystal::Expressions) - return unless cond.keyword.paren? - - return unless exp = cond.single_expression? - return unless strip_parentheses?(exp, is_ternary) - - issue_for cond, MSG_REDUNDANT do |corrector| - corrector.remove_trailing(cond, 1) - corrector.remove_leading(cond, 1) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/redundant_begin.cr b/lib/ameba/src/ameba/rule/style/redundant_begin.cr deleted file mode 100644 index be21d73..0000000 --- a/lib/ameba/src/ameba/rule/style/redundant_begin.cr +++ /dev/null @@ -1,154 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows redundant begin blocks. - # - # Currently it is able to detect: - # - # 1. Exception handler block that can be used as a part of the method. - # - # For example, this: - # - # ``` - # def method - # begin - # read_content - # rescue - # close_file - # end - # end - # ``` - # - # should be rewritten as: - # - # ``` - # def method - # read_content - # rescue - # close_file - # end - # ``` - # - # 2. begin..end block as a top level block in a method. - # - # For example this is considered invalid: - # - # ``` - # def method - # begin - # a = 1 - # b = 2 - # end - # end - # ``` - # - # and has to be written as the following: - # - # ``` - # def method - # a = 1 - # b = 2 - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Style/RedundantBegin: - # Enabled: true - # ``` - class RedundantBegin < Base - include AST::Util - - properties do - description "Disallows redundant begin blocks" - end - - MSG = "Redundant `begin` block detected" - - def test(source, node : Crystal::Def) - return unless def_loc = node.location - - case body = node.body - when Crystal::ExceptionHandler - return if begin_exprs_in_handler?(body) || inner_handler?(body) - when Crystal::Expressions - return unless redundant_begin_in_expressions?(body) - else - return - end - - return unless begin_range = def_redundant_begin_range(source, node) - - begin_loc, end_loc = begin_range - begin_loc, end_loc = def_loc.seek(begin_loc), def_loc.seek(end_loc) - begin_end_loc = begin_loc.adjust(column_number: {{ "begin".size - 1 }}) - end_end_loc = end_loc.adjust(column_number: {{ "end".size - 1 }}) - - issue_for begin_loc, begin_end_loc, MSG do |corrector| - corrector.remove(begin_loc, begin_end_loc) - corrector.remove(end_loc, end_end_loc) - end - end - - private def redundant_begin_in_expressions?(node) - !!node.keyword.try(&.begin?) - end - - private def inner_handler?(handler) - handler.body.is_a?(Crystal::ExceptionHandler) - end - - private def begin_exprs_in_handler?(handler) - return unless (body = handler.body).is_a?(Crystal::Expressions) - body.expressions.first?.is_a?(Crystal::ExceptionHandler) - end - - private def def_redundant_begin_range(source, node) - return unless code = node_source(node, source.lines) - - lexer = Crystal::Lexer.new code - return unless begin_loc = def_redundant_begin_loc(lexer) - return unless end_loc = def_redundant_end_loc(lexer) - - {begin_loc, end_loc} - end - - private def def_redundant_begin_loc(lexer) - in_body = in_argument_list = false - - loop do - token = lexer.next_token - - case token.type - when .eof?, .op_minus_gt? - break - when .ident? - next unless in_body - return unless token.value == Crystal::Keyword::BEGIN - return token.location - when .op_lparen? - in_argument_list = true - when .op_rparen? - in_argument_list = false - when .newline? - in_body = true unless in_argument_list - when .space? - # ignore - else - return if in_body - end - end - end - - private def def_redundant_end_loc(lexer) - end_loc = def_end_loc = nil - - Tokenizer.new(lexer).run do |token| - next unless token.value == Crystal::Keyword::END - - end_loc, def_end_loc = def_end_loc, token.location - end - - end_loc - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/redundant_next.cr b/lib/ameba/src/ameba/rule/style/redundant_next.cr deleted file mode 100644 index ce81d08..0000000 --- a/lib/ameba/src/ameba/rule/style/redundant_next.cr +++ /dev/null @@ -1,128 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows redundant next expressions. A `next` keyword allows - # a block to skip to the next iteration early, however, it is considered - # redundant in cases where it is the last expression in a block or combines - # into the node which is the last in a block. - # - # For example, this is considered invalid: - # - # ``` - # block do |v| - # next v + 1 - # end - # ``` - # - # ``` - # block do |v| - # case v - # when .nil? - # next "nil" - # when .blank? - # next "blank" - # else - # next "empty" - # end - # end - # ``` - # - # And has to be written as the following: - # - # ``` - # block do |v| - # v + 1 - # end - # ``` - # - # ``` - # block do |v| - # case arg - # when .nil? - # "nil" - # when .blank? - # "blank" - # else - # "empty" - # end - # end - # ``` - # - # ### Configuration params - # - # 1. *allow_multi_next*, default: true - # - # Allows end-user to configure whether to report or not the next statements - # which yield tuple literals i.e. - # - # ``` - # block do - # next a, b - # end - # ``` - # - # If this param equals to `false`, the block above will be forced to be written as: - # - # ``` - # block do - # {a, b} - # end - # ``` - # - # 2. *allow_empty_next*, default: true - # - # Allows end-user to configure whether to report or not the next statements - # without arguments. Sometimes such statements are used to yild the `nil` value explicitly. - # - # ``` - # block do - # @foo = :empty - # next - # end - # ``` - # - # If this param equals to `false`, the block above will be forced to be written as: - # - # ``` - # block do - # @foo = :empty - # nil - # end - # ``` - # - # ### YAML config example - # - # ``` - # Style/RedundantNext: - # Enabled: true - # AllowMultiNext: true - # AllowEmptyNext: true - # ``` - class RedundantNext < Base - include AST::Util - - properties do - description "Reports redundant next expressions" - - allow_multi_next true - allow_empty_next true - end - - MSG = "Redundant `next` detected" - - def test(source, node : Crystal::Block) - AST::RedundantControlExpressionVisitor.new(self, source, node.body) - end - - def test(source, node : Crystal::Next, visitor : AST::RedundantControlExpressionVisitor) - return if allow_multi_next? && node.exp.is_a?(Crystal::TupleLiteral) - return if allow_empty_next? && (node.exp.nil? || node.exp.try(&.nop?)) - - if exp_code = control_exp_code(node, source.lines) - issue_for node, MSG do |corrector| - corrector.replace(node, exp_code) - end - else - issue_for node, MSG - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/redundant_return.cr b/lib/ameba/src/ameba/rule/style/redundant_return.cr deleted file mode 100644 index 234fd09..0000000 --- a/lib/ameba/src/ameba/rule/style/redundant_return.cr +++ /dev/null @@ -1,125 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows redundant return expressions. - # - # For example, this is considered invalid: - # - # ``` - # def foo - # return :bar - # end - # ``` - # - # ``` - # def bar(arg) - # case arg - # when .nil? - # return "nil" - # when .blank? - # return "blank" - # else - # return "empty" - # end - # end - # ``` - # - # And has to be written as the following: - # - # ``` - # def foo - # :bar - # end - # ``` - # - # ``` - # def bar(arg) - # case arg - # when .nil? - # "nil" - # when .blank? - # "blank" - # else - # "empty" - # end - # end - # ``` - # - # ### Configuration params - # - # 1. *allow_multi_return*, default: true - # - # Allows end-user to configure whether to report or not the return statements - # which return tuple literals i.e. - # - # ``` - # def method(a, b) - # return a, b - # end - # ``` - # - # If this param equals to `false`, the method above has to be written as: - # - # ``` - # def method(a, b) - # {a, b} - # end - # ``` - # - # 2. *allow_empty_return*, default: true - # - # Allows end-user to configure whether to report or not the return statements - # without arguments. Sometimes such returns are used to return the `nil` value explicitly. - # - # ``` - # def method - # @foo = :empty - # return - # end - # ``` - # - # If this param equals to `false`, the method above has to be written as: - # - # ``` - # def method - # @foo = :empty - # nil - # end - # ``` - # - # ### YAML config example - # - # ``` - # Style/RedundantReturn: - # Enabled: true - # AllowMultiReturn: true - # AllowEmptyReturn: true - # ``` - class RedundantReturn < Base - include AST::Util - - properties do - description "Reports redundant return expressions" - - allow_multi_return true - allow_empty_return true - end - - MSG = "Redundant `return` detected" - - def test(source, node : Crystal::Def) - AST::RedundantControlExpressionVisitor.new(self, source, node.body) - end - - def test(source, node : Crystal::Return, visitor : AST::RedundantControlExpressionVisitor) - return if allow_multi_return? && node.exp.is_a?(Crystal::TupleLiteral) - return if allow_empty_return? && (node.exp.nil? || node.exp.try(&.nop?)) - - if exp_code = control_exp_code(node, source.lines) - issue_for node, MSG do |corrector| - corrector.replace(node, exp_code) - end - else - issue_for node, MSG - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/unless_else.cr b/lib/ameba/src/ameba/rule/style/unless_else.cr deleted file mode 100644 index ba9c380..0000000 --- a/lib/ameba/src/ameba/rule/style/unless_else.cr +++ /dev/null @@ -1,85 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows the use of an `else` block with the `unless`. - # - # For example, the rule considers these valid: - # - # ``` - # unless something - # :ok - # end - # - # if something - # :one - # else - # :two - # end - # ``` - # - # But it considers this one invalid as it is an `unless` with an `else`: - # - # ``` - # unless something - # :one - # else - # :two - # end - # ``` - # - # The solution is to swap the order of the blocks, and change the `unless` to - # an `if`, so the previous invalid example would become this: - # - # ``` - # if something - # :two - # else - # :one - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Style/UnlessElse: - # Enabled: true - # ``` - class UnlessElse < Base - properties do - description "Disallows the use of an `else` block with the `unless`" - end - - MSG = "Favour if over unless with else" - - def test(source, node : Crystal::Unless) - return if node.else.nop? - - location = node.location - cond_end_location = node.cond.end_location - else_location = node.else_location - end_location = node.end_location - - unless location && cond_end_location && else_location && end_location - issue_for node, MSG - return - end - - issue_for location, cond_end_location, MSG do |corrector| - keyword_begin_pos = source.pos(location) - keyword_end_pos = keyword_begin_pos + {{ "unless".size }} - keyword_range = keyword_begin_pos...keyword_end_pos - - cond_end_pos = source.pos(cond_end_location, end: true) - else_begin_pos = source.pos(else_location) - body_range = cond_end_pos...else_begin_pos - - else_end_pos = else_begin_pos + {{ "else".size }} - end_end_pos = source.pos(end_location, end: true) - end_begin_pos = end_end_pos - {{ "end".size }} - else_range = else_end_pos...end_begin_pos - - corrector.replace(keyword_range, "if") - corrector.replace(body_range, source.code[else_range]) - corrector.replace(else_range, source.code[body_range]) - end - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/verbose_block.cr b/lib/ameba/src/ameba/rule/style/verbose_block.cr deleted file mode 100644 index fa4ae96..0000000 --- a/lib/ameba/src/ameba/rule/style/verbose_block.cr +++ /dev/null @@ -1,247 +0,0 @@ -module Ameba::Rule::Style - # This rule is used to identify usage of single expression blocks with - # argument as a receiver, that can be collapsed into a short form. - # - # For example, this is considered invalid: - # - # ``` - # (1..3).any? { |i| i.odd? } - # ``` - # - # And it should be written as this: - # - # ``` - # (1..3).any?(&.odd?) - # ``` - # - # YAML configuration example: - # - # ``` - # Style/VerboseBlock: - # Enabled: true - # ExcludeMultipleLineBlocks: true - # ExcludeCallsWithBlock: true - # ExcludePrefixOperators: true - # ExcludeOperators: true - # ExcludeSetters: false - # MaxLineLength: ~ - # MaxLength: 50 # use ~ to disable - # ``` - class VerboseBlock < Base - include AST::Util - - properties do - description "Identifies usage of collapsible single expression blocks" - - exclude_multiple_line_blocks true - exclude_calls_with_block true - exclude_prefix_operators true - exclude_operators true - exclude_setters false - - max_line_length nil, as: Int32? - max_length 50, as: Int32? - end - - MSG = "Use short block notation instead: `%s`" - CALL_PATTERN = "%s(%s&.%s)" - - protected def same_location_lines?(a, b) - return unless a_location = name_location(a) - return unless b_location = b.location - - a_location.line_number == b_location.line_number - end - - private PREFIX_OPERATORS = {"+", "-", "~"} - private OPERATOR_CHARS = - {'[', ']', '!', '=', '>', '<', '~', '+', '-', '*', '/', '%', '^', '|', '&'} - - protected def prefix_operator?(node) - node.name.in?(PREFIX_OPERATORS) && node.args.empty? - end - - protected def operator?(name) - !name.empty? && name[0].in?(OPERATOR_CHARS) - end - - protected def setter?(name) - !name.empty? && name[0].letter? && name.ends_with?('=') - end - - protected def valid_length?(code) - if max_length = self.max_length - return code.size <= max_length - end - true - end - - protected def valid_line_length?(node, code) - if max_line_length = self.max_line_length - if location = name_location(node) - final_line_length = location.column_number + code.size - return final_line_length <= max_line_length - end - end - true - end - - protected def reference_count(node, obj : Crystal::Var) - i = 0 - case node - when Crystal::Call - i += reference_count(node.obj, obj) - i += reference_count(node.block, obj) - - node.args.each do |arg| - i += reference_count(arg, obj) - end - node.named_args.try &.each do |arg| - i += reference_count(arg.value, obj) - end - when Crystal::BinaryOp - i += reference_count(node.left, obj) - i += reference_count(node.right, obj) - when Crystal::Block - i += reference_count(node.body, obj) - when Crystal::Var - i += 1 if node == obj - end - i - end - - protected def args_to_s(io : IO, node : Crystal::Call, short_block = nil, skip_last_arg = false) : Nil - args = node.args.dup - args.pop? if skip_last_arg - args.join io, ", " - - named_args = node.named_args - if named_args - io << ", " unless args.empty? || named_args.empty? - named_args.join io, ", " do |arg, inner_io| - inner_io << arg.name << ": " << arg.value - end - end - - if short_block - io << ", " unless args.empty? && (named_args.nil? || named_args.empty?) - io << short_block - end - end - - protected def node_to_s(source, node : Crystal::Call) - String.build do |str| - case name = node.name - when "[]" - str << '[' - args_to_s(str, node) - str << ']' - when "[]?" - str << '[' - args_to_s(str, node) - str << "]?" - when "[]=" - str << '[' - args_to_s(str, node, skip_last_arg: true) - str << "]=(" << node.args.last? << ')' - else - short_block = short_block_code(source, node) - str << name - if !node.args.empty? || (node.named_args && !node.named_args.try(&.empty?)) || short_block - str << '(' - args_to_s(str, node, short_block) - str << ')' - end - str << " {...}" if node.block && short_block.nil? - end - end - end - - protected def short_block_code(source, node : Crystal::Call) - return unless block = node.block - return unless block_location = block.location - return unless block_end_location = block.body.end_location - - block_code = source_between(block_location, block_end_location, source.lines) - block_code if block_code.try(&.starts_with?("&.")) - end - - protected def call_code(source, call, body) - args = String.build { |io| args_to_s(io, call) }.presence - args += ", " if args - - call_chain = %w[].tap do |arr| - obj = body.obj - while obj.is_a?(Crystal::Call) - arr << node_to_s(source, obj) - obj = obj.obj - end - arr.reverse! - arr << node_to_s(source, body) - end - - name = - call_chain.join('.') - - CALL_PATTERN % {call.name, args, name} - end - - # ameba:disable Metrics/CyclomaticComplexity - protected def issue_for_valid(source, call : Crystal::Call, block : Crystal::Block, body : Crystal::Call) - return if exclude_calls_with_block? && body.block - return if exclude_multiple_line_blocks? && !same_location_lines?(call, body) - return if exclude_prefix_operators? && prefix_operator?(body) - return if exclude_operators? && operator?(body.name) - return if exclude_setters? && setter?(body.name) - - call_code = - call_code(source, call, body) - - return unless valid_line_length?(call, call_code) - return unless valid_length?(call_code) - - return unless location = name_location(call) - return unless end_location = block.end_location - - if call_code.includes?("{...}") - issue_for location, end_location, MSG % call_code - else - issue_for location, end_location, MSG % call_code do |corrector| - corrector.replace(location, end_location, call_code) - end - end - end - - def test(source, node : Crystal::Call) - # we are interested only in calls with block taking a single argument - # - # ``` - # (1..3).any? { |i| i.to_i64.odd? } - # ^--- ^ ^------------ - # block arg body - # ``` - return unless (block = node.block) && block.args.size == 1 - - arg = block.args.first - - # we filter out the blocks that are of call type - `i.to_i64.odd?` - return unless (body = block.body).is_a?(Crystal::Call) - - # we need to "unwind" the call chain, so the final receiver object - # ends up being a variable - `i` - obj = body.obj - while obj.is_a?(Crystal::Call) - obj = obj.obj - end - - # only calls with a first argument used as a receiver are the valid game - return unless obj == arg - - # we bail out if the block node include the block argument - return if reference_count(body, arg) > 1 - - # add issue if the given nodes pass all of the checks - issue_for_valid source, node, block, body - end - end -end diff --git a/lib/ameba/src/ameba/rule/style/while_true.cr b/lib/ameba/src/ameba/rule/style/while_true.cr deleted file mode 100644 index 7003640..0000000 --- a/lib/ameba/src/ameba/rule/style/while_true.cr +++ /dev/null @@ -1,46 +0,0 @@ -module Ameba::Rule::Style - # A rule that disallows the use of `while true` instead of using the idiomatic `loop` - # - # For example, this is considered invalid: - # - # ``` - # while true - # do_something - # break if some_condition - # end - # ``` - # - # And should be replaced by the following: - # - # ``` - # loop do - # do_something - # break if some_condition - # end - # ``` - # - # YAML configuration example: - # - # ``` - # Style/WhileTrue: - # Enabled: true - # ``` - class WhileTrue < Base - properties do - description "Disallows while statements with a true literal as condition" - end - - MSG = "While statement using true literal as condition" - - def test(source, node : Crystal::While) - return unless node.cond.true_literal? - - return unless location = node.location - return unless end_location = node.cond.end_location - - issue_for node, MSG do |corrector| - corrector.replace(location, end_location, "loop do") - end - end - end -end diff --git a/lib/ameba/src/ameba/runner.cr b/lib/ameba/src/ameba/runner.cr deleted file mode 100644 index 5a526fa..0000000 --- a/lib/ameba/src/ameba/runner.cr +++ /dev/null @@ -1,227 +0,0 @@ -module Ameba - # Represents a runner for inspecting sources files. - # Holds a list of rules to do inspection based on, - # list of sources to run inspection on and a formatter - # to prepare a report. - # - # ``` - # config = Ameba::Config.load - # runner = Ameba::Runner.new config - # runner.run.success? # => true or false - # ``` - class Runner - # An error indicating that the inspection loop got stuck correcting - # issues back and forth. - class InfiniteCorrectionLoopError < RuntimeError - def initialize(path, issues_by_iteration, loop_start = -1) - root_cause = - issues_by_iteration[loop_start..-1] - .join(" -> ", &.map(&.rule.name).uniq!.join(", ")) - - message = String.build do |io| - io << "Infinite loop" - io << " in " << path unless path.empty? - io << " caused by " << root_cause - end - - super message - end - end - - # A list of rules to do inspection based on. - @rules : Array(Rule::Base) - - # A list of sources to run inspection on. - getter sources : Array(Source) - - # A level of severity to be reported. - @severity : Severity - - # A formatter to prepare report. - @formatter : Formatter::BaseFormatter - - # A syntax rule which always inspects a source first - @syntax_rule = Rule::Lint::Syntax.new - - # Checks for unneeded disable directives. Always inspects a source last - @unneeded_disable_directive_rule : Rule::Base? - - # Returns `true` if correctable issues should be autocorrected. - private getter? autocorrect : Bool - - # Instantiates a runner using a `config`. - # - # ``` - # config = Ameba::Config.load - # config.files = files - # config.formatter = formatter - # - # Ameba::Runner.new config - # ``` - def initialize(config : Config) - @sources = config.sources - @formatter = config.formatter - @severity = config.severity - @rules = config.rules.select(&.enabled?).reject!(&.special?) - @autocorrect = config.autocorrect? - - @unneeded_disable_directive_rule = - config.rules - .find &.class.==(Rule::Lint::UnneededDisableDirective) - end - - protected def initialize(@rules, @sources, @formatter, @severity, @autocorrect = false) - end - - # Performs the inspection. Iterates through all sources and test it using - # list of rules. If a specific rule fails on a specific source, it adds - # an issue to that source. - # - # This action also notifies formatter when inspection is started/finished, - # and when a specific source started/finished to be inspected. - # - # ``` - # runner = Ameba::Runner.new config - # runner.run # => returns runner again - # ``` - def run - @formatter.started @sources - - channels = @sources.map { Channel(Exception?).new } - @sources.zip(channels).each do |source, channel| - spawn do - run_source(source) - rescue e - channel.send(e) - else - channel.send(nil) - end - end - - channels.each do |chan| - chan.receive.try { |e| raise e } - end - - self - ensure - @formatter.finished @sources - end - - private def run_source(source) - @formatter.source_started source - - # This variable is a 2D array used to track corrected issues after each - # inspection iteration. This is used to output meaningful infinite loop - # error message. - corrected_issues = [] of Array(Issue) - - # When running with --fix, we need to inspect the source until no more - # corrections are made (because automatic corrections can introduce new - # issues). In the normal case the loop is only executed once. - loop_unless_infinite(source, corrected_issues) do - # We have to reprocess the source to pick up any changes. Since a - # change could (theoretically) introduce syntax errors, we break the - # loop if we find any. - @syntax_rule.test(source) - break unless source.valid? - - @rules.each do |rule| - next if rule.excluded?(source) - rule.test(source) - end - check_unneeded_directives(source) - break unless autocorrect? && source.correct? - - # The issues that couldn't be corrected will be found again so we - # only keep the corrected ones in order to avoid duplicate reporting. - corrected_issues << source.issues.select(&.correctable?) - source.issues.clear - end - - corrected_issues.flatten.reverse_each do |issue| - source.issues.unshift(issue) - end - - File.write(source.path, source.code) unless corrected_issues.empty? - ensure - @formatter.source_finished source - end - - # Explains an issue at a specified *location*. - # - # Runner should perform inspection before doing the explain. - # This is necessary to be able to find the issue at a specified location. - # - # ``` - # runner = Ameba::Runner.new config - # runner.run - # runner.explain({file: file, line: l, column: c}) - # ``` - def explain(location, output = STDOUT) - Formatter::ExplainFormatter.new(output, location).finished @sources - end - - # Indicates whether the last inspection successful or not. - # It returns `true` if no issues matching severity in sources found, `false` otherwise. - # - # ``` - # runner = Ameba::Runner.new config - # runner.run - # runner.success? # => true or false - # ``` - def success? - @sources.all? do |source| - source.issues - .reject(&.disabled?) - .none?(&.rule.severity.<=(@severity)) - end - end - - private MAX_ITERATIONS = 200 - - private def loop_unless_infinite(source, corrected_issues, &) - # Keep track of the state of the source. If a rule modifies the source - # and another rule undoes it producing identical source we have an - # infinite loop. - processed_sources = [] of UInt64 - - # It is possible for a rule to keep adding indefinitely to a file, - # making it bigger and bigger. If the inspection loop runs for an - # excessively high number of iterations, this is likely happening. - iterations = 0 - - loop do - check_for_infinite_loop(source, corrected_issues, processed_sources) - - if (iterations += 1) > MAX_ITERATIONS - raise InfiniteCorrectionLoopError.new(source.path, corrected_issues) - end - - yield - end - end - - # Check whether a run created source identical to a previous run, which - # means that we definitely have an infinite loop. - private def check_for_infinite_loop(source, corrected_issues, processed_sources) - checksum = source.code.hash - - if loop_start = processed_sources.index(checksum) - raise InfiniteCorrectionLoopError.new( - source.path, - corrected_issues, - loop_start: loop_start - ) - end - - processed_sources << checksum - end - - private def check_unneeded_directives(source) - return unless rule = @unneeded_disable_directive_rule - return unless rule.enabled? - - rule.test(source) - end - end -end diff --git a/lib/ameba/src/ameba/severity.cr b/lib/ameba/src/ameba/severity.cr deleted file mode 100644 index 52dd745..0000000 --- a/lib/ameba/src/ameba/severity.cr +++ /dev/null @@ -1,67 +0,0 @@ -require "colorize" - -module Ameba - enum Severity - Error - Warning - Convention - - # Returns a symbol uniquely indicating severity. - # - # ``` - # Severity::Warning.symbol # => 'W' - # ``` - def symbol : Char - case self - in Error then 'E' - in Warning then 'W' - in Convention then 'C' - end - end - - # Returns a color uniquely indicating severity. - # - # ``` - # Severity::Warning.color # => Colorize::ColorANSI::Red - # ``` - def color : Colorize::Color - case self - in Error then Colorize::ColorANSI::Red - in Warning then Colorize::ColorANSI::Red - in Convention then Colorize::ColorANSI::Blue - end - end - - # Creates Severity by the name. - # - # ``` - # Severity.parse("convention") # => Severity::Convention - # Severity.parse("foo-bar") # => Exception: Incorrect severity name - # ``` - def self.parse(name : String) - super name - rescue ArgumentError - raise "Incorrect severity name #{name}. Try one of: #{values.map(&.to_s).join(", ")}" - end - end - - # Converter for `YAML.mapping` which converts severity enum to and from YAML. - class SeverityYamlConverter - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) - unless node.is_a?(YAML::Nodes::Scalar) - raise "Severity must be a scalar, not #{node.class}" - end - - case value = node.value - when String then Severity.parse(value) - when Nil then raise "Missing severity" - else - raise "Incorrect severity: #{value}" - end - end - - def self.to_yaml(value : Severity, yaml : YAML::Nodes::Builder) - yaml.scalar value - end - end -end diff --git a/lib/ameba/src/ameba/source.cr b/lib/ameba/src/ameba/source.cr deleted file mode 100644 index 29d9d19..0000000 --- a/lib/ameba/src/ameba/source.cr +++ /dev/null @@ -1,88 +0,0 @@ -module Ameba - # An entity that represents a Crystal source file. - # Has path, lines of code and issues reported by rules. - class Source - include InlineComments - include Reportable - - # Path to the source file. - getter path : String - - # Crystal code (content of a source file). - getter code : String - - # Creates a new source by `code` and `path`. - # - # For example: - # - # ``` - # path = "./src/source.cr" - # Ameba::Source.new File.read(path), path - # ``` - def initialize(@code, @path = "") - end - - # Corrects any correctable issues and updates `code`. - # Returns `false` if no issues were corrected. - def correct? - corrector = Corrector.new(code) - issues.each(&.correct(corrector)) - - corrected_code = corrector.process - return false if code == corrected_code - - @code = corrected_code - @lines = nil - @ast = nil - - true - end - - # Returns lines of code split by new line character. - # Since `code` is immutable and can't be changed, this - # method caches lines in an instance variable, so calling - # it second time will not perform a split, but will return - # lines instantly. - # - # ``` - # source = Ameba::Source.new "a = 1\nb = 2", path - # source.lines # => ["a = 1", "b = 2"] - # ``` - getter lines : Array(String) { code.split('\n') } - - # Returns AST nodes constructed by `Crystal::Parser`. - # - # ``` - # source = Ameba::Source.new code, path - # source.ast - # ``` - getter ast : Crystal::ASTNode do - Crystal::Parser.new(code) - .tap(&.wants_doc = true) - .tap(&.filename = path) - .parse - end - - getter fullpath : String do - File.expand_path(path) - end - - # Returns `true` if the source is a spec file, `false` otherwise. - def spec? - path.ends_with?("_spec.cr") - end - - # Returns `true` if *filepath* matches the source's path, `false` otherwise. - def matches_path?(filepath) - fullpath == File.expand_path(filepath) - end - - # Converts an AST location to a string position. - def pos(location : Crystal::Location, end end_pos = false) : Int32 - line, column = location.line_number, location.column_number - pos = lines[0...line - 1].sum(&.size) + line + column - 2 - pos += 1 if end_pos - pos - end - end -end diff --git a/lib/ameba/src/ameba/source/corrector.cr b/lib/ameba/src/ameba/source/corrector.cr deleted file mode 100644 index 33e722d..0000000 --- a/lib/ameba/src/ameba/source/corrector.cr +++ /dev/null @@ -1,200 +0,0 @@ -require "./rewriter" - -class Ameba::Source - # This class takes source code and rewrites it based - # on the different correction actions supplied. - class Corrector - @line_sizes = [] of Int32 - - def initialize(code : String) - code.each_line(chomp: false) do |line| - @line_sizes << line.size - end - @rewriter = Rewriter.new(code) - end - - # Replaces the code of the given range with *content*. - def replace(location, end_location, content) - @rewriter.replace(loc_to_pos(location), loc_to_pos(end_location) + 1, content) - end - - # :ditto: - def replace(range : Range(Int32, Int32), content) - begin_pos, end_pos = range.begin, range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.replace(begin_pos, end_pos, content) - end - - # Inserts the given strings before and after the given range. - def wrap(location, end_location, insert_before, insert_after) - @rewriter.wrap(loc_to_pos(location), loc_to_pos(end_location) + 1, insert_before, insert_after) - end - - # :ditto: - def wrap(range : Range(Int32, Int32), insert_before, insert_after) - begin_pos, end_pos = range.begin, range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.wrap(begin_pos, end_pos, insert_before, insert_after) - end - - # Shortcut for `replace(location, end_location, "")` - def remove(location, end_location) - @rewriter.remove(loc_to_pos(location), loc_to_pos(end_location) + 1) - end - - # Shortcut for `replace(range, "")` - def remove(range : Range(Int32, Int32)) - begin_pos, end_pos = range.begin, range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.remove(begin_pos, end_pos) - end - - # Shortcut for `wrap(location, end_location, content, nil)` - def insert_before(location, end_location, content) - @rewriter.insert_before(loc_to_pos(location), loc_to_pos(end_location) + 1, content) - end - - # Shortcut for `wrap(range, content, nil)` - def insert_before(range : Range(Int32, Int32), content) - begin_pos, end_pos = range.begin, range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.insert_before(begin_pos, end_pos, content) - end - - # Shortcut for `wrap(location, end_location, nil, content)` - def insert_after(location, end_location, content) - @rewriter.insert_after(loc_to_pos(location), loc_to_pos(end_location) + 1, content) - end - - # Shortcut for `wrap(range, nil, content)` - def insert_after(range : Range(Int32, Int32), content) - begin_pos, end_pos = range.begin, range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.insert_after(begin_pos, end_pos, content) - end - - # Shortcut for `insert_before(location, location, content)` - def insert_before(location, content) - @rewriter.insert_before(loc_to_pos(location), content) - end - - # Shortcut for `insert_before(pos.., content)` - def insert_before(pos : Int32, content) - @rewriter.insert_before(pos, content) - end - - # Shortcut for `insert_after(location, location, content)` - def insert_after(location, content) - @rewriter.insert_after(loc_to_pos(location) + 1, content) - end - - # Shortcut for `insert_after(...pos, content)` - def insert_after(pos : Int32, content) - @rewriter.insert_after(pos, content) - end - - # Removes *size* characters prior to the source range. - def remove_preceding(location, end_location, size) - @rewriter.remove(loc_to_pos(location) - size, loc_to_pos(location)) - end - - # :ditto: - def remove_preceding(range : Range(Int32, Int32), size) - begin_pos = range.begin - @rewriter.remove(begin_pos - size, begin_pos) - end - - # Removes *size* characters from the beginning of the given range. - # If *size* is greater than the size of the range, the removed region can - # overrun the end of the range. - def remove_leading(location, end_location, size) - @rewriter.remove(loc_to_pos(location), loc_to_pos(location) + size) - end - - # :ditto: - def remove_leading(range : Range(Int32, Int32), size) - begin_pos = range.begin - @rewriter.remove(begin_pos, begin_pos + size) - end - - # Removes *size* characters from the end of the given range. - # If *size* is greater than the size of the range, the removed region can - # overrun the beginning of the range. - def remove_trailing(location, end_location, size) - @rewriter.remove(loc_to_pos(end_location) + 1 - size, loc_to_pos(end_location) + 1) - end - - # :ditto: - def remove_trailing(range : Range(Int32, Int32), size) - end_pos = range.end - end_pos -= 1 unless range.excludes_end? - @rewriter.remove(end_pos - size, end_pos) - end - - private def loc_to_pos(location : Crystal::Location | {Int32, Int32}) - if location.is_a?(Crystal::Location) - line, column = location.line_number, location.column_number - else - line, column = location - end - @line_sizes[0...line - 1].sum + (column - 1) - end - - # Replaces the code of the given node with *content*. - def replace(node : Crystal::ASTNode, content) - replace(location(node), end_location(node), content) - end - - # Inserts the given strings before and after the given node. - def wrap(node : Crystal::ASTNode, insert_before, insert_after) - wrap(location(node), end_location(node), insert_before, insert_after) - end - - # Shortcut for `replace(node, "")` - def remove(node : Crystal::ASTNode) - remove(location(node), end_location(node)) - end - - # Shortcut for `wrap(node, content, nil)` - def insert_before(node : Crystal::ASTNode, content) - insert_before(location(node), content) - end - - # Shortcut for `wrap(node, nil, content)` - def insert_after(node : Crystal::ASTNode, content) - insert_after(end_location(node), content) - end - - # Removes *size* characters prior to the given node. - def remove_preceding(node : Crystal::ASTNode, size) - remove_preceding(location(node), end_location(node), size) - end - - # Removes *size* characters from the beginning of the given node. - # If *size* is greater than the size of the node, the removed region can - # overrun the end of the node. - def remove_leading(node : Crystal::ASTNode, size) - remove_leading(location(node), end_location(node), size) - end - - # Removes *size* characters from the end of the given node. - # If *size* is greater than the size of the node, the removed region can - # overrun the beginning of the node. - def remove_trailing(node : Crystal::ASTNode, size) - remove_trailing(location(node), end_location(node), size) - end - - private def location(node : Crystal::ASTNode) - node.location || raise "Missing location" - end - - private def end_location(node : Crystal::ASTNode) - node.end_location || raise "Missing end location" - end - - # Applies all scheduled changes and returns modified source as a new string. - def process - @rewriter.process - end - end -end diff --git a/lib/ameba/src/ameba/source/rewriter.cr b/lib/ameba/src/ameba/source/rewriter.cr deleted file mode 100644 index ec71bd1..0000000 --- a/lib/ameba/src/ameba/source/rewriter.cr +++ /dev/null @@ -1,136 +0,0 @@ -class Ameba::Source - # This class performs the heavy lifting in the source rewriting process. - # It schedules code updates to be performed in the correct order. - # - # For simple cases, the resulting source will be obvious. - # - # Examples for more complex cases follow. Assume these examples are acting on - # the source `puts(:hello, :world)`. The methods `#wrap`, `#remove`, etc. - # receive a range as the first two arguments; for clarity, examples below use - # English sentences and a string of raw code instead. - # - # ## Overlapping deletions: - # - # * remove `:hello, ` - # * remove `, :world` - # - # The overlapping ranges are merged and `:hello, :world` will be removed. - # - # ## Multiple actions at the same end points: - # - # Results will always be independent of the order they were given. - # Exception: rewriting actions done on exactly the same range (covered next). - # - # Example: - # - # * replace `, ` by ` => ` - # * wrap `:hello, :world` with `{` and `}` - # * replace `:world` with `:everybody` - # * wrap `:world` with `[`, `]` - # - # The resulting string will be `puts({:hello => [:everybody]})` - # and this result is independent of the order the instructions were given in. - # - # ## Multiple wraps on same range: - # - # * wrap `:hello` with `(` and `)` - # * wrap `:hello` with `[` and `]` - # - # The wraps are combined in order given and results would be `puts([(:hello)], :world)`. - # - # ## Multiple replacements on same range: - # - # * replace `:hello` by `:hi`, then - # * replace `:hello` by `:hey` - # - # The replacements are made in the order given, so the latter replacement - # supersedes the former and `:hello` will be replaced by `:hey`. - # - # ## Swallowed insertions: - # - # * wrap `world` by `__`, `__` - # * replace `:hello, :world` with `:hi` - # - # A containing replacement will swallow the contained rewriting actions - # and `:hello, :world` will be replaced by `:hi`. - # - # ## Implementation - # - # The updates are organized in a tree, according to the ranges they act on - # (where children are strictly contained by their parent). - class Rewriter - getter code : String - - def initialize(@code) - @action_root = Rewriter::Action.new(0, code.size) - end - - # Returns `true` if no (non trivial) update has been recorded - def empty? - @action_root.empty? - end - - # Replaces the code of the given range with *content*. - def replace(begin_pos, end_pos, content) - combine begin_pos, end_pos, - replacement: content.to_s - end - - # Inserts the given strings before and after the given range. - def wrap(begin_pos, end_pos, insert_before, insert_after) - combine begin_pos, end_pos, - insert_before: insert_before.to_s, - insert_after: insert_after.to_s - end - - # Shortcut for `replace(begin_pos, end_pos, "")` - def remove(begin_pos, end_pos) - replace(begin_pos, end_pos, "") - end - - # Shortcut for `wrap(begin_pos, end_pos, content, nil)` - def insert_before(begin_pos, end_pos, content) - wrap(begin_pos, end_pos, content, nil) - end - - # Shortcut for `wrap(begin_pos, end_pos, nil, content)` - def insert_after(begin_pos, end_pos, content) - wrap(begin_pos, end_pos, nil, content) - end - - # Shortcut for `insert_before(pos, pos, content)` - def insert_before(pos, content) - insert_before(pos, pos, content) - end - - # Shortcut for `insert_after(pos, pos, content)` - def insert_after(pos, content) - insert_after(pos, pos, content) - end - - # Applies all scheduled changes and returns modified source as a new string. - def process - String.build do |io| - last_end = 0 - @action_root.ordered_replacements.each do |begin_pos, end_pos, replacement| - io << code[last_end...begin_pos] << replacement - last_end = end_pos - end - io << code[last_end...code.size] - end - end - - protected def combine(begin_pos, end_pos, **attributes) - check_range_validity(begin_pos, end_pos) - action = Rewriter::Action.new(begin_pos, end_pos, **attributes) - @action_root = @action_root.combine(action) - end - - private def check_range_validity(begin_pos, end_pos) - return unless begin_pos < 0 || end_pos > code.size - raise IndexError.new( - "The range #{begin_pos}...#{end_pos} is outside the bounds of the source" - ) - end - end -end diff --git a/lib/ameba/src/ameba/source/rewriter/action.cr b/lib/ameba/src/ameba/source/rewriter/action.cr deleted file mode 100644 index a13013c..0000000 --- a/lib/ameba/src/ameba/source/rewriter/action.cr +++ /dev/null @@ -1,185 +0,0 @@ -class Ameba::Source::Rewriter - # :nodoc: - # Actions are arranged in a tree and get combined so that: - # - children are strictly contained by their parent - # - siblings all disjoint from one another and ordered - # - only actions with `replacement == nil` may have children - class Action - getter begin_pos : Int32 - getter end_pos : Int32 - getter replacement : String? - getter insert_before : String - getter insert_after : String - protected getter children : Array(Action) - - def initialize(@begin_pos, - @end_pos, - @insert_before = "", - @replacement = nil, - @insert_after = "", - @children = [] of Action) - end - - def combine(action) - return self if action.empty? # Ignore empty action - - if action.begin_pos == @begin_pos && action.end_pos == @end_pos - merge(action) - else - place_in_hierarchy(action) - end - end - - def empty? - replacement = @replacement - - @insert_before.empty? && - @insert_after.empty? && - @children.empty? && - (replacement.nil? || - (replacement.empty? && @begin_pos == @end_pos)) - end - - def ordered_replacements - replacement = @replacement - reps = [] of {Int32, Int32, String} - reps << {@begin_pos, @begin_pos, @insert_before} unless @insert_before.empty? - reps << {@begin_pos, @end_pos, replacement} if replacement - reps.concat(@children.flat_map(&.ordered_replacements)) - reps << {@end_pos, @end_pos, @insert_after} unless @insert_after.empty? - reps - end - - def insertion? - replacement = @replacement - - !@insert_before.empty? || - !@insert_after.empty? || - (replacement && !replacement.empty?) - end - - protected def with(*, - begin_pos = @begin_pos, - end_pos = @end_pos, - insert_before = @insert_before, - replacement = @replacement, - insert_after = @insert_after, - children = @children) - children = [] of Action if replacement - self.class.new(begin_pos, end_pos, insert_before, replacement, insert_after, children) - end - - protected def place_in_hierarchy(action) - family = analyze_hierarchy(action) - sibling_left, sibling_right = family[:sibling_left], family[:sibling_right] - - if fusible = family[:fusible] - child = family[:child] - child ||= [] of Action - fuse_deletions(action, fusible, sibling_left + child + sibling_right) - else - extra_sibling = - case - when parent = family[:parent] - # action should be a descendant of one of the children - parent.combine(action) - when child = family[:child] - # or it should become the parent of some of the children, - action.with(children: child).combine_children(action.children) - else - # or else it should become an additional child - action - end - self.with(children: sibling_left + [extra_sibling] + sibling_right) - end - end - - # Assumes *more_children* all contained within `@begin_pos...@end_pos` - protected def combine_children(more_children) - more_children.reduce(self) do |parent, new_child| - parent.place_in_hierarchy(new_child) - end - end - - protected def fuse_deletions(action, fusible, other_siblings) - without_fusible = self.with(children: other_siblings) - fusible = [action] + fusible - fused_begin_pos = fusible.min_of(&.begin_pos) - fused_end_pos = fusible.max_of(&.end_pos) - fused_deletion = action.with(begin_pos: fused_begin_pos, end_pos: fused_end_pos) - without_fusible.combine(fused_deletion) - end - - # Similar to `@children.bsearch_index || size` except allows for a starting point - protected def bsearch_child_index(from = 0, &) - size = @children.size - (from...size).bsearch { |i| yield @children[i] } || size - end - - # Returns the children in a hierarchy with respect to *action*: - # - # - `:sibling_left`, `:sibling_right` (for those that are disjoint from *action*) - # - `:parent` (in case one of our children contains *action*) - # - `:child` (in case *action* strictly contains some of our children) - # - `:fusible` (in case *action* overlaps some children but they can be fused in one deletion) - # - # In case a child has equal range to *action*, it is returned as `:parent` - # - # Reminder: an empty range 1...1 is considered disjoint from 1...10 - protected def analyze_hierarchy(action) # ameba:disable Metrics/CyclomaticComplexity - # left_index is the index of the first child that isn't completely to the left of action - left_index = bsearch_child_index { |child| child.end_pos > action.begin_pos } - # right_index is the index of the first child that is completely on the right of action - start = left_index == 0 ? 0 : left_index - 1 # See "corner case" below for reason of -1 - right_index = bsearch_child_index(start) { |child| child.begin_pos >= action.end_pos } - center = right_index - left_index - case center - when 0 - # All children are disjoint from action, nothing else to do - when -1 - # Corner case: if a child has empty range == action's range - # then it will appear to be both disjoint and to the left of action, - # as well as disjoint and to the right of action. - # Since ranges are equal, we return it as parent - left_index -= 1 # Fix indices, as otherwise this child would be - right_index += 1 # considered as a sibling (both left and right!) - parent = @children[left_index] - else - overlap_left = @children[left_index].begin_pos <=> action.begin_pos - overlap_right = @children[right_index - 1].end_pos <=> action.end_pos - - raise "Unable to compare begin pos" if overlap_left.nil? - raise "Unable to compare end pos" if overlap_right.nil? - - # For one child to be the parent of action, we must have: - if center == 1 && overlap_left <= 0 && overlap_right >= 0 - parent = @children[left_index] - else - # Otherwise consider all non disjoint elements (center) to be contained... - contained = @children[left_index...right_index] - fusible = [] of Action - fusible << contained.shift if overlap_left < 0 # ... but check first and last one - fusible << contained.pop if overlap_right > 0 # ... for overlaps - fusible = nil if fusible.empty? - end - end - - { - parent: parent, - sibling_left: @children[0...left_index], - sibling_right: @children[right_index...@children.size], - fusible: fusible, - child: contained, - } - end - - # Assumes *action* has the exact same range and has no children - protected def merge(action) - self.with( - insert_before: "#{action.insert_before}#{insert_before}", - replacement: action.replacement || @replacement, - insert_after: "#{insert_after}#{action.insert_after}", - ).combine_children(action.children) - end - end -end diff --git a/lib/ameba/src/ameba/tokenizer.cr b/lib/ameba/src/ameba/tokenizer.cr deleted file mode 100644 index 86f6aa3..0000000 --- a/lib/ameba/src/ameba/tokenizer.cr +++ /dev/null @@ -1,100 +0,0 @@ -require "compiler/crystal/syntax/*" - -module Ameba - # Represents Crystal syntax tokenizer based on `Crystal::Lexer`. - # - # ``` - # source = Ameba::Source.new code, path - # tokenizer = Ameba::Tokenizer.new(source) - # tokenizer.run do |token| - # puts token - # end - # ``` - class Tokenizer - # Instantiates Tokenizer using a `source`. - # - # ``` - # source = Ameba::Source.new code, path - # Ameba::Tokenizer.new(source) - # ``` - def initialize(source) - @lexer = Crystal::Lexer.new source.code - @lexer.count_whitespace = true - @lexer.comments_enabled = true - @lexer.wants_raw = true - @lexer.filename = source.path - end - - # Instantiates Tokenizer using a `lexer`. - # - # ``` - # lexer = Crystal::Lexer.new(code) - # Ameba::Tokenizer.new(lexer) - # ``` - def initialize(@lexer : Crystal::Lexer) - end - - # Runs the tokenizer and yields each token as a block argument. - # - # ``` - # Ameba::Tokenizer.new(source).run do |token| - # puts token - # end - # ``` - def run(&block : Crystal::Token -> _) - run_normal_state @lexer, &block - true - rescue e : Crystal::SyntaxException - # puts e - false - end - - private def run_normal_state(lexer, break_on_rcurly = false, &block : Crystal::Token -> _) - loop do - token = @lexer.next_token - block.call token - - case token.type - when .delimiter_start? - run_delimiter_state lexer, token, &block - when .string_array_start?, .symbol_array_start? - run_array_state lexer, token, &block - when .eof? - break - when .op_rcurly? - break if break_on_rcurly - end - end - end - - private def run_delimiter_state(lexer, token, &block : Crystal::Token -> _) - loop do - token = @lexer.next_string_token(token.delimiter_state) - block.call token - - case token.type - when .delimiter_end? - break - when .interpolation_start? - run_normal_state lexer, break_on_rcurly: true, &block - when .eof? - break - end - end - end - - private def run_array_state(lexer, token, &block : Crystal::Token -> _) - loop do - lexer.next_string_array_token - block.call token - - case token.type - when .string_array_end? - break - when .eof? - break - end - end - end - end -end diff --git a/lib/ameba/src/cli.cr b/lib/ameba/src/cli.cr deleted file mode 100644 index 4bb38cf..0000000 --- a/lib/ameba/src/cli.cr +++ /dev/null @@ -1,3 +0,0 @@ -require "./ameba/cli/cmd" - -Ameba::Cli.run diff --git a/lib/ameba/src/contrib/read_type_doc.cr b/lib/ameba/src/contrib/read_type_doc.cr deleted file mode 100644 index 53ec0e2..0000000 --- a/lib/ameba/src/contrib/read_type_doc.cr +++ /dev/null @@ -1,29 +0,0 @@ -require "compiler/crystal/syntax/*" - -private class DocFinder < Crystal::Visitor - getter type_name : String - getter doc : String? - - def initialize(nodes, @type_name) - self.accept(nodes) - end - - def visit(node : Crystal::ASTNode) - return false if @doc - - if node.responds_to?(:name) && (name = node.name).is_a?(Crystal::Path) - @doc = node.doc if name.names.last? == @type_name - end - - true - end -end - -type_name, path_to_source_file = ARGV - -source = File.read(path_to_source_file) -nodes = Crystal::Parser.new(source) - .tap(&.wants_doc = true) - .parse - -puts DocFinder.new(nodes, type_name).doc diff --git a/lib/crinja/.ameba.yml b/lib/crinja/.ameba.yml deleted file mode 100644 index 7a65b95..0000000 --- a/lib/crinja/.ameba.yml +++ /dev/null @@ -1,5 +0,0 @@ -Lint/UnusedArgument: - IgnoreProcs: true - -Style/WhileTrue: - Enabled: false diff --git a/lib/crinja/.circleci/config.yml b/lib/crinja/.circleci/config.yml deleted file mode 100644 index 9c93b30..0000000 --- a/lib/crinja/.circleci/config.yml +++ /dev/null @@ -1,114 +0,0 @@ -version: 2 - -dry: - restore_shards_cache: &restore_shards_cache - # Use {{ checksum "shard.yml" }} if developing a shard instead of an app - keys: - - shards-cache-v1-{{ .Branch }}-{{ checksum "shard.yml" }} - - shards-cache-v1-{{ .Branch }} - - shards-cache-v1 - - save_shards_cache: &save_shards_cache - # Use {{ checksum "shard.yml" }} if developing a shard instead of an app - key: shards-cache-v1-{{ .Branch }}-{{ checksum "shard.yml" }} - paths: - - ./shards-cache - -jobs: - test: - docker: - # Use crystallang/crystal:latest or specific crystallang/crystal:VERSION - - image: crystallang/crystal:latest - environment: - SHARDS_CACHE_PATH: ./shards-cache - steps: - - run: crystal --version - - - checkout - - - restore_cache: *restore_shards_cache - - run: shards - - save_cache: *save_shards_cache - - - run: - name: Install bats - command: | - git clone --branch v0.4.0 https://github.com/sstephenson/bats.git bats-v0.4.0 - ./bats-v0.4.0/install.sh /usr/local - - - run: make test - - - run: crystal tool format --check spec src - - deploy-docs: - docker: - # Use crystallang/crystal:latest or specific crystallang/crystal:VERSION - - image: crystallang/crystal:latest - environment: - SHARDS_CACHE_PATH: ./shards-cache - steps: - - run: crystal --version - - - checkout - - - restore_cache: *restore_shards_cache - - run: shards - - save_cache: *save_shards_cache - - - run: scripts/generate-docs.sh - - - run: apt update && apt install -y curl rsync - - run: - command: curl https://raw.githubusercontent.com/straight-shoota/autodeploy-docs/master/autodeploy-docs.sh | bash - environment: - GIT_COMMITTER_NAME: cirlceci - GIT_COMMITTER_EMAIL: circle@circleci.com - - test-on-nightly: - docker: - - image: crystallang/crystal:nightly - environment: - SHARDS_CACHE_PATH: ./shards-cache - steps: - - run: crystal --version - - - checkout - - - restore_cache: *restore_shards_cache - - run: shards - - - run: - name: Install bats - command: | - git clone --branch v0.4.0 https://github.com/sstephenson/bats.git bats-v0.4.0 - ./bats-v0.4.0/install.sh /usr/local - - - run: make test - - - run: crystal tool format --check spec src - -workflows: - version: 2 - # Run tests on every single commit - ci: - jobs: - - test - # Build and depoy docs only on master branch - - deploy-docs: - requires: - - test - filters: &master-only - branches: - only: - - master - # Run tests every night using crystal nightly - nightly: - triggers: - - schedule: - cron: "0 4 * * *" - filters: - branches: - only: - - master - jobs: - - test-on-nightly diff --git a/lib/crinja/.editorconfig b/lib/crinja/.editorconfig deleted file mode 100644 index 8f0c87a..0000000 --- a/lib/crinja/.editorconfig +++ /dev/null @@ -1,7 +0,0 @@ -[*.cr] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -indent_style = space -indent_size = 2 -trim_trailing_whitespace = true diff --git a/lib/crinja/.gitignore b/lib/crinja/.gitignore deleted file mode 100644 index e283476..0000000 --- a/lib/crinja/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -/docs/ -/tmp/ -/lib/ -/build/ -/bin/ -.shards/ -/examples/*/lib/ -/examples/*/docs/ -/examples/*/bin/ -/examples/*/shard.lock - -*.rendered.html -/.cache -__pycache__ - -# Libraries don't need dependency lock -# Dependencies will be locked in application that uses them -/shard.lock diff --git a/lib/crinja/.overcommit.yml b/lib/crinja/.overcommit.yml deleted file mode 100644 index 5894e1b..0000000 --- a/lib/crinja/.overcommit.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Use this file to configure the Overcommit hooks you wish to use. This will -# extend the default configuration defined in: -# https://github.com/brigade/overcommit/blob/master/config/default.yml -# -# At the topmost level of this YAML file is a key representing type of hook -# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can -# customize each hook, such as whether to only run it on certain files (via -# `include`), whether to only display output if it fails (via `quiet`), etc. -# -# For a complete list of hooks, see: -# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook -# -# For a complete list of options that you can use to customize hooks, see: -# https://github.com/brigade/overcommit#configuration -# -# Uncomment the following lines to make the configuration take effect. - -PreCommit: -# RuboCop: -# enabled: true -# on_warn: fail # Treat all warnings as failures -# - CustomScript: - enabled: true - command: crystal spec - CustomScript: - enabled: true - command: crystal format - TrailingWhitespace: - enabled: true -# exclude: -# - '**/db/structure.sql' # Ignore trailing whitespace in generated files -# -#PostCheckout: -# ALL: # Special hook name that customizes all hooks of this type -# quiet: true # Change all post-checkout hooks to only display output on failure -# -# IndexTags: -# enabled: true # Generate a tags file with `ctags` each time HEAD changes diff --git a/lib/crinja/.travis.yml b/lib/crinja/.travis.yml deleted file mode 100644 index ba07afe..0000000 --- a/lib/crinja/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -dist: bionic -language: crystal - -env: - global: - GIT_COMMITTER_NAME: travis-ci - GIT_COMMITTER_EMAIL: travis@travis-ci.org - -crystal: -- latest -- nightly - -script: -- make test/unit -- make format CRYSTAL_OPTS=--check - -jobs: - include: - - stage: test - name: integration tests - addons: - apt: - sources: - - sourceline: 'ppa:duggan/bats' - packages: - - bats - script: make test/integration - crystal: latest - - stage: test - name: integration tests - addons: - apt: - sources: - - sourceline: 'ppa:duggan/bats' - packages: - - bats - script: make test/integration - crystal: nightly diff --git a/lib/crinja/CHANGELOG.md b/lib/crinja/CHANGELOG.md deleted file mode 100644 index ccf7252..0000000 --- a/lib/crinja/CHANGELOG.md +++ /dev/null @@ -1,108 +0,0 @@ -# Changelog -All notable changes to Crinja will be documented in this file. - -## 0.8.0 (2023-03-06) - -Compatibility with PCRE2 (Crystal 1.8) - -## 0.8.0 (2021-07-16) - -Compatbility with Crystal 1.1 - -* Updates dependencies with more relaxed version restrictions -* Removes autogeneration for predicate method without suffix to avoid duplicate when conditions -* Fixes type bugs discovered through Crystal 1.1 -* Adds GitHub actions -* Fixes some minor documentation bugs - -## 0.7.0 (2021-02-06) - -* Improves `TagCycleException` -* Adds `do` tag (#33, thanks @n-rodriguez) -* Adds compatibility with Crystal >= 0.35.1 and Shards >= 0.11.0 -* Adds support for mapping predicate methods -* Smaller cleanup and improvements - -## 0.6.1 (2020-06-09) - -Compatibility with Crystal 0.35.0 - -## 0.6.0 (2020-04-03) - -Compatibility with Crystal 0.34.0 - -* Improvements to Makefile and CI setup -* Use `Log` framework from Crystal 0.34.0 - -## 0.5.1 (2020-01-14) - -This release brings compatibility with Crystal 0.32.1 - -## 0.5.0 (2019-06-07) - -This release brings compatibility with Crystal 0.29.0 - -* Rename `FeatureLibrary#aliasses` to `#aliases` -* Add experimental support for liquid syntax with `Crinja.liquid_support` - -## 0.4.1 (2019-01-01) - -This release doesn't add any new features but fixes compatibility with Crystal 0.27.0. - -## 0.4.0 (2018-10-16) - -This release comes with some refactorings of the public API to make it easier to use. -Most prominently, annotation based autogenerator for exposing object properties to the Crinja runtime were added. - -```cr -require "crinja" - -class User - include Crinja::Object::Auto - - @[Crinja::Attribute] - def name : String - "john paul" - end -end - -Crinja.new.from_string("{{ user.name }}").render({"user" => User.new}) # => "john paul" -``` - -Autogeneration of `crinja_call` will be left for the next release. - -Most other changes involve the CI infrastructure, with Circle CI taking over the main load from travis. - -* **(breaking-change)** Replaced `Crinja::PyObject` by `Crinja::Object` and renamed hook methods to `crinja_attribute` and `crinja_call`. `getitem` hook has been removed. -* **(breaking-change)** Added `Crinja::Object::Auto` for generating automatic bindings for `crinja_attribute` (previously provided by `Crinja::PyObject.getattr`). The behaviour can be configured using annotations `Crinja::Attribute` and `Crinja::Attributes`. -* **(breaking-change)** Renamed `Crinja::Callable::Arguments` to `Crinja::Arguments`. The API has been simplified by removing unused setters. -* **(breaking-change)** Removed `Crinja::Arguments#[](key : Symbol)`. Use a string key instead. -* **(breaking-change)** Renamed `Crinja::Callable::Arguments::UnknownArgumentException` to `Crinja::Arguments.:UnknownArgumentError`. -* **(breaking-change)** Renamed `Crinja::Callable::Arguments::ArgumentError` to `Crinja::Arguments::Error`. -* **(breaking-change)** Renamed `Crinja::PyTuple` to `Crinja::Tuple`. -* **(breaking-change)** Updated `BakedFileLoader` for compatibility with `baked_file_system 0.9.6` -* Upgraded to Crystal 0.26.1. -* Fixed number filters (`int` and `float`) to not rely on raising an error. -* Fixed `generate-docs` script. -* Added `Makefile`. -* Added Circle CI integration with nighly builds testing with Crystal nightly. -* Added integration tests for usage examples (`./examples`) in travis-ci and Circle CI. -* Added automatic docs generation to circle CI workflow and removed it from travis-ci. -* Added formatter checks to CI checks. -* Added preliminary Windows support by removing dependency on `xml`. -* Added this changelog. -* Improved reference to exmples in README. - -## 0.3.0 (2018-06-29) - -This release updated Crinja to work with Crystal 0.25.1 - -Notable changes: - -* **(breaking-change)** Renamed `Crinja::Environment` to just `Crinja` -* **(breaking-change)** Removed `Crinja::Type` in favour of `Crinja::Value` to avoid recursive aliases and reduce a lot of `.as(Type)` castings all over the place. This change was similar to `JSON::Type` -> `JSON::Any` in Crystal 0.25.0. -* **(breaking-change)** Removed `Crinja::Bindings`. Some methods are obsolete with `Crinja::Value`, others moved to `Crinja` namespace. -* Added dedicated documentation of [*Template Syntax*](https://github.com/straight-shoota/crinja/blob/5b1a3c30fac48f8bfccab5043fbda209f7859046/TEMPLATE_SYNTAX.md) - -## 0.2.1 (2018-01-01) - diff --git a/lib/crinja/LICENSE b/lib/crinja/LICENSE deleted file mode 100644 index d06d775..0000000 --- a/lib/crinja/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2017 Johannes Mรผller - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/lib/crinja/Makefile b/lib/crinja/Makefile deleted file mode 100644 index c6ec712..0000000 --- a/lib/crinja/Makefile +++ /dev/null @@ -1,60 +0,0 @@ --include Makefile.local # for optional local options - -SHARDS ::= shards # The shards command to use -CRYSTAL ::= crystal # The crystal command to use - -SRC_SOURCES ::= $(shell find src lib -name '*.cr' 2>/dev/null) -LIB_SOURCES ::= $(shell find lib -name '*.cr' 2>/dev/null) -SPEC_SOURCES ::= $(shell find spec -name '*.cr' 2>/dev/null) - -.PHONY: test -test: ## Run test suite -test: test/unit test/integration - -.PHONY: test/unit -test/unit: ## Run unit tests -test/unit: lib - $(CRYSTAL) spec - -.PHONY: test/integration -test/integration: ## Run integration tests -test/integration: - make -C examples - -.PHONY: format -format: ## Apply source code formatting -format: $(SRC_SOURCES) $(SPEC_SOURCES) - $(CRYSTAL) tool format src spec - -docs: ## Generate API docs -docs: $(SRC_SOURCES) lib - $(CRYSTAL) docs src/docs.cr - -lib: shard.lock - $(SHARDS) install - -shard.lock: ## Update shards -shard.lock: shard.yml - $(SHARDS) update - -.PHONY: clean -clean: ## Remove application binary -clean: - rm -rf docs - -.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}' diff --git a/lib/crinja/README.md b/lib/crinja/README.md deleted file mode 100644 index 273d882..0000000 --- a/lib/crinja/README.md +++ /dev/null @@ -1,346 +0,0 @@ -# crinja - -[![Build Status](https://travis-ci.org/straight-shoota/crinja.svg?branch=master)](https://travis-ci.org/straight-shoota/crinja) -[![CircleCI](https://circleci.com/gh/straight-shoota/crinja.svg?style=svg)](https://circleci.com/gh/straight-shoota/crinja) -[![Open Source Helpers](https://www.codetriage.com/straight-shoota/crinja/badges/users.svg)](https://www.codetriage.com/straight-shoota/crinja) - -Crinja is an implementation of the [Jinja2 template engine](http://jinja.pocoo.org) written in [Crystal](http://crystallang.org). Templates are parsed and evaluated at runtime (see [Background](#background)). It includes a script runtime for evaluation of dynamic python-like expressions used by the Jinja2 syntax. - -**[API Documentation](https://straight-shoota.github.io/crinja/api/latest/)** ยท -**[Github Repo](https://github.com/straight-shoota/crinja)** ยท -**[Template Syntax](https://github.com/straight-shoota/crinja/blob/master/TEMPLATE_SYNTAX.md)** - -## Features - -Crinja tries to stay close to the Jinja2 language design and implementation. It currently provides most features of the original template language, such as: - -* all basic language features like control structures and expressions -* template inheritance -* block scoping -* custom tags, filters, functions, operators and tests -* autoescape by default -* template cache - -From Jinja2 all builtin [control structures (tags)](http://jinja.pocoo.org/docs/2.9/templates/#list-of-control-structures), [tests](http://jinja.pocoo.org/docs/2.9/templates/#list-of-builtin-tests), [global functions](http://jinja.pocoo.org/docs/2.9/templates/#list-of-global-functions), [operators](http://jinja.pocoo.org/docs/2.9/templates/#expressions) and [filters](http://jinja.pocoo.org/docs/2.9/templates/#list-of-builtin-filters) have been ported to Crinja. See `Crinja::Filter`, `Crinja::Test`, `Crinja::Function`, `Crinja::Tag`, `Crinja::Operator` for lists of builtin features. - -Currently, template errors fail fast raising an exception. It is considered to change this behaviour to collect multiple errors, similar to what Jinjava does. - -## Installation - -Add this to your application's `shard.yml`: - -```yaml -dependencies: - crinja: - github: straight-shoota/crinja -``` - -## Usage - -### Simple string template -```crystal -require "crinja" - -Crinja.render("Hello, {{ name }}!", {"name" => "John"}) # => "Hello, John!" -``` - -### File loader - -With this template file: -```html -# views/index.html.j2 -

Hello {{ name | default('World') }}

-``` - -It can be loaded with a `FileSystemLoader`: - -```crystal -require "crinja" - -env = Crinja.new -env.loader = Crinja::Loader::FileSystemLoader.new("views/") -template = env.get_template("index.html.j2") -template.render # => "Hello, World!" -template.render({ "name" => "John" }) # => "Hello, John!" -``` - -### Crystal Playground - -Run the **Crystal playground** inside this repostitory and the server is prepared with examples of using Crinja's API (check the `Workbooks` section). - -```shell -$ crystal play -``` - -You can also browse the examples and documentation online (without the interactive playground): [objects](https://straight-shoota.github.io/crinja/api/latest/playground/objects.html) & [features](https://straight-shoota.github.io/crinja/api/latest/playground/features.html) - -### Crinja Playground - -The **Crinja Example Server** in [`examples/server`](https://github.com/straight-shoota/crinja/tree/master/examples/server) is an HTTP server which renders Crinja templates from `examples/server/pages`. It has also an interactive playground for Crinja template testing at `/play`. - -```shell -$ cd examples/server && crystal server.cr -``` - -Other examples can be found in the [`examples` folder](https://github.com/straight-shoota/crinja/tree/master/examples). - -## Template Syntax - -The following is a quick overview of the template language to get you started. - -More details can be found in **[the template guide](https://github.com/straight-shoota/crinja/blob/master/TEMPLATE_SYNTAX.md)**. -The original [Jinja2 template reference](http://jinja.pocoo.org/docs/2.9/templates/) can also be helpful, Crinja templates are mostly similar. - -### Expressions - -In a template, **expressions** inside double curly braces (`{{` ... `}}`) will be evaluated and printed to the template output. - -Assuming there is a variable `name` with value `"World"`, the following template renders `Hello, World!`. - -```html+jinja -Hello, {{ name }}! -``` - -Properties of an object can be accessed by dot (`.`) or square brackets (`[]`). Filters modify the value of an expression. - -```html+jinja -Hello, {{ current_user.name | default("World") | titelize }}! -``` - -Tests are similar to filters, but are used in the context of a boolean expression, for example as condition of an `if` tag. - -```html+jinja -{% if current_user is logged_in %} - Hello, {{ current_user.name }}! -{% else %} - Hey, stranger! -{% end %} -``` - -### Tags - -**Tags** control the logic of the template. They are enclosed in `{%` and `%}`. - -```html+jinja -{% if is_morning %} - Good Morning, {{ name }}! -{% else %} - Hello, {{ name }}! -{% end %} -``` - -The `for` tag allows looping over a collection. - -```html+jinja -{% for name in users %} - {{ user.name }} -{% endfor %} -``` - -Other templates can be included using the `include` tag: - -```html+jinja -{% include "header.html" %} - -
- Content -
- -{% include "footer.html" %} -``` - -### Macros - -Macros are similar to functions in other programming languages. - -```html+jinja -{% macro say_hello(name) %}Hello, {{ name | default("stranger") }}!{% endmacro %} -{{ say_hello('Peter') }} -{{ say_hello('Paul') }} -``` - -### Template Inheritance -Template inheritance enables the use of `block` tags in parent templates that can be overwritten by child templates. This is useful for implementating layouts: - -```html+jinja -{# layout.html #} - -

{% block page_title %}{% endblock %}

- -
- {% block body %} - {# This block is typically overwritten by child templates #} - {% endblock %} -
- -{% block footer %} - {% include "footer.html" %} -{% endblock %} -``` - -```html+jinja -{# page.html #} -{% extends "layout.html" %} - -{% block page_title %}Blog Index{% endblock %} -{% block body %} - -{% endblock %} -``` - -## Crystal API - -The API tries to stick ot the original [Jinja2 API](http://jinja.pocoo.org/docs/2.9/api/) which is written in Python. - -**[API Documentation](https://straight-shoota.github.io/crinja/api/latest/)** - -### Configuration - -Currently the following configuration options for `Config` are supported: - -
-
autoescape
-
-

This config allows the same settings as select_autoescape in Jinja 2.9.

-

It intelligently sets the initial value of autoescaping based on the filename of the template.

-

When set to a boolean value, false deactivates any autoescape and true activates autoescape for any template. - It also allows more detailed configuration:

-
-
enabled_extensions
-
List of filename extensions that autoescape should be enabled for. Default: ["html", "htm", "xml"]
-
disabled_extensions
-
List of filename extensions that autoescape should be disabled for. Default: [] of String
-
default_for_string
-
Determines autoescape default value for templates loaded from a string (without a filename). Default: false
-
default
-
If nothing matches, this will be the default autoescape value. Default: false
-
-

Note: The default configuration of Crinja differs from that of Jinja 2.9, that autoescape is activated by default for HTML and XML files. This will most likely be changed by Jinja2 in the future, too.

-
-
disabled_filters
-
A list of *disabled_filters* that will raise a `SecurityError` when invoked.
-
disabled_functions
-
A list of *disabled_functions* that will raise a `SecurityError` when invoked.
-
disabled_operators
-
A list of *disabled_operators* that will raise a `SecurityError` when invoked.
-
disabled_tags
-
A list of *disabled_tags* that will raise a `SecurityError` when invoked.
-
disabled_tests
-
A list of *disabled_tests* that will raise a `SecurityError` when invoked.
-
keep_trailing_newline
-
Preserve the trailing newline when rendering templates. If set to `false`, a single newline, if present, will be stripped from the end of the template. Default: false
-
trim_blocks
-
If this is set to true, the first newline after a block is removed. This only applies to blocks, not expression tags. Default: false.
-
lstrip_blocks
-
If this is set to true, leading spaces and tabs are stripped from the start of a line to a block. Default: false.
- register_defaults -
If register_defaults is set to true, all feature libraries will be populated with the defaults (Crinja standards and registered custom features). - Otherwise the libraries will be empty. They can be manually populated with library.register_defaults. - This setting needs to be set at the creation of an environment.
-
- -See also the original [Jinja2 API Documentation](http://jinja.pocoo.org/docs/2.9/api/). - -### Custom features - -You can provide custom tags, filters, functions, operators and tests. Create an implementation using the macros `Crinja.filter`, `Crinja.function`, `Crinja.test`. They need to be passed a block which will be converted to a Proc. Optional arguments are a `Hash` or `NamedTuple` with default arguments and a name. If a name is provided, it will be added to the feature library defaults and available in every environment which uses the registered defaults. - -Example with macro `Crinja.filter`: - -```crystal -env = Crinja.new - -myfilter = Crinja.filter({ attribute: nil }) do - "#{target} is #{arguments["attribute"]}!" -end - -env.filters["customfilter"] = myfilter - -template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }})) -template.render # => "Hello World is super!" -``` - -Or you can define a class for more complex features: -```crystal -class Customfilter - include Crinja::Callable - - getter name = "customfilter" - - getter defaults = Crinja.variables({ - "attribute" => "great" - }) - - def call(arguments) - "#{arguments.target} is #{arguments["attribute"]}!" - end -end - -env = Crinja.new -env.filters << Customfilter.new - -template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }})) -template.render # => "Hello World is super!" -``` - -Custom tags and operator can be implemented through subclassing `Crinja::Operator` and `Crinja:Tag` and adding an instance to the feature library defaults (`Crinja::Operator::Library.defaults << MyTag.new`) or to a specific environment (`env.tags << MyTag.new`). - -## Differences from Jinja2 - -This is an incomplete list of **Differences to the original Jinja2**: - -* **Python expressions:** Because templates are evaluated inside a compiled Crystal program, it's not possible to use ordinary Python expressions in Crinja. But it might be considered to implement some of the Python stdlib like `Dict#iteritems()` which is often used to make dicts iterable. -* **Line statements and line comments**: Are not supported, because their usecase is negligible. -* **String representation:** Some objects will have slightly different representation as string or JSON. Crinja uses Crystal internals, while Jinja uses Python internals. For example, an array with strings like `{{ ["foo", "bar"] }}` will render as `[u'foo', u'bar']` in Jinja2 and as `['foo', 'bar']` in Crinja. -* **Double escape:** `{{ ''|escape|escape }}` will render as `<html>` in Jinja2, but `&lt;html&gt;`. Should we change that behaviour? -* **Complex numbers**: Complex numbers are not supported yet. -* **Configurable syntax**: It is not possible to reconfigure the syntax symbols. This makes the parser less complex and faster. - -The following features are not yet fully implemented, but on the [roadmap](ROADMAP.md): - -* Sandboxed execution. -* Some in-depth features like extended macro reflection, reusable blocks. - -## Background - -Crystal is a great programming language with a clean syntax inspired by Ruby, but it is compiled and runs incredibly fast. - -There are already some [template engines for crystal](https://github.com/veelenga/awesome-crystal#template-engine). But if you want control structures and dynamic expressions without some sort of Domain Specific Language, there is only [Embedded Crystal (ECR)](https://crystal-lang.org/api/0.21.1/ECR.html), which is a part of Crystal's standard library. It uses macros to convert templates to Crystal code and embed them into the source at compile time. So for every change in a template, you have to recompile the binary. This approach is certainly applicable for many projects and provides very fast template rendering. The downside is, you need a crystal build stack for template design. This makes it impossible to render dynamic, user defined templates, that can be changed at runtime. - -Jinja2 is a powerful, mature template engine with a great syntax and proven language design. Its philosophy is: - -> Application logic is for the controller, but don't make the template designer's life difficult by restricting functionality too much. - -Jinja derived from the [Django Template Language](http://docs.djangoproject.com/en/dev/ref/templates/builtins/). While it comes from web development and is heavily used there ([Flask](http://flask.pocoo.org/)) -[Ansible](https://ansible.com/) and [Salt](http://www.saltstack.com/) use it for dynamic enhancements of configuration data. It has quite a number of implementations and adaptations in other languages: - -* [Jinjava](https://github.com/HubSpot/jinjava) - Jinja2 implementation in Java using [Unified Expression Language](https://uel.java.net/) (`javaex.el`) for expression resolving. It served as an inspiration for some parts of Crinja. -* [Liquid](https://shopify.github.io/liquid/) - Jinja2-inspired template engine in Ruby -* [Liquid.cr](https://github.com/TechMagister/liquid.cr) - Liquid implementation in Crystal -* [Twig](https://twig.symfony.com/) - Jinja2-inspired template engine in PHP -* [ginger](https://hackage.haskell.org/package/ginger) - Jinja2 implementation in Haskell -* [Jinja-Js](https://github.com/sstur/jinja-js) - Jinja2-inspired template engin in Javascript -* [jigo](https://github.com/jmoiron/jigo) - Jinja2 implementation in Go -* [tera](https://github.com/Keats/tera) - Jinja2 implementation in Rust -* [jingoo](https://github.com/tategakibunko/jingoo) - Jinja2 implementation in OCaml -* [nunjucks](https://mozilla.github.io/nunjucks/) - Jinja2 inspired template engine in Javascript - -## Contributing - -1. Fork it () -2. Create your feature branch (`git checkout -b my-new-feature`) -3. Commit your changes (`git commit -am 'Add some feature'`) -4. Push to the branch (`git push origin my-new-feature`) -5. Create a new Pull Request - -## Contributors - -- [straight-shoota](https://github.com/straight-shoota) Johannes Mรผller - creator, maintainer diff --git a/lib/crinja/TEMPLATE_SYNTAX.md b/lib/crinja/TEMPLATE_SYNTAX.md deleted file mode 100644 index 3826c89..0000000 --- a/lib/crinja/TEMPLATE_SYNTAX.md +++ /dev/null @@ -1,181 +0,0 @@ -# Introduction to Crinja Templates - -The template features supported by Crinja are based on the [Jinja2 template language](http://jinja.pocoo.org) which is originally written in Python. - -**[API Documentation](https://straight-shoota.github.io/crinja/api/latest/)** ยท -**[Github Repo](https://github.com/straight-shoota/crinja)** - -## Overview - -Crinja template syntax can be embedded in any text content and individual templates features are enclosed in delimiters: - -```html+jinja -{{ }} - print -{% %} - tag -{# #} - comment -``` - -When a template is rendered, **print statements** enclosed by double curly braces (`{{` ... `}}`) print their inner value to the template output. -Template **expressions** inside will be evaluated. - -Assuming there is a variable `name` with value `"World"`, the following template expands to `Hello, World!`. - -```html+jinja -Hello, {{ name }}! -``` - -**Tags** control the logic of the template. They are enclosed in `{%` and `%}`. - -The `set` tag for example is used for assigments: -```html+jinja -{% set name = "John" %} -Hello, {{ name }}! -``` - -Most tags expect a content which spans between an opening tag and a closing tag. The latter has the same name name as the opening tag prefixed with `end`. -Tags can be nested. - -```html+jinja -{% if name == "World" %} -Hello ๐ŸŒ! -{% endif %} -``` - -**Comments** are enclosed in `{#` and `#}`. They are not parsed as template content and will not included in the template output. - -## Variables - -Template variables are defined in the context of each template. -Varibales can be populated externally by the application calling the template, or dynamically defined within. -The [`set` tag](#set-tag) allows to set or modify variables inside the template. - -```html+jinja -{% set name = "World" %} -Hello, {{ name }}! -> Hello, World! -``` - -Members of objects can be traversed by a dot (`.`). `foo.bar` resolves the property `bar` of object `foo`. -Another option are square brackets (`[]`) where the name of the member equals to the value between the brackets. Above expression would be equal to `foo["bar"]`. - -An empty value is expressed as `none`, similar to `nil` in Crystal. - -If the value of a variable or expression simply does not exist at all, it will be *undefined*. Printing an undefined value will insert the empty string. In other contexts an undefined value might also be treated as empty or raise an error. - -## Filters - -Filters transform or alter a value. They are appended to any expression using a pipe symobl (`|`) followed by the name of the filter. `name | upper` applies the filter `upper` to the value of the variable `name`. - -Arguments are added in parenthesis: `names | join(', ')`. - -Filters can be chained and the outputs will be used in sequence: - -```html+jinja -Hello, {{ name | default("World") | titelize }}! -> Hello, WORLD! -``` - -## Tests - -Tests are conceptually similar to filters, but are used in the context of a boolean expression, for example as condition of an `if` tag. -Instead of a pipe they are applied using the keyword `is`. - -For example, the expression `name is defined` returns `true` if the variable `name` is defined. - -Test can accept arguments as well. If the test only takes one argument, the parentheses can be omitted: `9 is divisible by 3`. - -```html+jinja -{% if current_user is logged_in %} - Hello, {{ current_user.name }}! -{% else %} - Hey, stranger! -{% end %} -``` - -## Tags - -**Tags** control the logic of the template. They are enclosed in `{%` and `%}`. - -```html+jinja -{% if is_morning %} - Good Moring, {{ name }}! -{% else %} - Hello, {{ name }}! -{% end %} -``` - -The `for` tag allows looping over a collection. - -```html+jinja -{% for name in users %} - {{ user.name }} -{% endfor %} -``` - -Other templates can be included using the `include` tag: - -```html+jinja -{% include "header.html" %} - -
- Content -
- -{% include "header.html" %} -``` - -## Macros - -Macros can define re-usable template instructions that can be included in different places in the template. -They are similar to functions in other programming languages. - -When a macro is called, the output produced by the macro is assigned as the return value of the expression. - -```html+jinja -{# define macro: #} -{% macro say_hello(name) %}Hello, {{ name | default("stranger") }}!{% endmacro %} -{# invoke macro #} -{{ say_hello('Peter') }} -> Hello, Peter! - -{# print to a variable #} -{% set hello_paul = say_hello('Paul') %} -{{ hello_paul }} -> Hello, Paul! - -{# invoke with default value %} -{{ say_hello() }} -> Hello, stranger! -``` - -### Template Inheritance -Templates inheritance enables the use of `block` tags in parent templates that can be overwritten by child templates. This is useful for implementating layouts: - -```html+jinja -{# layout.html #} - -

{% block page_title %}{% endblock %}

- -
- {% block body %} - {# This block is typically overwritten by child templates #} - {% endblock %} -
- -{% block footer %} - {% include "footer.html" %} -{% endblock %} -``` - -```html+jinja -{# page.html #} -{% extends "layout.html" %} - -{% block page_title %}Blog Index{% endblock %} -{% block body %} - -{% endblock %} -``` diff --git a/lib/crinja/lib b/lib/crinja/lib deleted file mode 120000 index a96aa0e..0000000 --- a/lib/crinja/lib +++ /dev/null @@ -1 +0,0 @@ -.. \ No newline at end of file diff --git a/lib/crinja/playground/features.md b/lib/crinja/playground/features.md deleted file mode 100644 index a6af14e..0000000 --- a/lib/crinja/playground/features.md +++ /dev/null @@ -1,44 +0,0 @@ -# Custom features - -You can provide custom tags, filters, functions, operators and tests. Create an implementation using the macros `Crinja.filter`, `Crinja.function`, `Crinja.test`. They need to be passed a block which will be converted to a Proc. Optional arguments are a `Hash` or `NamedTuple` with default arguments and a name. If a name is provided, it will be added to the feature library defaults and available in every environment which uses the registered defaults. - -Example with macro `Crinja.filter`: - -```playground -require "./crinja" -env = Crinja.new - -myfilter = Crinja.filter({ attribute: nil }) do - "#{target} is #{arguments["attribute"]}!" -end - -env.filters["customfilter"] = myfilter - -template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }})) -puts template.render -``` - -Or you can define a class for more complex features: - -```playground -require "./crinja" -env = Crinja.new - -class Customfilter - include Crinja::Callable - - getter name = "customfilter" - - getter defaults = Crinja.variables({ - "attribute" => "great" - }) - - def call(arguments) - "#{arguments.target} is #{arguments["attribute"]}!" - end -end -env.filters << Customfilter.new - -template = env.from_string(%({{ "Hello World" | customfilter(attribute="super") }})) -puts template.render -``` diff --git a/lib/crinja/playground/objects.md b/lib/crinja/playground/objects.md deleted file mode 100644 index 7275fe9..0000000 --- a/lib/crinja/playground/objects.md +++ /dev/null @@ -1,100 +0,0 @@ -# Using custom objects - -To make custom objects usable in Crinja, they need to include `Crinja::Object`. - -> This module does not define any methods or requires a specific interface, it is just necessary to have a dedicated - type for this because Crystal cannot use `Object` as type of an instance variable (yet). - -Types *may* implement the following methods to make properties accessbile: - -* `#crinja_attribute(name : Crinja::Value) : Crinja::Value`: - Access an attribute (e.g. an instance property) of this type. -* `#crinja_call(name : String) : Crinja::Callable | Callable::Proc | Nil`: - Expose a callable as method of this type. - -`crinja_attribute` *must* return an `Crinja::Undefined` if there is no attribute or item of that name. `crinja_call` returns `nil` in that case. - -## Example - -```playground -require "./crinja" - -class User - include Crinja::Object - - property name : String - property dob : Time - - def initialize(@name, @dob) - end - - def age - (Time.now - @dob).years - end - - def crinja_attribute(attr : Crinja::Value) - value = case attr.to_string - when "name" - name - when "age" - age - else - Crinja::Undefined.new(attr.to_s) - end - - Crinja::Value.new(value) - end -end - -users = [ - User.new("john", Time.new(1982, 10, 10)), - User.new("bob", Time.new(1997, 9, 16)), - User.new("peter", Time.new(2002, 4, 1)) -] - -Crinja.render STDOUT, <<-'TEMPLATE', {users: users} - {%- for user in users -%} - * {{ user.name }} ({{ user.age }}) - {% endfor -%} - TEMPLATE -``` - -# Automatic exposure - -The method definition of `crinja_attribute` is often pretty boring as it usually just maps names of methods to the respective method calls. - -This can easily be generated automatically by the use of `Crinja::Object::Auto`. This module defines an automatically generated `crinja_attribute` method that exposes the types method as attributes. - -A method will be exposed if it is annotated with `@[Crystal::Attribute]`. - -A type annotated with `@[Crystal::Attributes]` exposes all methods defined on that type and matching the signature (no argument, no block). -This annotation take an optional `expose` argument which whitelist methods to expose. - -```playground -@[Crinja::Attributes(expose: [name, age])] -class User - include Crinja::Object::Auto - - property name : String - property dob : Time - - def initialize(@name, @dob) - end - - def age - (Time.now - @dob).years - end -end - -users = [ - User.new("john", Time.new(1982, 10, 10)), - User.new("bob", Time.new(1997, 9, 16)), - User.new("peter", Time.new(2002, 4, 1)) -] - -Crinja.render STDOUT, <<-'TEMPLATE', {users: users} - {%- for user in users -%} - * {{ user.name }} ({{ user.age }}) - {% endfor -%} - TEMPLATE -``` diff --git a/lib/crinja/scripts/coverage b/lib/crinja/scripts/coverage deleted file mode 100644 index 0a957ee..0000000 --- a/lib/crinja/scripts/coverage +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -e - -COVERAGE_OUT="~/cov-out2/" -CSS_FILE="${COVERAGE_OUT}/data/bcov.css" -BIN_FILE="build/all_specs" - -echo "Building all_specs.cr..." -crystal build -o "${BIN_FILE}" spec/all_specs.cr - -echo "Running kcov..." -kcov --exclude-path=/opt/crystal,/usr/include --exclude-pattern=_spec.cr "${COVERAGE_OUT}" "${BIN_FILE}" - -echo "Patching css file..." -sed -i -- 's/pre\.source \{ font-family: monospace; white-space: pre; \}/pre.source { font-family: monospace; white-space: normal; }/' "${CSS_FILE}" -echo 'source-line { display: block; white-space: pre; }' >> "${CSS_FILE}" diff --git a/lib/crinja/scripts/feature-comparison.sh b/lib/crinja/scripts/feature-comparison.sh deleted file mode 100755 index 843b6fd..0000000 --- a/lib/crinja/scripts/feature-comparison.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /usr/bin/env bash - -diff -B <( ./scripts/jinja/default_lib.py ) <( crystal ./src/cli.cr -- --library-defaults --only-names ) diff --git a/lib/crinja/scripts/generate-docs.sh b/lib/crinja/scripts/generate-docs.sh deleted file mode 100755 index 8edd1cd..0000000 --- a/lib/crinja/scripts/generate-docs.sh +++ /dev/null @@ -1,25 +0,0 @@ -#! /usr/bin/env bash - -set -e - -GENERATED_DOCS_DIR="./docs" - -echo -e "Building docs into ${GENERATED_DOCS_DIR}" -echo -e "Clearing ${GENERATED_DOCS_DIR} directory" -rm -rf "${GENERATED_DOCS_DIR}" - -echo -e "Running \`make docs\`..." -make docs - -echo -e "Copying README.md and TEMPLATE_SYNTAX.md" - -# "{{" and "{%"" need to be escaped, otherise Jekyll might interpret the expressions (on Github Pages) -ESCAPE_TEMPLATE='s/{{/{{"{{"}}/g; s/{\%/{{"{%"}}/g;' -sed "${ESCAPE_TEMPLATE}" README.md > "${GENERATED_DOCS_DIR}/README.md" -sed "${ESCAPE_TEMPLATE}" TEMPLATE_SYNTAX.md > "${GENERATED_DOCS_DIR}/TEMPLATE_SYNTAX.md" - -echo -e "Copying playground files" -mkdir -p "${GENERATED_DOCS_DIR}/playground" -for file in playground/*.md; do - sed "s/\`\`\`playground/\`\`\`crystal/g; ${ESCAPE_TEMPLATE}" "${file}" | cat <(echo -e "---\n---\n") - > "${GENERATED_DOCS_DIR}/${file}" -done diff --git a/lib/crinja/shard.yml b/lib/crinja/shard.yml deleted file mode 100644 index ff34c2f..0000000 --- a/lib/crinja/shard.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: crinja -version: 0.8.1 -license: Apache-2.0 -crystal: '>= 0.35.0' - -authors: - - Johannes Mรผller - -description: | - Implementation of Jinja2 runtime template language in Crystal - - https://straight-shoota.github.io/crinja/ - -targets: - crinja: - main: src/cli.cr - -development_dependencies: - diff: - github: MakeNowJust/crystal-diff - baked_file_system: - github: schovi/baked_file_system - version: ">= 0.10.0" diff --git a/lib/crinja/src/arguments.cr b/lib/crinja/src/arguments.cr deleted file mode 100644 index c0c4810..0000000 --- a/lib/crinja/src/arguments.cr +++ /dev/null @@ -1,112 +0,0 @@ -require "./crinja" -require "./error" - -# This holds arguments and environment information for function, filter, test and macro calls. -struct Crinja::Arguments - # Returns the variable arguments of the call. - getter varargs : Array(Value) - - # Returns the target of the call (if any). - getter target : Value? - - # Returns the keyword arguments of the call. - getter kwargs : Hash(String, Value) - - # Default argument values defined by the call implementation. - getter defaults : Variables - - # :nodoc: - setter defaults : Variables - - # Returns the crinja environment. - getter env : Crinja - - def initialize(@env, @varargs = [] of Value, @kwargs = Hash(String, Value).new, @defaults = Variables.new, @target = nil) - end - - def [](name : String) : Value - if kwargs.has_key?(name) - kwargs[name] - elsif index = defaults.index { |k, v| k == name } - if varargs.size > index - varargs[index] - else - default(name) - end - else - raise UnknownArgumentError.new(name, self) - end - end - - def fetch(name, default : Value) - fetch(name) { default } - end - - def fetch(name, default = nil) - fetch name, Value.new(default) - end - - def fetch(name) - value = self[name] - if value.raw.nil? - Value.new(yield) - else - value - end - end - - def target! - if (t = target).nil? - raise UndefinedError.new("undefined target") - else - t - end - end - - def to_h - [@kwargs.keys, @defaults.keys].flatten.uniq.each_with_object(Hash(String, Value).new) do |key, hash| - hash[key] = self[key] - end - end - - def is_set?(name : Symbol) - is_set?(name.to_s) - end - - def is_set?(name : String) - kwargs.has_key?(name) || (index = defaults.index { |k, v| k == name }) && varargs.size > index - end - - def default(name : Symbol) - default(name.to_s) - end - - def default(name : String) - Value.new defaults[name] - end - - class UnknownArgumentError < RuntimeError - def initialize(name, arguments) - super "unknown argument \"#{name}\" for #{arguments.inspect}" - end - end - - class Error < RuntimeError - property callee - property argument : String? - - def self.new(argument : Symbol | String, msg = nil, cause = nil) - new nil, msg, cause, argument: argument - end - - def initialize(@callee : Callable | Callable::Proc | Operator?, msg = nil, cause = nil, @argument = nil) - super msg, cause - end - - def message - arg = "" - arg = " argument: #{argument}" unless argument.nil? - "#{super} (called: #{callee}#{arg})" - end - end -end diff --git a/lib/crinja/src/cli.cr b/lib/crinja/src/cli.cr deleted file mode 100644 index 72c2a8d..0000000 --- a/lib/crinja/src/cli.cr +++ /dev/null @@ -1,112 +0,0 @@ -require "option_parser" -require "log" -require "./crinja" - -module Crinja::CLI - Log = ::Log.for(self) - - @@env = Crinja.new - @@loader = Crinja::Loader::FileSystemLoader.new("") - @@env.loader = @@loader - @@template_string : String? - - def self.env - @@env - end - - def self.loader - @@loader - end - - def self.display_help_and_exit(opts) - puts "crinja [options]