Skip to content

Commit

Permalink
Merge pull request #9 from inaka/jfacorro.2.console.tool
Browse files Browse the repository at this point in the history
[#2] Implemented elvis for console usage
  • Loading branch information
elbrujohalcon committed Jun 26, 2014
2 parents bab713b + af8baa5 commit 390375a
Show file tree
Hide file tree
Showing 15 changed files with 598 additions and 2 deletions.
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

0 comments on commit 390375a

Please sign in to comment.