-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a check for snake_case function names
- Loading branch information
1 parent
f4562e2
commit af7e6f7
Showing
5 changed files
with
334 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
lib/elixir_analyzer/exercise_test/common_checks/function_names.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.FunctionNames do | ||
@moduledoc """ | ||
Reports the first function/macro/guard with a name that's not snake_case. | ||
Doesn't report more if there are more. | ||
A single comment should be enough for the student to know what to fix. | ||
Common check to be run on every single solution. | ||
""" | ||
|
||
alias ElixirAnalyzer.Constants | ||
alias ElixirAnalyzer.Comment | ||
|
||
@def_ops [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] | ||
|
||
@spec run(Macro.t()) :: [{:pass | :fail | :skip, %Comment{}}] | ||
def run(ast) do | ||
{_, names} = Macro.prewalk(ast, [], &traverse/2) | ||
|
||
if names == [] do | ||
[] | ||
else | ||
wrong_name = | ||
names | ||
|> Enum.reverse() | ||
|> hd() | ||
|> to_string() | ||
|
||
correct_name = to_snake_case(wrong_name) | ||
|
||
[ | ||
{:fail, | ||
%Comment{ | ||
type: :actionable, | ||
comment: Constants.solution_function_name_snake_case(), | ||
params: %{ | ||
expected: correct_name, | ||
actual: wrong_name | ||
} | ||
}} | ||
] | ||
end | ||
end | ||
|
||
defp traverse({op, _meta, [{name, _meta2, _arguments} | _]} = ast, names) when op in @def_ops do | ||
if snake_case?(name) do | ||
{ast, names} | ||
else | ||
{ast, [name | names]} | ||
end | ||
end | ||
|
||
defp traverse(ast, names) do | ||
{ast, names} | ||
end | ||
|
||
defp snake_case?(name) do | ||
# the code had to compile and pass all the tests to get to the analyzer | ||
# so we can assume the name is otherwise valid | ||
to_snake_case(name) == to_string(name) | ||
end | ||
|
||
defp to_snake_case(name) do | ||
# Macro.underscore is good enough because a module attribute name must be a valid Elixir identifier anyway | ||
Macro.underscore(to_string(name)) | ||
end | ||
end |
177 changes: 177 additions & 0 deletions
177
test/elixir_analyzer/exercise_test/common_checks/function_names_test.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
# credo:disable-for-this-file Credo.Check.Readability.FunctionNames | ||
|
||
defmodule ElixirAnalyzer.ExerciseTest.CommonChecks.FunctionNamesTest do | ||
use ExUnit.Case | ||
alias ElixirAnalyzer.Comment | ||
alias ElixirAnalyzer.Constants | ||
alias ElixirAnalyzer.ExerciseTest.CommonChecks.FunctionNames | ||
|
||
test "returns empty list if there are functions, macros, or guards" do | ||
code = | ||
quote do | ||
defmodule User do | ||
defstruct(:name, :email) | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [] | ||
end | ||
|
||
test "doesn't crash when a variable is named def" do | ||
code = | ||
quote do | ||
defmodule User do | ||
def name(user) do | ||
def = user.first_name | ||
defmacro = user.last_name | ||
def <> " " <> defmacro | ||
end | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [] | ||
end | ||
|
||
test "returns empty list if all names are correct" do | ||
code = | ||
quote do | ||
defmodule User do | ||
def first_name(user), do: user.first_name | ||
defp registered_in_last_quarter?(user), do: true | ||
defmacro validate_user!, do: true | ||
defmacrop do_validate_user, do: true | ||
defguard(is_user, do: true) | ||
defguardp(is_registered_user, do: true) | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [] | ||
end | ||
|
||
test "returns an actionable comment with params" do | ||
code = | ||
quote do | ||
defmodule User do | ||
def firstName(user), do: user.first_name | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [ | ||
{:fail, | ||
%Comment{ | ||
type: :actionable, | ||
comment: Constants.solution_function_name_snake_case(), | ||
params: %{ | ||
expected: "first_name", | ||
actual: "firstName" | ||
} | ||
}} | ||
] | ||
end | ||
|
||
test "works for def, defp, defmacro, defmacrop, defguard, and defguardp" do | ||
get_result = fn expected, actual -> | ||
[ | ||
{:fail, | ||
%Comment{ | ||
type: :actionable, | ||
comment: Constants.solution_function_name_snake_case(), | ||
params: %{ | ||
expected: expected, | ||
actual: actual | ||
} | ||
}} | ||
] | ||
end | ||
|
||
code = | ||
quote do | ||
defmodule User do | ||
defp registeredInLastQuarter?(user), do: true | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == | ||
get_result.("registered_in_last_quarter?", "registeredInLastQuarter?") | ||
|
||
code = | ||
quote do | ||
defmodule User do | ||
defmacro validateUser!, do: true | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == get_result.("validate_user!", "validateUser!") | ||
|
||
code = | ||
quote do | ||
defmodule User do | ||
defmacrop doValidateUser, do: true | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == get_result.("do_validate_user", "doValidateUser") | ||
|
||
code = | ||
quote do | ||
defmodule User do | ||
defguard(isUser, do: true) | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == get_result.("is_user", "isUser") | ||
|
||
code = | ||
quote do | ||
defmodule User do | ||
defguardp(isRegisteredUser, do: true) | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == get_result.("is_registered_user", "isRegisteredUser") | ||
end | ||
|
||
test "only reports the first wrong name" do | ||
code = | ||
quote do | ||
defmodule User do | ||
def first_name(user), do: getFirstName(user) | ||
defguard(isRegisteredUser(user), do: true) | ||
defp getFirstName(user), do: user.first_name | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [ | ||
{:fail, | ||
%Comment{ | ||
type: :actionable, | ||
comment: Constants.solution_function_name_snake_case(), | ||
params: %{ | ||
expected: "is_registered_user", | ||
actual: "isRegisteredUser" | ||
} | ||
}} | ||
] | ||
end | ||
|
||
test "can handle function heads" do | ||
code = | ||
quote do | ||
defmodule User do | ||
def getName(user, fallback \\ "Doe") | ||
end | ||
end | ||
|
||
assert FunctionNames.run(code) == [ | ||
{:fail, | ||
%Comment{ | ||
type: :actionable, | ||
comment: Constants.solution_function_name_snake_case(), | ||
params: %{ | ||
expected: "get_name", | ||
actual: "getName" | ||
} | ||
}} | ||
] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters