Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2] Implemented elvis for console usage #9

Merged
merged 15 commits into from
Jun 26, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ ebin
*.beam
*.plt
erl_crash.dump
logs
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
PROJECT = elvis

DEPS = lager
DEPS = lager sync

dep_lager = https://github.com/basho/lager.git master
dep_sync = https://github.com/rustyio/sync.git master

include erlang.mk

ERLC_OPTS += +'{parse_transform, lager_transform}'
ERLC_OPTS += +warn_unused_vars +warn_export_all +warn_shadow_vars +warn_unused_import +warn_unused_function
ERLC_OPTS += +warn_bif_clash +warn_unused_record +warn_deprecated_function +warn_obsolete_guard +strict_validation
ERLC_OPTS += +warn_export_vars +warn_exported_vars +warn_missing_spec +warn_untyped_record +debug_info

# Commont Test Config

TEST_ERLC_OPTS += +'{parse_transform, lager_transform}'
CT_SUITES = elvis
CT_OPTS = -cover test/elvis.coverspec -erl_args -config config/test
88 changes: 87 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,95 @@
![](http://www.reactiongifs.com/wp-content/uploads/2013/01/elvis-dance.gif)

## elvis
# elvis

Erlang Style Reviewer

## Usage

After adding **elvis** as a dependency and setting up its [configuration](#configutation), you can run it
form an Erlang shell in the following two ways.

```erlang
elvis:rock().
%%+ # src/elvis.erl [OK]
%%+ # src/elvis_result.erl [OK]
%%+ # src/elvis_style.erl [OK]
%%+ # src/elvis_utils.erl [OK]
%%= ok
```

This will try to load the configuration for **elvis** specified in the [application's configuration](http://www.erlang.org/doc/man/config.html),
for this to be available, the application needs to be started. If no configuration is found `invalid_config` will be thrown.

To start the application in the shell enter the following command:

```erlang
application:start(elvis).
%%= ok
```

Another option for using **elvis** from the shell is explicitly providing a configuration as an argument to ``rock()``:

```erlang
Config = [{src_dirs, ["src"]}, {rules, []}],
elvis:rock(Config).
%%+ # src/elvis.erl [OK]
%%+ # src/elvis_result.erl [OK]
%%+ # src/elvis_style.erl [OK]
%%+ # src/elvis_utils.erl [OK]
%%= ok
```

`Config` should have a valid format, since this is a project under development the definition for *valid format* is still a
work in progress.

We have only presented results where all files were well-behaved (respect all the rules), so here's an example of how
it looks when files break some rules:

```
# ../../test/examples/fail_line_length.erl [FAIL]
- line_length
- Line 14 is too long: " io:format(\"This line is 81 characters long and should be detected, yeah!!!\").".
- Line 20 is too long: " io:format(\"This line is 90 characters long and should be detected!!!!!!!!!!!!!!!!!!\").".
# ../../test/examples/fail_no_tabs.erl [FAIL]
- no_tabs
- Line 6 has a tab at column 0.
- Line 15 has a tab at column 0.
# ../../test/examples/small.erl [OK]
```

## Configuration

To run **elvis** as described in the first option of the [Usage](#usage) section, you should include the following
environment values in your [configuration](http://www.erlang.org/doc/man/config.html) file:

```erlang
[
{elvis,
[
{src_dirs, ["src", "test"]},
{rules,
[
{elvis_style, line_length, [80]},
{elvis_style, no_tabs, []},
%% ..
]
}
]
}
]
```

The `src_dirs` entry is a list that indicates where **elvis** should look for the `*.erl` files that will be run through
each of the rules specified by the `rules` entry, which is list of rules with the following structure `{Module, Function, Args}`.

As you can see a rule is just a function, one that takes 3 arguments: **elvis**'s [configuration](#configuration), the path of the file and the
`Args` specified for the rule in the configuration. This means that you can define rules of your own as long as the functions
that implement them respect this arity.

There's currently no default configuration for **elvis**, but in the meantime you can take the one in `config/app.config`
as a starting point.

## References

Inspired on [HoundCI][houndci]
Expand Down
14 changes: 14 additions & 0 deletions config/app.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
elvis,
[
{src_dirs, ["src"]},
{rules,
[
{elvis_style, line_length, [80]},
{elvis_style, no_tabs, []}
]
}
]
}
].
14 changes: 14 additions & 0 deletions config/test.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
elvis,
[
{src_dirs, ["../../test/examples"]},
{rules,
[
{elvis_style, line_length, [80]},
{elvis_style, no_tabs, []}
]
}
]
}
].
56 changes: 56 additions & 0 deletions src/elvis.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-module(elvis).

%% Public API

-export([
rock/0,
rock/1
]).

-export_type([
config/0
]).

-type config() :: [{atom(), term()}].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Public API
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec rock() -> ok.
rock() ->
Config = application:get_all_env(elvis),
rock(Config).

-spec rock(config()) -> ok.
rock(Config) ->
case elvis_utils:validate_config(Config) of
valid ->
run(Config);
invalid ->
throw(invalid_config)
end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec run(config()) -> ok.
run(Config) ->
SrcDirs = elvis_utils:source_dirs(Config),
Pattern = "*.erl",
FilePaths = elvis_utils:find_files(SrcDirs, Pattern),
Results = lists:map(fun (Path) -> apply_rules(Config, Path) end, FilePaths),

elvis_result:print(Results).

apply_rules(Config, FilePath) ->
Rules = elvis_utils:rules(Config),
Acc = {[], Config, FilePath},
{RuleResults, _, _} = lists:foldl(fun apply_rule/2, Acc, Rules),
elvis_result:new(file, FilePath, RuleResults).

apply_rule({Module, Function, Args}, {Result, Config, FilePath}) ->
Results = Module:Function(Config, FilePath, Args),
RuleResult = elvis_result:new(rule, Function, Results),
{[RuleResult | Result], Config, FilePath}.
90 changes: 90 additions & 0 deletions src/elvis_result.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-module(elvis_result).

%% API
-export([
new/3,
print/1
]).

%% Types
-export_type([
item_result/0,
rule_result/0,
file_result/0
]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Records
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-record(item_result,
{
message = "" :: string(),
info = [] :: list()
}).

-record(rule_result,
{
name :: atom(),
results = [] :: [item_result()]
}).

-record(file_result,
{
path = "" :: string(),
rules = [] :: list()
}).

-type item_result() :: #item_result{}.
-type rule_result() :: #rule_result{}.
-type file_result() :: #file_result{}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Public
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec new(item | rule | file, any(), any()) ->
item_result() | rule_result() | file_result().
new(item, Msg, Info) ->
#item_result{message = Msg, info= Info};
new(rule, Name, Results) ->
#rule_result{name = Name, results = Results};
new(file, Path, Rules) ->
#file_result{path = Path, rules = Rules}.

-spec print(item_result() | rule_result() | [file_result()]) -> ok.
print([]) ->
ok;
print([Result | Results]) ->
print(Result),
print(Results);

print(#file_result{path = Path, rules = Rules}) ->
Status = case file_status(Rules) of
ok -> "OK";
fail -> "FAIL"
end,

io:format("# ~s [~s]~n", [Path, Status]),
print(Rules);

print(#rule_result{results = []}) ->
ok;
print(#rule_result{name = Name, results = Results}) ->
io:format(" - ~s~n", [atom_to_list(Name)]),
print(Results);

print(#item_result{message = Msg, info = Info}) ->
io:format(" - " ++ Msg ++ "~n", Info).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec file_status([rule_result()]) -> ok | fail.
file_status([]) ->
ok;
file_status([#rule_result{results = []} | Rules]) ->
file_status(Rules);
file_status(_Rules) ->
fail.
56 changes: 56 additions & 0 deletions src/elvis_style.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-module(elvis_style).

-export([
line_length/3,
no_tabs/3
]).

-define(LINE_LENGTH_MSG, "Line ~p is too long: ~p.").
-define(NO_TABS_MSG, "Line ~p has a tab at column ~p.").

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Rules
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% @doc Target can be either a filename or the
%% name of a module.
-spec line_length(elvis:config(), string(), [term()]) ->
[elvis_result:item_result()].
line_length(Config, Target, [Limit]) ->
{ok, Src} = elvis_utils:src(Config, Target),
elvis_utils:check_lines(Src, fun check_line_length/3, [Limit]).

-spec no_tabs(elvis:config(), string(), [term()]) ->
[elvis_result:item_result()].
no_tabs(Config, Target, []) ->
{ok, Src} = elvis_utils:src(Config, Target),
elvis_utils:check_lines(Src, fun check_no_tabs/3, []).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec check_line_length(binary(), integer(), [term()]) ->
no_result | {ok, elvis_result:item_result()}.
check_line_length(Line, Num, [Limit]) ->
case byte_size(Line) of
Large when Large > Limit ->
Msg = ?LINE_LENGTH_MSG,
Info = [Num, binary_to_list(Line)],
Result = elvis_result:new(item, Msg, Info),
{ok, Result};
_ ->
no_result
end.

-spec check_no_tabs(binary(), integer(), [term()]) ->
no_result | {ok, elvis_result:item_result()}.
check_no_tabs(Line, Num, _Args) ->
case binary:match(Line, <<"\t">>) of
nomatch ->
no_result;
{Index, _} ->
Msg = ?NO_TABS_MSG,
Result = elvis_result:new(item, Msg, [Num, Index]),
{ok, Result}
end.
Loading