Current section

Files

Jump to
elvis_core src elvis_text_style.erl
Raw

src/elvis_text_style.erl

-module(elvis_text_style).
-behaviour(elvis_rule).
-export([default/1]).
-export([
line_length/2,
no_tabs/2,
no_trailing_whitespace/2,
no_redundant_blank_lines/2
]).
% The whole file is considered to have either callback functions or rules.
-ignore_xref(elvis_text_style).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Default values
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec default(RuleName :: atom()) -> elvis_rule:def().
default(line_length) ->
elvis_rule:defmap(#{
limit => 100,
skip_comments => false,
no_whitespace_after_limit => true
});
default(no_trailing_whitespace) ->
elvis_rule:defmap(#{
ignore_empty_lines => false
});
default(no_redundant_blank_lines) ->
elvis_rule:defmap(#{
max_lines => 1
});
default(_RuleName) ->
elvis_rule:defmap(#{}).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Rules
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% @doc File can be either a filename or the
%% name of a module.
-spec line_length(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()].
line_length(Rule, _ElvisConfig) ->
Limit = elvis_rule:option(limit, Rule),
SkipComments = elvis_rule:option(skip_comments, Rule),
NoWhitespace = elvis_rule:option(no_whitespace_after_limit, Rule),
{Src, File} = elvis_file:src(elvis_rule:file(Rule)),
Encoding = elvis_file:encoding(File),
Args = [Limit, SkipComments, Encoding, NoWhitespace],
check_lines(Src, fun check_line_length/3, Args).
-spec no_tabs(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()].
no_tabs(Rule, _ElvisConfig) ->
{Src, _} = elvis_file:src(elvis_rule:file(Rule)),
check_lines(Src, fun check_no_tabs/2, []).
-spec no_trailing_whitespace(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()].
no_trailing_whitespace(Rule, _ElvisConfig) ->
{Src, _} = elvis_file:src(elvis_rule:file(Rule)),
IgnoreEmptyLines = elvis_rule:option(ignore_empty_lines, Rule),
check_lines(
Src,
fun(Src1, Fun, _Args) ->
check_no_trailing_whitespace(Src1, Fun, IgnoreEmptyLines)
end,
elvis_rule:def(Rule)
).
-spec no_redundant_blank_lines(elvis_rule:t(), elvis_config:t()) -> [elvis_result:item()].
no_redundant_blank_lines(Rule, _ElvisConfig) ->
MaxLines = elvis_rule:option(max_lines, Rule) + 1,
{Src, _} = elvis_file:src(elvis_rule:file(Rule)),
Lines = elvis_utils:split_all_lines(Src, [trim]),
Result = redundant_blank_lines(Lines, {1, []}),
ResultFun =
fun
({Line, BlankLinesLength}) when BlankLinesLength >= MaxLines ->
{true,
elvis_result:new_item(
"there are too many (~p) blank lines; prefer respecting the configured "
"limit",
[BlankLinesLength],
#{line => Line, limit => BlankLinesLength}
)};
(_) ->
false
end,
lists:filtermap(ResultFun, Result).
redundant_blank_lines([], {_, Result}) ->
Result;
redundant_blank_lines(Lines, {CurrentLineNum, ResultList}) ->
BlankLines = lists:takewhile(fun(X) -> X =:= <<>> end, Lines),
BlankElements = length(BlankLines),
Index =
case BlankElements of
0 ->
1;
Num ->
Num
end,
NextLineNum = CurrentLineNum + Index,
case BlankElements of
0 ->
redundant_blank_lines(lists:nthtail(Index, Lines), {NextLineNum, ResultList});
_ ->
redundant_blank_lines(
lists:nthtail(Index, Lines),
{NextLineNum, [{CurrentLineNum, BlankElements} | ResultList]}
)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Line Length
-spec line_is_comment(binary()) -> boolean().
line_is_comment(Line) ->
re:run(Line, "^[ \t]*%") =/= nomatch.
-spec remove_comment(binary()) -> binary().
remove_comment(Line) ->
case re:run(Line, "([^%]+)", [{capture, first, binary}]) of
nomatch ->
Line;
{match, [Without]} ->
Without
end.
-spec check_line_length(binary(), integer(), [term()]) ->
no_result | {ok, elvis_result:item()}.
check_line_length(Line, Num, [Limit, whole_line, Encoding, NoWhitespace]) ->
case line_is_comment(Line) of
false ->
check_line_length(Line, Num, [Limit, Encoding, NoWhitespace]);
true ->
no_result
end;
check_line_length(Line, Num, [Limit, any, Encoding, NoWhitespace]) ->
LineWithoutComment = remove_comment(Line),
check_line_length(LineWithoutComment, Num, [Limit, Encoding, NoWhitespace]);
check_line_length(Line, Num, [Limit, _, Encoding, NoWhitespace]) ->
check_line_length(Line, Num, [Limit, Encoding, NoWhitespace]);
check_line_length(Line0, Num, [Limit, Encoding, NoWhitespace]) ->
Line = unicode:characters_to_binary(Line0, Encoding),
case string:length(Line) of
Len when Len =< Limit ->
no_result;
Len when NoWhitespace ->
{ok, line_length_res(Num, Len, Limit)};
Len ->
case binary:match(Line, <<"\s">>, [{scope, {Limit, Len - Limit}}]) of
{_, _} ->
{ok, line_length_res(Num, Len, Limit)};
nomatch ->
no_result
end
end.
line_length_res(Num, Len, Limit) ->
elvis_result:new_item(
"there are too many (~p) characters; prefer respecting the configured limit",
[Len],
#{line => Num, limit => Limit}
).
%% No Tabs
-spec check_no_tabs(binary(), integer()) -> no_result | {ok, elvis_result:item()}.
check_no_tabs(Line, Num) ->
case binary:match(Line, <<"\t">>) of
nomatch ->
no_result;
{Index, _} ->
{ok,
elvis_result:new_item(
"an unexpected tab character was found; prefer spaces",
[],
#{line => Num, column => Index}
)}
end.
%% No Trailing Whitespace
-spec check_no_trailing_whitespace(binary(), integer(), boolean()) ->
no_result | {ok, elvis_result:item()}.
check_no_trailing_whitespace(Line, Num, IgnoreEmptyLines) ->
Regex =
case IgnoreEmptyLines of
%% Lookbehind assertion: https://erlang.org/doc/man/re.html#sect17
true ->
"(?<=\\S)\\s+$";
false ->
"\\s+$"
end,
case re:run(Line, Regex) of
nomatch ->
no_result;
{match, _} ->
{ok,
elvis_result:new_item(
"unexpected trailing whitespace was found",
[],
#{line => Num}
)}
end.
%% @doc Takes a binary that holds source code and applies
%% Fun to each line. Fun takes 2 or 3 arguments (the line
%% as a binary, the line number and the optional supplied Args) and
%% returns 'no_result' or {'ok', Result}.
-spec check_lines(binary(), fun(), term()) -> [elvis_result:item()].
check_lines(Src, Fun, Args) ->
Lines = elvis_utils:split_all_lines(Src),
check_lines(Lines, Fun, Args, [], 1).
check_lines([], _Fun, _Args, Results, _Num) ->
lists:flatten(
lists:reverse(Results)
);
check_lines([Line | Lines], Fun, Args, Results, Num) ->
FunRes =
case is_function(Fun, 3) of
true ->
Fun(Line, Num, Args);
false ->
Fun(Line, Num)
end,
case FunRes of
{ok, Result} ->
check_lines(Lines, Fun, Args, [Result | Results], Num + 1);
no_result ->
check_lines(Lines, Fun, Args, Results, Num + 1)
end.