diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..6dc96d23 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +# Description + +A brief description of your changes. + +Closes #<issue>. + +- [ ] I have read and understood the [contributing guidelines](/inaka/elvis_core/blob/main/CONTRIBUTING.md) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea99f8b8..fb2a5728 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ jobs: runs-on: ${{matrix.os}} strategy: matrix: - otp_vsn: ['24', '25', '26', '27'] - rebar3_vsn: ['3.23'] - os: [ubuntu-22.04, windows-2022] + otp_vsn: ['25', '26', '27'] + rebar3_vsn: ['3.24'] + os: [ubuntu-24.04, windows-2022] steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 @@ -42,10 +42,8 @@ jobs: -otp-${{steps.setup-beam.outputs.otp-version}}\ -rebar3-${{steps.setup-beam.outputs.rebar3-version}}\ -hash-${{hashFiles('rebar.lock')}}" - - name: Compile - run: rebar3 compile - name: Format check - if: ${{ matrix.os == 'ubuntu-22.04' && matrix.otp_vsn == '26' }} + if: ${{ matrix.os == 'ubuntu-24.04' && matrix.otp_vsn == '26' }} run: rebar3 format --verify - name: Run test run: rebar3 test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9931855d..26abae7d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,14 +12,14 @@ on: jobs: md_and_yml: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 # uses .markdownlint.yml for configuration - name: markdownlint - uses: DavidAnson/markdownlint-cli2-action@v16 + uses: DavidAnson/markdownlint-cli2-action@v17 with: globs: | LICENSE diff --git a/.markdownlint.yml b/.markdownlint.yml index 90b8143a..9b250008 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -2,3 +2,5 @@ default: true MD013: line_length: 100 +MD024: + siblings_only: true diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..b57a0ff6 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at + or . +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d26ad3fd..b177d556 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,11 @@ The following is a checklist you can follow when implementing a new Elvis rule: and in the [`config/elvis.config`](https://github.com/inaka/elvis/blob/HEAD/config/elvis.config) file. +## Development + +CI for `elvis_core` is pretty simple, but in case you want to replicate it locally before +pushing most issues will be found via `rebar3 do format --verify, test`. + ## Questions? If you have any questions or general comments regarding how to contribute, please use our public diff --git a/MIGRATION.md b/MIGRATION.md index 14059752..a3187e5d 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,29 @@ since these are incremental. This file's format is influenced by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and [Make a README](https://www.makeareadme.com/). +## Going from `3.x` to `4.x` + +### Update + +- your `atom_naming_convention`'s options to be the ones defined at + +- your `macro_names`' options to be the ones defined at + +- your `operator_spaces` options to be the ones defined at + +- your `no_space` options to be the ones defined at + +- your `function_naming_convention` options to be the ones defined at + +- your `module_naming_convention` options to be the ones defined at + +- your `no_debug_call` options to be the ones defined at + +- your `no_common_caveats_call` options to be the ones defined at + + +On the other hand you may choose to not implement those changes and merely adapt your code. + ## Going from `0.x` to `1.x` ### Update diff --git a/RULES.md b/RULES.md index 3fa5e5ea..7221b5d0 100644 --- a/RULES.md +++ b/RULES.md @@ -62,6 +62,7 @@ identified with `(since ...)` for convenience purposes. - [Used Ignored Variable](doc_rules/elvis_style/used_ignored_variable.md) - [Variable Naming Convention](doc_rules/elvis_style/variable_naming_convention.md) - [Not Use List In Init Functions](doc_rules/elvis_style/no_init_lists.md) +- [Prefer Unquoted Atoms](doc_rules/elvis_text_style/prefer_unquoted_atoms.md) ## Project rules @@ -84,6 +85,7 @@ The six pre-defined rulesets are: - `elvis_config`, for elvis configuration files. - `erl_files`, for Erlang source files (pre-defined rule set). - `erl_files_strict`, for Erlang source files (all available rules). +- `gitignore`, for `.gitignore` files. - `hrl_files`, for Erlang header files. - `makefiles`, for Makefiles. - `rebar_config`, for rebar configuration files. @@ -201,6 +203,9 @@ line numbers will most surely not correspond with those in the source file. , filter => "elvis.config" , ruleset => elvis_config , rules => [] } + , #{ dirs => ["."] + , filter => ".gitignore" + , ruleset => gitignore } ]} , {verbose, true} ]}]. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..19cafbff --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,34 @@ +# Security policy + +Thanks for helping make `elvis_core` safer for everyone. + +Find below updated information on our security policy. + +## Security + +We take the security of this software seriously. + +We don't implement a bug bounty program or bounty rewards, but will work with +you to ensure that your findings get the appropriate handling. + +## Reporting Security Issues + +If you believe you have found a security vulnerability in this repository, +please report it to or . + +Please do not report security vulnerabilities through public channels, like +GitHub issues, discussions, or pull requests. + +Please include as much of the information listed below as you can to help us +better understand and resolve the issue: + +- the type of issue (e.g., buffer overflow, SQL injection, or cross-site + scripting) +- full paths of source file(s) related to the manifestation of the issue +- the location of the affected source code (tag/branch/commit or direct URL) +- any special configuration required to reproduce the issue +- step-by-step instructions to reproduce the issue +- proof-of-concept or exploit code (if possible) +- impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. diff --git a/config/test.config b/config/test.config index 591a39cd..ae547665 100644 --- a/config/test.config +++ b/config/test.config @@ -26,5 +26,10 @@ ruleset => rebar_config}, #{dirs => ["../../_build/test/lib/elvis_core/test/examples"], filter => "elvis.config", - ruleset => elvis_config}]}, + ruleset => elvis_config}, + #{dirs => + ["../../_build/test/lib/elvis_core/test/dirs/apps/app1", + "../../_build/test/lib/elvis_core/test/dirs/apps/app2"], + filter => ".gitignore", + ruleset => gitignore}]}, {output_format, plain}]}]. diff --git a/doc_rules/elvis_gitignore/forbidden_patterns.md b/doc_rules/elvis_gitignore/forbidden_patterns.md new file mode 100644 index 00000000..522d865a --- /dev/null +++ b/doc_rules/elvis_gitignore/forbidden_patterns.md @@ -0,0 +1,16 @@ +# `.gitignore` forbidden patterns + +(since [4.0.0](https://github.com/inaka/elvis_core/releases/tag/4.0.0)) + +Exclude, from the project's `.gitignore` file, the patterns identified by the rule. + +## Options + +- `regexes :: [string()]`. + - default: `["^rebar.lock$"]`. + +## Example + +```erlang +{elvis_gitignore, forbidden_patterns, #{}} +``` diff --git a/doc_rules/elvis_gitignore/required_patterns.md b/doc_rules/elvis_gitignore/required_patterns.md new file mode 100644 index 00000000..b0eb9d75 --- /dev/null +++ b/doc_rules/elvis_gitignore/required_patterns.md @@ -0,0 +1,22 @@ +# `.gitignore` required patterns + +(since [4.0.0](https://github.com/inaka/elvis_core/releases/tag/4.0.0)) + +Include, in the project's `.gitignore` file, the patterns identified by the rule. + +## Options + +- `regexes :: [string()]`. + - default: `["^.rebar3/$", + "^_build/$", + "^_checkouts/$", + "^doc/$", + "^/erl_crash.dump$", + "^/rebar3.crashdump$", + "^test/logs/$"]`. + +## Example + +```erlang +{elvis_gitignore, required_patterns, #{}} +``` diff --git a/doc_rules/elvis_style/always_shortcircuit.md b/doc_rules/elvis_style/always_shortcircuit.md index d2e268ea..e5c04c78 100644 --- a/doc_rules/elvis_style/always_shortcircuit.md +++ b/doc_rules/elvis_style/always_shortcircuit.md @@ -4,7 +4,7 @@ Use short-circuiting operators instead of the regular ones. -> Works on `.beam` file? Yes. +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/behaviour_spelling.md b/doc_rules/elvis_style/behaviour_spelling.md index 419473d1..02998b55 100644 --- a/doc_rules/elvis_style/behaviour_spelling.md +++ b/doc_rules/elvis_style/behaviour_spelling.md @@ -2,7 +2,7 @@ Use a consistent spelling for behaviour attributes in modules. -> Works on `.beam` file? Yes. +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/consistent_generic_type.md b/doc_rules/elvis_style/consistent_generic_type.md index bd9a2b60..e860ba5d 100644 --- a/doc_rules/elvis_style/consistent_generic_type.md +++ b/doc_rules/elvis_style/consistent_generic_type.md @@ -2,7 +2,7 @@ Use `term()` or `any()` consistently for types in specs. -> Works on `.beam` file? Yes. +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/export_used_types.md b/doc_rules/elvis_style/export_used_types.md index c5af361e..89979de3 100644 --- a/doc_rules/elvis_style/export_used_types.md +++ b/doc_rules/elvis_style/export_used_types.md @@ -6,7 +6,7 @@ Exporting a function without exporting the types it depends on can result in reimplementing the types in every module that uses those functions. To avoid this, when a function is exported, its types should be too. -> "Works on `.beam` file? Yes!" +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/function_naming_convention.md b/doc_rules/elvis_style/function_naming_convention.md index 6f152310..af91372a 100644 --- a/doc_rules/elvis_style/function_naming_convention.md +++ b/doc_rules/elvis_style/function_naming_convention.md @@ -7,7 +7,7 @@ All functions should be named according to the regular expression provided. ## Options - `regex :: string()`. - - default: `"^([a-z][a-z0-9]*_?)*(_SUITE)?$"`. + - default: `"^[a-z](_?[a-z0-9]+)*$"`. ## Example diff --git a/doc_rules/elvis_style/god_modules.md b/doc_rules/elvis_style/god_modules.md index 9cebabbb..9b73d7d1 100644 --- a/doc_rules/elvis_style/god_modules.md +++ b/doc_rules/elvis_style/god_modules.md @@ -6,7 +6,7 @@ There shouldn't be any modules that export a number of functions greater than th ## Options -- `limit :: non_neg_integer().` +- `limit :: non_neg_integer()` - default: `25` ## Example diff --git a/doc_rules/elvis_style/macro_names.md b/doc_rules/elvis_style/macro_names.md index 45500c21..ead21a51 100644 --- a/doc_rules/elvis_style/macro_names.md +++ b/doc_rules/elvis_style/macro_names.md @@ -7,7 +7,7 @@ Macro names should only contain upper-case alphanumeric characters. ## Options - `regex :: string()`. (since [1.0.0](https://github.com/inaka/elvis_core/releases/tag/1.0.0)) - - default: `"^([A-Z][A-Z_0-9]+)$"`. + - default: `"^[A-Z](_?[A-Z0-9]+)*$"`. ## Example diff --git a/doc_rules/elvis_style/module_naming_convention.md b/doc_rules/elvis_style/module_naming_convention.md index 303c3276..3949ed8f 100644 --- a/doc_rules/elvis_style/module_naming_convention.md +++ b/doc_rules/elvis_style/module_naming_convention.md @@ -7,7 +7,7 @@ All modules should be named according to the regular expression provided. ## Options - `regex :: string()`. - - default: `"^([a-z][a-z0-9]*_?)*(_SUITE)?$"`. + - default: `"^[a-z](_?[a-z0-9]+)*(_SUITE)?$"`. ## Example diff --git a/doc_rules/elvis_style/nesting_level.md b/doc_rules/elvis_style/nesting_level.md index ca93a52a..5520e9bf 100644 --- a/doc_rules/elvis_style/nesting_level.md +++ b/doc_rules/elvis_style/nesting_level.md @@ -6,7 +6,7 @@ There shouldn't be any nested expressions that exceed the specified level. ## Options -- `level :: pos_integer().` +- `level :: pos_integer()` - default: `4` (prior to [0.7.0](https://github.com/inaka/elvis_core/releases/tag/0.7.0) was `3`) ## Example diff --git a/doc_rules/elvis_style/no_behavior_info.md b/doc_rules/elvis_style/no_behavior_info.md index 3142344a..12777a67 100644 --- a/doc_rules/elvis_style/no_behavior_info.md +++ b/doc_rules/elvis_style/no_behavior_info.md @@ -2,7 +2,7 @@ Don't use attributes `behavior_info` or `behaviour_info`; use `callback` instead. -> Works on `.beam` file? No. +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/no_block_expressions.md b/doc_rules/elvis_style/no_block_expressions.md index 5919f0c1..79560061 100644 --- a/doc_rules/elvis_style/no_block_expressions.md +++ b/doc_rules/elvis_style/no_block_expressions.md @@ -4,7 +4,7 @@ Block expressions should be avoided. -> Works on `.beam` file? Yes. +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_style/no_call.md b/doc_rules/elvis_style/no_call.md index 14da14a6..9cf8622d 100644 --- a/doc_rules/elvis_style/no_call.md +++ b/doc_rules/elvis_style/no_call.md @@ -3,7 +3,7 @@ (since [0.4.0](https://github.com/inaka/elvis_core/releases/tag/0.4.0)) This rule raise a warning when certain functions are called. It is also used internally to implement -`no_debug_call` and `no_common_caveats` but, on its own, makes no checks on your code (the default +`no_debug_call` and `no_common_caveats_call` but, on its own, makes no checks on your code (the default `no_call_functions` list is empty). However, it is a convenient place to add your own list of calls to avoid (especially calls to third party libraries, where you can't just deprecate undesired functions). diff --git a/doc_rules/elvis_style/no_common_caveats_call.md b/doc_rules/elvis_style/no_common_caveats_call.md index 93e0f3cf..2645e653 100644 --- a/doc_rules/elvis_style/no_common_caveats_call.md +++ b/doc_rules/elvis_style/no_common_caveats_call.md @@ -5,7 +5,8 @@ The [Erlang Efficiency Guide](https://erlang.org/doc/efficiency_guide/commoncaveats.html) has a list of "Common Caveats" suggesting more efficient alternatives to several common functions. This rule provides warnings if you call "inefficient" functions with entirely equivalent (efficient) -alternatives. +alternatives. It also warns you about functions with a more explicit interface (e.g. `gen_server:call/3` +instead of `gen_server:call/2`) so you're sure to not have forgotten it. > Works on `.beam` file? Yes! @@ -17,6 +18,9 @@ alternatives. , {timer, send_interval, 2} , {timer, send_interval, 3} , {erlang, size, 1} + , {gen_statem, call, 2} + , {gen_server, call, 2} + , {gen_event, call, 3} ]`. **Notice**: this rule is not enforced by default. Check the diff --git a/doc_rules/elvis_style/no_debug_call.md b/doc_rules/elvis_style/no_debug_call.md index ced9fd1a..6d0e5d0c 100644 --- a/doc_rules/elvis_style/no_debug_call.md +++ b/doc_rules/elvis_style/no_debug_call.md @@ -7,7 +7,8 @@ Don't leave debugging function calls such as `io:format` or `ct:pal` in your sou ## Options - `debug_functions :: [{module(), function(), arity()} | {module(), function()}]`. - - default: `[{ct, pal}, {ct, print}, {io, format, 1}, {io, format, 2}, {erlang, display, 1}]` + - default: `[{ct, pal}, {ct, print}, {io, format, 1}, {io, format, 2}, {erlang, display, 1}, + {io, put_chars, 1}, {io, put_chars, 2}]` (`{erlang, display, 1}` is only included since [1.5.0](https://github.com/inaka/elvis_core/releases/tag/1.5.0)). diff --git a/doc_rules/elvis_style/no_dollar_space.md b/doc_rules/elvis_style/no_dollar_space.md index e4927b58..a7779cac 100644 --- a/doc_rules/elvis_style/no_dollar_space.md +++ b/doc_rules/elvis_style/no_dollar_space.md @@ -5,7 +5,7 @@ Do not use , use `$\s` instead. -> Works on `.beam` file? No. +> Works on `.beam` file? Not really! (it consumes results Ok, but these might be unexpected) ## Options diff --git a/doc_rules/elvis_style/no_space.md b/doc_rules/elvis_style/no_space.md index a98d4cf5..fd71d8de 100644 --- a/doc_rules/elvis_style/no_space.md +++ b/doc_rules/elvis_style/no_space.md @@ -9,8 +9,8 @@ can be any string. ## Options -- `rules :: [{right | left, string()}].` - - default: `[{right, "("}, {left, ")"}, {left, ","}]` +- `rules :: [{right | left, string()}]` + - default: `[{right, "("}, {left, ")"}, {left, ","}, {right, "#"}, {right, "?"}]` ## Example diff --git a/doc_rules/elvis_style/numeric_format.md b/doc_rules/elvis_style/numeric_format.md index 96198840..8c531947 100644 --- a/doc_rules/elvis_style/numeric_format.md +++ b/doc_rules/elvis_style/numeric_format.md @@ -8,7 +8,8 @@ By default, all numbers are valid. The goal of the rule is to allow developers t presence of numbers like `1_23_456_7890` and require them to be written either as `1_234_567_890` or `1234567890`, instead. -> Works on `.beam` file? No. (numbers there are just numbers; they can't be formatted) +> Works on `.beam` file? Not really! (it consumes results Ok, but these might be unexpected, since +numbers there are just numbers; they can't be formatted) ## Options diff --git a/doc_rules/elvis_style/operator_spaces.md b/doc_rules/elvis_style/operator_spaces.md index cdf68035..d7c92758 100644 --- a/doc_rules/elvis_style/operator_spaces.md +++ b/doc_rules/elvis_style/operator_spaces.md @@ -7,7 +7,7 @@ operator can be any string. ## Options -- `rules :: [{right | left, string()}].` +- `rules :: [{right | left, string()}]` - default: - before [1.5.0](https://github.com/inaka/elvis_core/releases/tag/1.5.0): `[{right, ","}, {right, "++"}, {left, "++"}]` @@ -21,7 +21,8 @@ operator can be any string. {right, "/="}, {left, "/="}, {right, "=/="}, {left, "=/="}, {right, "--"}, {left, "--"}, {right, "=>"}, {left, "=>"}, {right, ":="}, {left, ":="}, {right, "<-"}, {left, "<-"}, {right, "<="}, {left, "<="}, {right, "||"}, {left, "||"}, {right, "|"}, {left, "|"}, - {right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}] + {right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}, {left, "!"}, + {right, "!"}] ``` ## Example diff --git a/doc_rules/elvis_style/private_data_types.md b/doc_rules/elvis_style/private_data_types.md index 1db108aa..69ebb72f 100644 --- a/doc_rules/elvis_style/private_data_types.md +++ b/doc_rules/elvis_style/private_data_types.md @@ -8,7 +8,7 @@ for defining their own internal data types. If these are needed outside the modules, they should be made [opaque](https://www.erlang.org/doc/reference_manual/opaques.html). -> "Works on `.beam` file? Yes!" +> Works on `.beam` file? Yes! ## Options diff --git a/doc_rules/elvis_text_style/line_length.md b/doc_rules/elvis_text_style/line_length.md index ea62a3e3..ed3b210b 100644 --- a/doc_rules/elvis_text_style/line_length.md +++ b/doc_rules/elvis_text_style/line_length.md @@ -2,13 +2,13 @@ No line should be longer than a given limit. Comments can be skipped. -> Works on `.beam` file? No! +> Works on `.beam` file? No. ## Options -- `limit :: pos_integer().` +- `limit :: pos_integer()` - default: `100` -- `skip_comments :: false | any | whole_line.` +- `skip_comments :: false | any | whole_line` - default: `false`, means _emit a warning for every line that goes over `Limit`_ - `any` means _don't emit a warning if the part of the line that goes over `Limit` belongs to a comment_ diff --git a/doc_rules/elvis_text_style/no_trailing_whitespace.md b/doc_rules/elvis_text_style/no_trailing_whitespace.md index 247904fa..7be8a816 100644 --- a/doc_rules/elvis_text_style/no_trailing_whitespace.md +++ b/doc_rules/elvis_text_style/no_trailing_whitespace.md @@ -2,11 +2,12 @@ There should be no lines that end with whitespace characters. -> Works on `.beam` file? No. +> Works on `.beam` file? Not really! (it consumes results Ok, but these might be unexpected, since +there's no notion of "whitespace" in BEAM files) ## Options -- `ignore_empty_lines :: boolean().` +- `ignore_empty_lines :: boolean()` - default: `false` ## Example diff --git a/doc_rules/elvis_text_style/prefer_unquoted_atoms.md b/doc_rules/elvis_text_style/prefer_unquoted_atoms.md new file mode 100644 index 00000000..c4f9732f --- /dev/null +++ b/doc_rules/elvis_text_style/prefer_unquoted_atoms.md @@ -0,0 +1,15 @@ +# Prefer unquoted atoms + +Do not use quotes on atoms that don't need to be quoted. + +> Works on `.beam` file? Not really! (it consumes results Ok, but these might be unexpected) + +## Options + +- None. + +## Example + +```erlang +{elvis_style, prefer_unquoted_atoms, #{}} +``` diff --git a/rebar.config b/rebar.config index 764bbfeb..162f6f87 100644 --- a/rebar.config +++ b/rebar.config @@ -3,7 +3,7 @@ {erl_opts, [warn_unused_import, warn_export_vars, warnings_as_errors, verbose, report, debug_info]}. -{minimum_otp_vsn, "23"}. +{minimum_otp_vsn, "25"}. {profiles, [{test, @@ -21,7 +21,7 @@ %% == Dependencies and plugins == -{deps, [{zipper, "1.0.1"}, {katana_code, "~> 2.1.0"}]}. +{deps, [{zipper, "1.1.0"}, {katana_code, "~> 2.1.0"}]}. {project_plugins, [{rebar3_hank, "~> 1.4.0"}, diff --git a/rebar.lock b/rebar.lock index 09fb6d0e..427dc331 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,11 +1,11 @@ {"1.2.0", [{<<"katana_code">>,{pkg,<<"katana_code">>,<<"2.1.1">>},0}, - {<<"zipper">>,{pkg,<<"zipper">>,<<"1.0.1">>},0}]}. + {<<"zipper">>,{pkg,<<"zipper">>,<<"1.1.0">>},0}]}. [ {pkg_hash,[ {<<"katana_code">>, <<"9AC515E6B5AE4903CD7B6C9161ABFBA49B610B6F3E19E8F0542802A4316C2405">>}, - {<<"zipper">>, <<"3CCB4F14B97C06B2749B93D8B6C204A1ECB6FAFC6050CACC3B93B9870C05952A">>}]}, + {<<"zipper">>, <<"FA775C01BCD33225BE89AF320C10CE61D23943109F4D5CA718551D0C492D7E35">>}]}, {pkg_hash_ext,[ {<<"katana_code">>, <<"0680F33525B9A882E6F4D3022518B15C46F648BD7B0DBE86900980FE1C291404">>}, - {<<"zipper">>, <<"6A1FD3E1F0CC1D1DF5642C9A0CE2178036411B0A5C9642851D1DA276BD737C2D">>}]} + {<<"zipper">>, <<"4644C83AE83EF3C09860CB5ED597B0C759FBBCD64AD5B00A62F51AE48AEF7535">>}]} ]. diff --git a/src/elvis_core.app.src b/src/elvis_core.app.src index e515fb7c..d308e632 100644 --- a/src/elvis_core.app.src +++ b/src/elvis_core.app.src @@ -5,7 +5,6 @@ {vsn, git}, {applications, [kernel, stdlib, zipper, katana_code]}, {modules, [elvis_core, elvis_config, elvis_result, elvis_utils, elvis_style]}, - {registered, []}, {licenses, ["Apache-2.0"]}, {links, [{"GitHub", "https://github.com/inaka/elvis_core"}]}, {build_tools, ["rebar3"]}]}. diff --git a/src/elvis_gitignore.erl b/src/elvis_gitignore.erl new file mode 100644 index 00000000..5b3e3f2f --- /dev/null +++ b/src/elvis_gitignore.erl @@ -0,0 +1,107 @@ +-module(elvis_gitignore). + +-export([required_patterns/3, forbidden_patterns/3]). + +-define(REQUIRED_PATTERN, "Your .gitignore file should contain pattern '~s'."). +-define(FORBIDDEN_PATTERN, "Your .gitignore file should not contain pattern '~s'."). + +-hank([{unnecessary_function_arguments, + [{required_patterns, 3}, {forbidden_patterns, 3}]}]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Default values +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec default(Rule :: atom()) -> DefaultRuleConfig :: term(). +default(required_patterns) -> + #{regexes => + ["^.rebar3/$", + "^_build/$", + "^_checkouts/$", + "^doc/$", + "^/erl_crash.dump$", + "^/rebar3.crashdump$", + "^test/logs/$"]}; +default(forbidden_patterns) -> + #{regexes => ["^rebar.lock$"]}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Rules +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec required_patterns(elvis_config:config(), + elvis_file:file(), + elvis_style:empty_rule_config()) -> + [elvis_result:item()]. +required_patterns(_Config, #{path := Path}, RuleConfig) -> + Regexes = option(regexes, RuleConfig, required_patterns), + case file:read_file(Path) of + {ok, PatternsBin} -> + Patterns = elvis_utils:split_all_lines(PatternsBin), + check_patterns_in_lines(Patterns, Regexes, [], required); + {error, _} -> + [] + end. + +-spec forbidden_patterns(elvis_config:config(), + elvis_file:file(), + elvis_style:empty_rule_config()) -> + [elvis_result:item()]. +forbidden_patterns(_Config, #{path := Path}, RuleConfig) -> + Regexes = option(regexes, RuleConfig, forbidden_patterns), + case file:read_file(Path) of + {ok, PatternsBin} -> + Patterns = elvis_utils:split_all_lines(PatternsBin), + check_patterns_in_lines(Patterns, Regexes, [], forbidden); + {error, _} -> + [] + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Private +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% .gitignore +%% @private +check_patterns_in_lines(_Lines, [], Results, _Mode) -> + {ok, Results}; +check_patterns_in_lines(Lines, [Pattern | Rest], Results0, Mode) -> + ModeRespected = + case Mode of + required -> + lists:any(fun(Line) -> re:run(Line, Pattern) =/= nomatch end, Lines); + forbidden -> + lists:all(fun(Line) -> re:run(Line, Pattern) =:= nomatch end, Lines) + end, + Results = + case ModeRespected of + true -> + Results0; + false when Mode =:= required -> + [elvis_result:new(item, ?REQUIRED_PATTERN, [Pattern]) | Results0]; + false when Mode =:= forbidden -> + [elvis_result:new(item, ?FORBIDDEN_PATTERN, [Pattern]) | Results0] + end, + check_patterns_in_lines(Lines, Rest, Results, Mode). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Internal Function Definitions +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec option(OptionName, RuleConfig, Rule) -> OptionValue + when OptionName :: atom(), + RuleConfig :: elvis_config:config(), + Rule :: atom(), + OptionValue :: term(). +option(OptionName, RuleConfig, Rule) -> + maybe_default_option(maps:get(OptionName, RuleConfig, undefined), OptionName, Rule). + +-spec maybe_default_option(UserDefinedOptionValue, OptionName, Rule) -> OptionValue + when UserDefinedOptionValue :: undefined | term(), + OptionName :: atom(), + Rule :: atom(), + OptionValue :: term(). +maybe_default_option(undefined = _UserDefinedOptionValue, OptionName, Rule) -> + maps:get(OptionName, default(Rule)); +maybe_default_option(UserDefinedOptionValue, _OptionName, _Rule) -> + UserDefinedOptionValue. diff --git a/src/elvis_rulesets.erl b/src/elvis_rulesets.erl index 1c9ca63e..2241c266 100644 --- a/src/elvis_rulesets.erl +++ b/src/elvis_rulesets.erl @@ -11,6 +11,9 @@ set_rulesets(RuleSets) -> maps:to_list(RuleSets)). -spec rules(Group :: atom()) -> [elvis_core:rule()]. +rules(gitignore) -> + lists:map(fun({Mod, Rule}) -> {Mod, Rule, apply(Mod, default, [Rule])} end, + [{elvis_gitignore, Rule} || Rule <- [required_patterns, forbidden_patterns]]); rules(hrl_files) -> lists:map(fun({Mod, Rule}) -> {Mod, Rule, apply(Mod, default, [Rule])} end, [{elvis_text_style, Rule} || Rule <- [line_length, no_tabs, no_trailing_whitespace]] diff --git a/src/elvis_style.erl b/src/elvis_style.erl index 85e7f567..1c74d8f3 100644 --- a/src/elvis_style.erl +++ b/src/elvis_style.erl @@ -150,7 +150,7 @@ -spec default(Rule :: atom()) -> DefaultRuleConfig :: term(). default(macro_names) -> - #{regex => "^([A-Z][A-Z_0-9]+)$"}; + #{regex => "^[A-Z](_?[A-Z0-9]+)*$"}; default(operator_spaces) -> #{rules => [{right, "++"}, {left, "++"}, {right, "="}, {left, "="}, {right, "+"}, {left, "+"}, @@ -160,19 +160,27 @@ default(operator_spaces) -> {right, "/="}, {left, "/="}, {right, "=/="}, {left, "=/="}, {right, "--"}, {left, "--"}, {right, "=>"}, {left, "=>"}, {right, ":="}, {left, ":="}, {right, "<-"}, {left, "<-"}, {right, "<="}, {left, "<="}, {right, "||"}, {left, "||"}, {right, "|"}, {left, "|"}, - {right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}]}; + {right, "::"}, {left, "::"}, {right, "->"}, {left, "->"}, {right, ","}, {left, "!"}, + {right, "!"}]}; default(no_space) -> - #{rules => [{right, "("}, {left, ")"}, {left, ","}]}; + #{rules => + [{right, "("}, + {left, ")"}, + {left, ","}, + {left, ":"}, + {right, ":"}, + {right, "#"}, + {right, "?"}]}; default(nesting_level) -> #{level => 4}; default(god_modules) -> #{limit => 25}; default(function_naming_convention) -> - #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"}; + #{regex => "^[a-z](_?[a-z0-9]+)*$"}; default(variable_naming_convention) -> #{regex => "^_?([A-Z][0-9a-zA-Z]*)$"}; default(module_naming_convention) -> - #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"}; + #{regex => "^[a-z](_?[a-z0-9]+)*(_SUITE)?$"}; default(dont_repeat_yourself) -> #{min_complexity => 10}; default(max_module_length) -> @@ -191,7 +199,13 @@ default(no_call) -> #{no_call_functions => []}; default(no_debug_call) -> #{debug_functions => - [{ct, pal}, {ct, print}, {erlang, display, 1}, {io, format, 1}, {io, format, 2}]}; + [{ct, pal}, + {ct, print}, + {erlang, display, 1}, + {io, format, 1}, + {io, format, 2}, + {io, put_chars, 1}, + {io, put_chars, 2}]}; default(no_common_caveats_call) -> #{caveat_functions => [{timer, send_after, 2}, @@ -200,7 +214,7 @@ default(no_common_caveats_call) -> {timer, send_interval, 3}, {erlang, size, 1}]}; default(atom_naming_convention) -> - #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", enclosed_atoms => ".*"}; + #{regex => "^[a-z](_?[a-z0-9]+)*(_SUITE)?$", enclosed_atoms => ".*"}; %% Not restrictive. Those who want more restrictions can set it like "^[^_]*$" default(numeric_format) -> #{regex => ".*", diff --git a/src/elvis_text_style.erl b/src/elvis_text_style.erl index c560d166..eaeb44b1 100644 --- a/src/elvis_text_style.erl +++ b/src/elvis_text_style.erl @@ -1,19 +1,26 @@ -module(elvis_text_style). --export([default/1, line_length/3, no_tabs/3, no_trailing_whitespace/3]). +-export([default/1, line_length/3, no_tabs/3, no_trailing_whitespace/3, + prefer_unquoted_atoms/3]). -export_type([line_length_config/0, no_trailing_whitespace_config/0]). -define(LINE_LENGTH_MSG, "Line ~p is too long. It has ~p characters."). -define(NO_TABS_MSG, "Line ~p has a tab at column ~p."). -define(NO_TRAILING_WHITESPACE_MSG, "Line ~b has ~b trailing whitespace characters."). +-define(ATOM_PREFERRED_QUOTES_MSG, + "Atom ~p on line ~p is quoted " + "but quotes are not needed."). % These are part of a non-declared "behaviour" % The reason why we don't try to handle them with different arity is % that arguments are ignored in different positions (1 and 3) so that'd % probably be messier than to ignore the warning -hank([{unnecessary_function_arguments, - [{no_trailing_whitespace, 3}, {no_tabs, 3}, {line_length, 3}]}]). + [{no_trailing_whitespace, 3}, + {no_tabs, 3}, + {line_length, 3}, + {prefer_unquoted_atoms, 3}]}]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Default values @@ -71,6 +78,37 @@ no_trailing_whitespace(_Config, Target, RuleConfig) -> end, RuleConfig). +-spec prefer_unquoted_atoms(elvis_config:config(), + elvis_file:file(), + elvis_style:empty_rule_config()) -> + [elvis_result:item()]. +prefer_unquoted_atoms(_Config, Target, _RuleConfig) -> + {Content, #{encoding := _Encoding}} = elvis_file:src(Target), + Tree = ktn_code:parse_tree(Content), + AtomNodes = elvis_code:find(fun is_atom_node/1, Tree, #{traverse => all, mode => node}), + check_atom_quotes(AtomNodes, []). + +%% @private +check_atom_quotes([] = _AtomNodes, Acc) -> + Acc; +check_atom_quotes([AtomNode | RemainingAtomNodes], AccIn) -> + AtomName = ktn_code:attr(text, AtomNode), + + IsException = is_exception_prefer_quoted(AtomName), + + AccOut = + case unicode:characters_to_list(AtomName, unicode) of + [$' | _] when not IsException -> + Msg = ?ATOM_PREFERRED_QUOTES_MSG, + {Line, _} = ktn_code:attr(location, AtomNode), + Info = [AtomName, Line], + Result = elvis_result:new(item, Msg, Info, Line), + AccIn ++ [Result]; + _ -> + AccIn + end, + check_atom_quotes(RemainingAtomNodes, AccOut). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Private %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -164,6 +202,19 @@ check_no_trailing_whitespace(Line, Num, IgnoreEmptyLines) -> {ok, Result} end. +%% @private +is_atom_node(MaybeAtom) -> + ktn_code:type(MaybeAtom) =:= atom. + +%% @private +is_exception_prefer_quoted(Elem) -> + KeyWords = + ["'after'", "'and'", "'andalso'", "'band'", "'begin'", "'bnot'", "'bor'", "'bsl'", + "'bsr'", "'bxor'", "'case'", "'catch'", "'cond'", "'div'", "'end'", "'fun'", "'if'", + "'let'", "'not'", "'of'", "'or'", "'orelse'", "'receive'", "'rem'", "'try'", "'when'", + "'xor'", "'maybe'"], + lists:member(Elem, KeyWords). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% Internal Function Definitions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/test/dirs/apps/app1/.gitignore b/test/dirs/apps/app1/.gitignore new file mode 100644 index 00000000..670beeac --- /dev/null +++ b/test/dirs/apps/app1/.gitignore @@ -0,0 +1,10 @@ +.rebar3/ +_build/ + +_checkouts/ +extra +doc/ +/erl_crash.dump +/rebar3.crashdump +test/logs/ +rebar0.lock diff --git a/test/dirs/apps/app2/.gitignore b/test/dirs/apps/app2/.gitignore new file mode 100644 index 00000000..bc25abda --- /dev/null +++ b/test/dirs/apps/app2/.gitignore @@ -0,0 +1,9 @@ +.rebar3/ +_build/ +_checkouts/ +#doc/ + +/erl_crash.dump +/rebar3.crashdump +test/logs/ +rebar.lock diff --git a/test/examples/fail_atom_naming_convention.erl b/test/examples/fail_atom_naming_convention.erl index 7cf7f503..2d75f84f 100644 --- a/test/examples/fail_atom_naming_convention.erl +++ b/test/examples/fail_atom_naming_convention.erl @@ -12,4 +12,12 @@ for_test() -> '\'\'', '\'startswithbacktick', 'backtick\'inside', - 'backtick at the end\''. + 'backtick at the end\'', + non200_, + '__', % invalid, even when '_' is actually valid + two_underscores__together_are_not_valid, + '_something', % invalid because it starts with underscore + '42_invalid_because_it_starts_with_a_number', + '42invalid', %% even without underscores + weDontSupportCamelCaseHere, + not_even_in_a__SUITE. diff --git a/test/examples/fail_function_naming_convention.erl b/test/examples/fail_function_naming_convention.erl index 63a7f177..8517a653 100644 --- a/test/examples/fail_function_naming_convention.erl +++ b/test/examples/fail_function_naming_convention.erl @@ -15,7 +15,8 @@ bad_names_inside() -> 'Initial_cap'(should, fail), 'ok-for-lisp'(should, fail), 'no_predicates?'(should, fail), - user@location(should, fail). + user@location(should, fail), + before__after(should, fail). %% Private / hidden functions still checked @@ -36,3 +37,6 @@ camelCase(Should, Fail) -> user@location(Should, Fail) -> [Should, Fail]. + +before__after(Should, Fail) -> + [Should, Fail]. diff --git a/test/examples/fail_macro_names.erl b/test/examples/fail_macro_names.erl index 5216c235..7f5be284 100644 --- a/test/examples/fail_macro_names.erl +++ b/test/examples/fail_macro_names.erl @@ -11,6 +11,7 @@ -define(CALLING_SOMETHING, call(1)). -define( 'POTENTIAL_BAD-NAME' , nomegusta). -define('A,aZ', 2). +-define (BAD__NAME, "Stumble"). -export([ define/1, diff --git a/test/examples/fail_module_naming_1_convention_1.erl b/test/examples/fail_module_naming_1_convention_1.erl deleted file mode 100644 index ea2c6df3..00000000 --- a/test/examples/fail_module_naming_1_convention_1.erl +++ /dev/null @@ -1 +0,0 @@ --module(fail_module_naming_1_convention_1). diff --git a/test/examples/fail_module_naming_1_convention_1_.erl b/test/examples/fail_module_naming_1_convention_1_.erl new file mode 100644 index 00000000..1f76bf9e --- /dev/null +++ b/test/examples/fail_module_naming_1_convention_1_.erl @@ -0,0 +1 @@ +-module(fail_module_naming_1_convention_1_). diff --git a/test/examples/fail_no_call_classes.erl b/test/examples/fail_no_call_classes.erl index 1ec52b99..57b1cdda 100644 --- a/test/examples/fail_no_call_classes.erl +++ b/test/examples/fail_no_call_classes.erl @@ -13,4 +13,13 @@ fail() -> _ = erlang:size(<<"fail_size">>), _ = erlang:tuple_size({1,2,3}), - _ = erlang:byte_size(<<"ok_size">>). + _ = erlang:byte_size(<<"ok_size">>), + + _ = gen_server:call(self(), request), + _ = gen_server:call(self(), request, infinity), + + _ = gen_event:call(self(), self(), request), + _ = gen_event:call(self(), self(), request, infinity), + + _ = gen_statem:call(self(), request), + _ = gen_statem:call(self(), request, infinity). diff --git a/test/examples/fail_no_debug_call.erl b/test/examples/fail_no_debug_call.erl index fd888d37..7f0b6368 100644 --- a/test/examples/fail_no_debug_call.erl +++ b/test/examples/fail_no_debug_call.erl @@ -11,4 +11,5 @@ fail() -> ct:pal("Debug"), ct:pal("Debug ~s", ["Debug Info"]), ct:print("Debug"), - ct:print("Debug ~s", ["Debug Info"]). + ct:print("Debug ~s", ["Debug Info"]), + io:put_chars("Debug"). diff --git a/test/examples/fail_no_space.erl b/test/examples/fail_no_space.erl index 36b47413..d96f216d 100644 --- a/test/examples/fail_no_space.erl +++ b/test/examples/fail_no_space.erl @@ -17,9 +17,13 @@ , unicode_characters/0 , windows_newlines/0 , this/0 - , this/1 + , this/2 + , use_record/0 ]). +-define(MACRO, "Brujo loves these"). +-record(a, {one, two}). + %% No space before and after coma,on a comment. %% ( inside a comment %% ( @@ -107,8 +111,11 @@ windows_newlines() -> this() -> should_not_crash. --spec this(shouldnt_either) +-spec this(shouldnt_either, A::integer()) -> 32. -this(shouldnt_either) +this(shouldnt_either, _A) -> A = 1 - 2, A, $ . + +use_record() -> + # a{one = 1, two = ? MACRO}. diff --git a/test/examples/fail_operator_spaces.erl b/test/examples/fail_operator_spaces.erl index 70f8becf..e071ae3b 100644 --- a/test/examples/fail_operator_spaces.erl +++ b/test/examples/fail_operator_spaces.erl @@ -20,6 +20,7 @@ , this/1 , pass_more_operators/0 , fail_more_operators/0 + , fail_no_space_excl/0 ]). %% No space before and after coma,on a comment. @@ -150,3 +151,6 @@ fail_more_operators()-> <<<<$>>>||<<$>>><=<<$>>>>>, [X|D] }. + +fail_no_space_excl() -> + self()!'a'. diff --git a/test/examples/fail_quoted_atoms.erl b/test/examples/fail_quoted_atoms.erl new file mode 100644 index 00000000..aa56755a --- /dev/null +++ b/test/examples/fail_quoted_atoms.erl @@ -0,0 +1,11 @@ +-module(fail_quoted_atoms). + +-export([test/1, test/2]). + +-define(TEST(), test(1, default)). + +test(_Test) -> {ok, test}. + +test(_A, 'ugly_atom_name') -> 'why_use_quotes_here'; +test(_A, default) -> ?TEST(); +test(_A, _B) -> 'and'. diff --git a/test/examples/pass_atom_naming_convention.erl b/test/examples/pass_atom_naming_convention.erl index d650e0a3..9536ef86 100644 --- a/test/examples/pass_atom_naming_convention.erl +++ b/test/examples/pass_atom_naming_convention.erl @@ -5,4 +5,10 @@ for_test() -> this_is_an_ok_atom, 'and_so_is_this', - 'and_this_1_too'. + 'and_this_1_too', + non_200, + '_', % used by ets/mnesia/etc. + non200, % valid, even without underscores + valid_200even_if_numb3rs_appear_between_letters, + blahblah_SUITE, + x. diff --git a/test/examples/pass_unquoted_atoms.erl b/test/examples/pass_unquoted_atoms.erl new file mode 100644 index 00000000..48ddb761 --- /dev/null +++ b/test/examples/pass_unquoted_atoms.erl @@ -0,0 +1,11 @@ +-module(pass_unquoted_atoms). + +-export([test/1, test/2]). + +test(_Test) -> ok. + +test(_A, nice_atom_name) -> perfect_atomname; +test(_Reserved, _Words) -> ['after', 'and', 'andalso', 'band', 'begin', 'bnot', 'bor', 'bsl', 'bsr', 'bxor', 'case', + 'catch', 'cond', 'div', 'end', 'fun', 'if', 'let', 'not', 'of', 'or', 'orelse', 'receive', + 'rem', 'try', 'when', 'xor', 'maybe']. + diff --git a/test/gitignore_SUITE.erl b/test/gitignore_SUITE.erl new file mode 100644 index 00000000..9502e7aa --- /dev/null +++ b/test/gitignore_SUITE.erl @@ -0,0 +1,63 @@ +-module(gitignore_SUITE). + +-behaviour(ct_suite). + +-export([all/0, init_per_suite/1, end_per_suite/1]). +-export([verify_required_patterns/1, verify_forbidden_patterns/1]). + +-define(EXCLUDED_FUNS, [module_info, all, test, init_per_suite, end_per_suite]). + +-type config() :: [{atom(), term()}]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Common test +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec all() -> [atom()]. +all() -> + Exports = ?MODULE:module_info(exports), + [F || {F, _} <- Exports, not lists:member(F, ?EXCLUDED_FUNS)]. + +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + {ok, _} = application:ensure_all_started(elvis_core), + Config. + +-spec end_per_suite(config()) -> config(). +end_per_suite(Config) -> + ok = application:stop(elvis_core), + Config. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Test Cases +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec verify_required_patterns(config()) -> any(). +verify_required_patterns(_Config) -> + GitIgnoreConfig = elvis_test_utils:config(gitignore), + [SrcDirPass, SrcDirFail] = elvis_config:dirs(GitIgnoreConfig), + NoRuleConfig = #{}, + + PathPass = ".gitignore", + {ok, FilePass} = elvis_test_utils:find_file([SrcDirPass], PathPass), + {ok, []} = elvis_gitignore:required_patterns(GitIgnoreConfig, FilePass, NoRuleConfig), + + PathFail = ".gitignore", + {ok, FileFail} = elvis_test_utils:find_file([SrcDirFail], PathFail), + {ok, [Res]} = elvis_gitignore:required_patterns(GitIgnoreConfig, FileFail, NoRuleConfig), + #{info := ["^doc/$"]} = Res. + +-spec verify_forbidden_patterns(config()) -> any(). +verify_forbidden_patterns(_Config) -> + GitIgnoreConfig = elvis_test_utils:config(gitignore), + [SrcDirPass, SrcDirFail] = elvis_config:dirs(GitIgnoreConfig), + NoRuleConfig = #{}, + + PathPass = ".gitignore", + {ok, FilePass} = elvis_test_utils:find_file([SrcDirPass], PathPass), + {ok, []} = elvis_gitignore:forbidden_patterns(GitIgnoreConfig, FilePass, NoRuleConfig), + + PathFail = ".gitignore", + {ok, FileFail} = elvis_test_utils:find_file([SrcDirFail], PathFail), + {ok, [Res]} = elvis_gitignore:forbidden_patterns(GitIgnoreConfig, FileFail, NoRuleConfig), + #{info := ["^rebar.lock$"]} = Res. diff --git a/test/style_SUITE.erl b/test/style_SUITE.erl index fa965a21..e2c09b98 100644 --- a/test/style_SUITE.erl +++ b/test/style_SUITE.erl @@ -25,7 +25,8 @@ verify_always_shortcircuit/1, verify_consistent_generic_type/1, verify_no_types/1, verify_no_specs/1, verify_export_used_types/1, verify_consistent_variable_casing/1, verify_no_match_in_condition/1, verify_param_pattern_matching/1, - verify_private_data_types/1, verify_no_init_lists/1]). + verify_private_data_types/1, verify_unquoted_atoms/1, verify_no_init_lists/1]). + %% -elvis attribute -export([verify_elvis_attr_atom_naming_convention/1, verify_elvis_attr_numeric_format/1, verify_elvis_attr_dont_repeat_yourself/1, verify_elvis_attr_function_naming_convention/1, @@ -82,7 +83,7 @@ groups() -> verify_always_shortcircuit, verify_no_catch_expressions, verify_no_single_clause_case, verify_no_macros, verify_export_used_types, verify_max_anonymous_function_arity, verify_max_function_arity, verify_no_match_in_condition, verify_behaviour_spelling, - verify_param_pattern_matching, verify_private_data_types]}]. + verify_param_pattern_matching, verify_private_data_types, verify_unquoted_atoms]}]. -spec init_per_suite(config()) -> config(). init_per_suite(Config) -> @@ -117,8 +118,9 @@ verify_function_naming_convention(Config) -> % pass PathPass = "pass_function_naming_convention." ++ Ext, + #{regex := DefaultRegex} = elvis_style:default(function_naming_convention), - RuleConfig = #{regex => "^([a-z][a-z0-9]*_?)*$"}, + RuleConfig = #{regex => DefaultRegex}, [] = elvis_core_apply_rule(Config, elvis_style, @@ -126,8 +128,7 @@ verify_function_naming_convention(Config) -> RuleConfig, PathPass), - RuleConfig2 = - #{regex => "^([a-z][a-z0-9]*_?)*$", ignore => [fail_function_naming_convention]}, + RuleConfig2 = #{regex => DefaultRegex, ignore => [fail_function_naming_convention]}, [] = elvis_core_apply_rule(Config, elvis_style, @@ -142,7 +143,8 @@ verify_function_naming_convention(Config) -> _InitialCapError, _HyphenError, _PredError, - _EmailError] = + _EmailError, + _BeforeAfter] = elvis_core_apply_rule(Config, elvis_style, function_naming_convention, @@ -150,14 +152,14 @@ verify_function_naming_convention(Config) -> PathFail), RuleConfig3 = - #{regex => "^([a-z][a-z0-9]*_?)*$", + #{regex => DefaultRegex, ignore => [{fail_function_naming_convention, camelCase}, {fail_function_naming_convention, 'ALL_CAPS'}, {fail_function_naming_convention, 'Initial_cap'}, {fail_function_naming_convention, 'ok-for-lisp'}, {fail_function_naming_convention, 'no_predicates?'}]}, - [_EmailError2] = + [_EmailError2, _BeforeAfter2] = elvis_core_apply_rule(Config, elvis_style, function_naming_convention, @@ -168,7 +170,7 @@ verify_function_naming_convention(Config) -> PathIgnored = "fail_function_naming_convention_ignored_function." ++ Ext, RuleConfig4 = - #{regex => "^([a-z][a-z0-9]*_?)*$", + #{regex => DefaultRegex, ignore => [{fail_function_naming_convention, camelCase}, {fail_function_naming_convention, 'ALL_CAPS'}, @@ -321,7 +323,7 @@ verify_macro_names_rule(Config) -> Path = "fail_macro_names." ++ Ext, - [_, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, macro_names, #{}, Path), + [_, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, macro_names, #{}, Path), [_, _] = elvis_core_apply_rule(Config, @@ -344,7 +346,7 @@ verify_macro_names_rule(Config) -> #{regex => "^[A-Za-z_, \-]+$"}, Path), - [_, _, _, _, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, macro_names, @@ -469,7 +471,8 @@ verify_operator_spaces(Config) -> #{info := [left, "/=" | _]}, #{info := [right, "=/=" | _]}, #{info := [left, "=/=" | _]}, #{info := [right, "--" | _]}, #{info := [left, "--" | _]}, #{info := [right, "||" | _]}, #{info := [left, "||" | _]}, #{info := [right, "||" | _]}, #{info := [left, "||" | _]}, - #{info := [right, "|" | _]}, #{info := [left, "|" | _]}] = + #{info := [right, "|" | _]}, #{info := [left, "|" | _]}, #{info := [left, "!" | _]}, + #{info := [right, "!" | _]}] = elvis_core_apply_rule(Config, elvis_style, operator_spaces, DefaultOptions, Path). -spec verify_no_space(config()) -> any(). @@ -478,16 +481,19 @@ verify_no_space(Config) -> Path1 = "fail_no_space." ++ Ext, [#{info := [right, "(", 3]}, - #{info := [right, "(", 32]}, - #{info := [right, "(", 48]}, - #{info := [left, ")", 48]}, - #{info := [left, ")", 75]}, - #{info := [right, "(", 105]}, - #{info := [left, ")", 105]}] = + #{info := [right, "(", 36]}, + #{info := [right, "(", 52]}, + #{info := [left, ")", 52]}, + #{info := [left, ",", 76]}, + #{info := [left, ")", 79]}, + #{info := [right, "(", 109]}, + #{info := [left, ")", 109]}, + #{info := [right, "#", 121]}, + #{info := [right, "?", 121]}] = elvis_core_apply_rule(Config, elvis_style, no_space, - #{rules => [{right, "("}, {left, ")"}]}, + elvis_style:default(no_space), Path1). -spec verify_no_space_after_pound(config()) -> any(). @@ -659,7 +665,8 @@ verify_no_behavior_info(Config) -> verify_module_naming_convention(Config) -> Ext = proplists:get_value(test_file_ext, Config, "erl"), - RuleConfig = #{regex => "^([a-z][a-z0-9]*_?)*$", ignore => []}, + #{regex := DefaultRegex} = elvis_style:default(module_naming_convention), + RuleConfig = #{regex => DefaultRegex, ignore => []}, PathPass = "pass_module_naming_convention." ++ Ext, [] = @@ -669,7 +676,7 @@ verify_module_naming_convention(Config) -> RuleConfig, PathPass), - PathFail = "fail_module_naming_1_convention_1." ++ Ext, + PathFail = "fail_module_naming_1_convention_1_." ++ Ext, [_] = elvis_core_apply_rule(Config, elvis_style, @@ -677,7 +684,7 @@ verify_module_naming_convention(Config) -> RuleConfig, PathFail), - RuleConfigIgnore = RuleConfig#{ignore => [fail_module_naming_1_convention_1]}, + RuleConfigIgnore = RuleConfig#{ignore => [fail_module_naming_1_convention_1_]}, [] = elvis_core_apply_rule(Config, elvis_style, @@ -1143,14 +1150,12 @@ verify_no_debug_call(Config) -> PathFail = "fail_no_debug_call." ++ Ext, - PathFail = "fail_no_debug_call." ++ Ext, - _ = case Group of beam_files -> % io:format is preprocessed - [_, _, _, _, _] = + [_, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, no_debug_call, #{}, PathFail); erl_files -> - [_, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, no_debug_call, #{}, PathFail) end, @@ -1228,7 +1233,10 @@ verify_no_call_flavours(Config, {{erlang, size, 1}, 2}, {{timer, send_after}, 2}, {{timer, '_', '_'}, 4}, - {{'_', tuple_size, 1}, 1}], + {{'_', tuple_size, 1}, 1}, + {{gen_statem, call, 2}, 1}, + {{gen_server, call, 2}, 1}, + {{gen_event, call, 3}, 1}], lists:foreach(fun({FunSpec, ExpectedCount}) -> ThisRuleConfig = maps:from_list([{RuleConfigMapKey, [FunSpec]}]), @@ -1297,12 +1305,22 @@ verify_no_successive_maps(_Config) -> -endif. +-spec verify_unquoted_atoms(config()) -> any(). +verify_unquoted_atoms(Config) -> + PassPath = "pass_unquoted_atoms." ++ "erl", + [] = + elvis_core_apply_rule(Config, elvis_text_style, prefer_unquoted_atoms, #{}, PassPath), + + FailPath = "fail_quoted_atoms." ++ "erl", + [_, _] = + elvis_core_apply_rule(Config, elvis_text_style, prefer_unquoted_atoms, #{}, FailPath). + -spec verify_atom_naming_convention(config()) -> any(). verify_atom_naming_convention(Config) -> Group = proplists:get_value(group, Config, erl_files), Ext = proplists:get_value(test_file_ext, Config, "erl"), - BaseRegex = "^([a-z][a-z0-9_]+)$", + BaseRegex = "^[a-z](_?[a-z0-9]+)*(_SUITE)?$", % pass PassModule = pass_atom_naming_convention, @@ -1337,37 +1355,37 @@ verify_atom_naming_convention(Config) -> FailModule2 = fail_atom_naming_convention_exception_class, FailPath2 = atom_to_list(FailModule2) ++ "." ++ Ext, - [_, _, _, _, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => BaseRegex, enclosed_atoms => same}, FailPath), - [_, _, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z_]+)$", enclosed_atoms => same}, FailPath), - [_, _] = + [_, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z_' \\\\]+)$", enclosed_atoms => same}, FailPath), - [_, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z\-_]+)$", enclosed_atoms => same}, FailPath), - [_] = + [_, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => "^([a-zA-Z\-_' \\\\]+)$", enclosed_atoms => same}, FailPath), - [] = + [_] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, @@ -1380,26 +1398,26 @@ verify_atom_naming_convention(Config) -> #{regex => BaseRegex, ignore => [FailModule]}, FailPath), KeepRegex = "^([a-zA-Z0-9_]+)$", - [_, _, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => KeepRegex, enclosed_atoms => "^([a-z][a-z0-9A-Z_]*)$"}, FailPath), - [_, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => KeepRegex, enclosed_atoms => "^([a-z][a-z0-9A-Z_' \\\\]*)$"}, FailPath), - [_, _, _, _, _, _, _] = + [_, _, _, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, #{regex => KeepRegex, enclosed_atoms => "^([a-z][\-a-z0-9A-Z_]*)$"}, FailPath), - [_, _, _, _] = + [_, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, @@ -1414,7 +1432,7 @@ verify_atom_naming_convention(Config) -> FailPath2), _ = case Group of beam_files -> % 'or_THIS' getting stripped of enclosing ' - [_, _, _, _] = + [_, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention, @@ -1422,7 +1440,7 @@ verify_atom_naming_convention(Config) -> enclosed_atoms => "^([\\\\][\-a-z0-9A-Z_' \\\\]*)$"}, FailPath); erl_files -> - [_, _, _, _, _] = + [_, _, _, _, _, _, _, _, _] = elvis_core_apply_rule(Config, elvis_style, atom_naming_convention,