Packages

EditorConfig Core implementation for Elixir: parser, glob matcher, resolver, and CLI.

Current section

Files

Jump to
editorconfig_core lib editor_config resolver.ex
Raw

lib/editor_config/resolver.ex

defmodule EditorConfig.Resolver do
@moduledoc "Resolves EditorConfig properties for a file path."
alias EditorConfig.FileSystem
alias EditorConfig.FileSystem.Memory
alias EditorConfig.Glob
alias EditorConfig.Parser
@spec properties(Path.t(), keyword()) :: {:ok, map()} | {:error, term()}
def properties(path, opts \\ []) do
with {:ok, props, _sources} <- properties_with_sources(path, opts), do: {:ok, props}
end
@spec properties_with_sources(Path.t(), keyword()) :: {:ok, map(), list()} | {:error, term()}
def properties_with_sources(path, opts \\ []) do
fs = Keyword.get(opts, :fs, FileSystem)
config_name = Keyword.get(opts, :config_name, ".editorconfig")
path = normalize_path(path)
result =
path
|> candidate_config_paths(config_name)
|> Enum.reduce_while({[], false}, &collect_config(&1, &2, fs))
case result do
{:error, reason} -> {:error, reason}
{configs, _root_found?} -> apply_configs(configs, path, opts)
end
end
defp normalize_path(path) do
Path.expand(path)
end
defp candidate_config_paths(path, config_name) do
path
|> Path.dirname()
|> ancestors()
|> Enum.map(&Path.join(&1, config_name))
end
defp ancestors("/"), do: ["/"]
defp ancestors(dir), do: [dir | ancestors(Path.dirname(dir))]
defp collect_config(_candidate, {configs, true}, _fs), do: {:halt, {configs, true}}
defp collect_config(candidate, {configs, false}, fs) do
if exists?(fs, candidate) do
with {:ok, body} <- read(fs, candidate),
{:ok, file} <- Parser.parse(body) do
{:cont, {[{candidate, file} | configs], file.root?}}
else
{:error, reason} -> {:halt, {:error, reason}}
end
else
{:cont, {configs, false}}
end
end
defp apply_configs(configs, path, opts) do
{props, sources} =
Enum.reduce(configs, {%{}, []}, fn {config_path, file}, acc ->
config_dir = Path.dirname(config_path)
Enum.reduce(file.sections, acc, &apply_section(&1, &2, path, config_path, config_dir))
end)
{:ok, normalize_props(props, opts), sources}
end
defp apply_section(section, {props, sources}, path, config_path, config_dir) do
if Glob.match?(section.name, path, config_dir) do
{merge_props(props, section.properties), sources ++ [{config_path, section.name}]}
else
{props, sources}
end
end
defp merge_props(props, section_props) do
Enum.reduce(section_props, props, fn
{key, value}, acc -> Map.put(acc, key, value)
end)
end
defp normalize_props(props, opts) do
props
|> downcase_known_values()
|> apply_defaults(Keyword.get(opts, :version, "0.17.2"))
end
defp downcase_known_values(props) do
always_lowercase =
~w(charset end_of_line indent_style insert_final_newline trim_trailing_whitespace)
Map.new(props, fn {key, value} ->
cond do
key in always_lowercase ->
{key, String.downcase(value)}
key == "indent_size" and String.downcase(value) in ["tab", "unset"] ->
{key, String.downcase(value)}
true ->
{key, value}
end
end)
end
defp apply_defaults(props, version) do
if before_0_9_0?(version) do
props
else
props
|> default_indent_size()
|> default_tab_width()
end
end
defp default_indent_size(%{"indent_style" => "tab"} = props) do
Map.put_new(props, "indent_size", Map.get(props, "tab_width", "tab"))
end
defp default_indent_size(props), do: props
defp default_tab_width(%{"indent_size" => "tab"} = props), do: props
defp default_tab_width(%{"indent_size" => indent_size} = props) do
Map.put_new(props, "tab_width", indent_size)
end
defp default_tab_width(props), do: props
defp before_0_9_0?(version) do
Version.compare(version, "0.9.0") == :lt
rescue
Version.InvalidVersionError -> false
end
defp exists?(%Memory{} = fs, path), do: Memory.exists?(fs, path)
defp exists?(module, path), do: module.exists?(path)
defp read(%Memory{} = fs, path), do: Memory.read(fs, path)
defp read(module, path), do: module.read(path)
end