Current section

Files

Jump to
elvis_core src elvis_text_style.erl
Raw

src/elvis_text_style.erl

-module(elvis_text_style).
-export([default/1, line_length/3, no_tabs/3, no_trailing_whitespace/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.").
% 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}]}]).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Default values
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec default(Rule :: atom()) -> DefaultRuleConfig :: term().
default(line_length) ->
#{limit => 100, skip_comments => false};
default(no_tabs) ->
#{};
default(no_trailing_whitespace) ->
#{ignore_empty_lines => false}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Rules
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-type line_length_config() ::
#{ignore => [elvis_style:ignorable()],
limit => integer(),
skip_comments => false | any | whole_line}.
%% @doc Target can be either a filename or the
%% name of a module.
-spec line_length(elvis_config:config(), elvis_file:file(), line_length_config()) ->
[elvis_result:item()].
line_length(_Config, Target, RuleConfig) ->
Limit = option(limit, RuleConfig, line_length),
SkipComments = option(skip_comments, RuleConfig, line_length),
{Src, #{encoding := Encoding}} = elvis_file:src(Target),
Args = [Limit, SkipComments, Encoding],
elvis_utils:check_lines(Src, fun check_line_length/3, Args).
-spec no_tabs(elvis_config:config(),
elvis_file:file(),
elvis_style:empty_rule_config()) ->
[elvis_result:item()].
no_tabs(_Config, Target, _RuleConfig) ->
{Src, _} = elvis_file:src(Target),
elvis_utils:check_lines(Src, fun check_no_tabs/2, []).
-type no_trailing_whitespace_config() ::
#{ignore => [module()], ignore_empty_lines => boolean()}.
-spec no_trailing_whitespace(Config :: elvis_config:config(),
Target :: elvis_file:file(),
no_trailing_whitespace_config()) ->
[elvis_result:item()].
no_trailing_whitespace(_Config, Target, RuleConfig) ->
{Src, _} = elvis_file:src(Target),
IgnoreEmptyLines = option(ignore_empty_lines, RuleConfig, no_trailing_whitespace),
elvis_utils:check_lines(Src,
fun(Src1, Fun, _Args) ->
check_no_trailing_whitespace(Src1, Fun, IgnoreEmptyLines)
end,
RuleConfig).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Line Length
-spec line_is_comment(binary()) -> boolean().
line_is_comment(Line) ->
case re:run(Line, "^[ \t]*%") of
nomatch ->
false;
{match, _} ->
true
end.
-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]) ->
case line_is_comment(Line) of
false ->
check_line_length(Line, Num, [Limit, Encoding]);
true ->
no_result
end;
check_line_length(Line, Num, [Limit, any, Encoding]) ->
LineWithoutComment = remove_comment(Line),
check_line_length(LineWithoutComment, Num, [Limit, Encoding]);
check_line_length(Line, Num, [Limit, _, Encoding]) ->
check_line_length(Line, Num, [Limit, Encoding]);
check_line_length(Line, Num, [Limit, Encoding]) ->
Chars = unicode:characters_to_list(Line, Encoding),
case length(Chars) of
Large when Large > Limit ->
Msg = ?LINE_LENGTH_MSG,
Info = [Num, Large],
Result = elvis_result:new(item, Msg, Info, Num),
{ok, Result};
_ ->
no_result
end.
%% 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, _} ->
Msg = ?NO_TABS_MSG,
Info = [Num, Index],
Result = elvis_result:new(item, Msg, Info, Num),
{ok, Result}
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: http://erlang.org/doc/man/re.html#sect17
true ->
"(?<=\\S)\\s+$";
false ->
"\\s+$"
end,
case re:run(Line, Regex) of
nomatch ->
no_result;
{match, [PosLen]} ->
Msg = ?NO_TRAILING_WHITESPACE_MSG,
Info = [Num, size(binary:part(Line, PosLen))],
Result = elvis_result:new(item, Msg, Info, Num),
{ok, Result}
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Internal Function Definitions
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec option(OptionName, RuleConfig, Rule) -> OptionValue
when OptionName :: atom(),
RuleConfig :: elvis_core:rule_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.