diff --git a/.check.exs b/.check.exs new file mode 100644 index 0000000..4fc0524 --- /dev/null +++ b/.check.exs @@ -0,0 +1,21 @@ +[ + ## all available options with default values (see `mix check` docs for description) + # parallel: true, + # skipped: true, + + ## list of tools (see `mix check` docs for defaults) + tools: [ + ## curated tools may be disabled (e.g. the check for compilation warnings) + # {:compiler, false}, + + ## ...or adjusted (e.g. use one-line formatter for more compact credo output) + # {:credo, "mix credo --format oneline"}, + + {:doctor, false} + + ## custom new tools may be added (mix tasks or arbitrary commands) + # {:my_mix_task, command: "mix release", env: %{"MIX_ENV" => "prod"}}, + # {:my_arbitrary_tool, command: "npm test", cd: "assets"}, + # {:my_arbitrary_script, command: ["my_script", "argument with spaces"], cd: "scripts"} + ] +] diff --git a/.credo.exs b/.credo.exs new file mode 100644 index 0000000..5dd57a2 --- /dev/null +++ b/.credo.exs @@ -0,0 +1,184 @@ +# This file contains the configuration for Credo and you are probably reading +# this after creating it with `mix credo.gen.config`. +# +# If you find anything wrong or unclear in this file, please report an +# issue on GitHub: https://github.com/rrrene/credo/issues +# +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], + excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + }, + # + # Load and configure plugins here: + # + plugins: [], + # + # If you create your own checks, you must specify the source files for + # them here, so they can be loaded by Credo before running the analysis. + # + requires: [], + # + # If you want to enforce a style guide and need a more traditional linting + # experience, you can change `strict` to `true` below: + # + strict: false, + # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # + # If you want to use uncolored output by default, you can change `color` + # to `false` below: + # + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, false}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 2]}, + {Credo.Check.Design.TagFIXME, []}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, false}, + {Credo.Check.Refactor.FunctionArity, [max_arity: 13]}, + {Credo.Check.Refactor.LongQuoteBlocks, false}, + {Credo.Check.Refactor.MapInto, false}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, [max_nesting: 10]}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.LazyLogging, false}, + {Credo.Check.Warning.MixEnv, false}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []}, + + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just replace `false` with `[]`) + # + {Credo.Check.Readability.StrictModuleLayout, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Consistency.UnusedVariableNames, false}, + {Credo.Check.Design.DuplicatedCode, false}, + {Credo.Check.Readability.AliasAs, false}, + {Credo.Check.Readability.MultiAlias, false}, + {Credo.Check.Readability.Specs, false}, + {Credo.Check.Readability.SinglePipe, false}, + {Credo.Check.Readability.WithCustomTaggedTuple, false}, + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.DoubleBooleanNegation, false}, + {Credo.Check.Refactor.ModuleDependencies, false}, + {Credo.Check.Refactor.NegatedIsNil, false}, + {Credo.Check.Refactor.PipeChainStart, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.LeakyEnvironment, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Warning.UnsafeToAtom, false} + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + ] +} diff --git a/.formatter.exs b/.formatter.exs index d2cda26..482ccb3 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,9 @@ -# Used by "mix format" +spark_locals_without_parens = [attributes: 1, decrypt_by_default: 1, on_decrypt: 1, vault: 1] + [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: spark_locals_without_parens, + export: [ + locals_without_parens: spark_locals_without_parens + ] ] diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7aa6f74 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at zach@zachdaniel.dev. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..5ec4c54 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing to Igniter + +- We have a zero tolerance policy for failure to abide by our code of conduct. It is very standard, but please make sure + you have read it. +- Issues may be opened to propose new ideas, to ask questions, or to file bugs. +- Before working on a feature, please talk to the core team/the rest of the community via a proposal. We are + building something that needs to be cohesive and well thought out across all use cases. Our top priority is + supporting real life use cases like yours, but we have to make sure that we do that in a sustainable way. The + best compromise there is to make sure that discussions are centered around the _use case_ for a feature, rather + than the propsed feature itself. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..81d1992 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,27 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: bug, needs review +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +A minimal set of resource definitions and calls that can reproduce the bug. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +\*\* Runtime + +- Elixir version +- Erlang version +- OS +- Igniter version +- any related extension versions + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/proposal.md b/.github/ISSUE_TEMPLATE/proposal.md new file mode 100644 index 0000000..f347dcb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/proposal.md @@ -0,0 +1,36 @@ +--- +name: Proposal +about: Suggest an idea for this project +title: '' +labels: enhancement, needs review +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Express the feature either with a change to resource syntax, or with a change to the resource interface** + +For example + +```elixir + attributes do + attribute :foo, :integer, bar: 10 # <- Adding `bar` here would cause + end +``` + +Or + +```elixir + Api.read(:resource, bar: 10) # <- Adding `bar` here would cause +``` + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8c13744 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,4 @@ +### Contributor checklist + +- [ ] Bug fixes include regression tests +- [ ] Features include unit/acceptance tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6977f1c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: mix + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml new file mode 100644 index 0000000..0f6cfe0 --- /dev/null +++ b/.github/workflows/elixir.yml @@ -0,0 +1,14 @@ +name: CI +on: + push: + tags: + - "v*" + branches: [main] + pull_request: + branches: [main] + workflow_call: +jobs: + ash-ci: + uses: ash-project/ash/.github/workflows/ash-ci.yml@main + secrets: + HEX_API_KEY: ${{ secrets.HEX_API_KEY }} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..5e71066 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +erlang 26.0.2 +elixir 1.16.2 diff --git a/FUNDING.yml b/FUNDING.yml new file mode 100644 index 0000000..b98cb0d --- /dev/null +++ b/FUNDING.yml @@ -0,0 +1 @@ +github: zachdaniel \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4eb51a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Zachary Scott Daniel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..838c1d3 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,17 @@ +import Config + +if Mix.env() == :dev do + config :git_ops, + mix_project: Igniter.MixProject, + changelog_file: "CHANGELOG.md", + repository_url: "https://github.com/ash-project/igniter", + # Instructs the tool to manage your mix version in your `mix.exs` file + # See below for more information + manage_mix_version?: true, + # Instructs the tool to manage the version in your README.md + # Pass in `true` to use `"README.md"` or a string to customize + manage_readme_version: [ + "README.md" + ], + version_tag_prefix: "v" +end diff --git a/lib/application.ex b/lib/application.ex new file mode 100644 index 0000000..394bc14 --- /dev/null +++ b/lib/application.ex @@ -0,0 +1,7 @@ +defmodule Igniter.Application do + @moduledoc "Codemods and tools for working with Application modules." + + def app_name do + Mix.Project.config()[:app] + end +end diff --git a/lib/args.ex b/lib/args.ex index f032e1d..d3132cd 100644 --- a/lib/args.ex +++ b/lib/args.ex @@ -1,4 +1,5 @@ defmodule Igniter.Args do + @moduledoc "Tools for validating and parsing command line arguments to tasks." def validate_nth_present_and_underscored(igniter, argv, n, option, message) do value = Enum.at(argv, n) diff --git a/lib/common.ex b/lib/common.ex index f8a7283..e2206ba 100644 --- a/lib/common.ex +++ b/lib/common.ex @@ -1,6 +1,6 @@ defmodule Igniter.Common do - @doc """ - Common utilities for working with igniter, primarily with zippers. + @moduledoc """ + Common utilities for working with igniter, primarily with `Sourceror.Zipper`. """ alias Sourceror.Zipper @@ -96,7 +96,7 @@ defmodule Igniter.Common do defp do_put_in_keyword(zipper, [key | rest], value, updater) do if node_matches_pattern?(zipper, value when is_list(value)) do case move_to_list_item(zipper, fn item -> - if is_tuple?(item) do + if tuple?(item) do first_elem = tuple_elem(item, 0) first_elem && node_matches_pattern?(first_elem, ^key) end @@ -127,7 +127,7 @@ defmodule Igniter.Common do def set_keyword_key(zipper, key, value, updater) do if node_matches_pattern?(zipper, value when is_list(value)) do case move_to_list_item(zipper, fn item -> - if is_tuple?(item) do + if tuple?(item) do first_elem = tuple_elem(item, 0) first_elem && node_matches_pattern?(first_elem, ^key) end @@ -176,7 +176,7 @@ defmodule Igniter.Common do zipper |> Zipper.down() |> move_to_list_item(fn item -> - if is_tuple?(item) do + if tuple?(item) do first_elem = tuple_elem(item, 0) first_elem && node_matches_pattern?(first_elem, ^key) end @@ -222,7 +222,7 @@ defmodule Igniter.Common do zipper |> Zipper.down() |> move_to_list_item(fn item -> - if is_tuple?(item) do + if tuple?(item) do first_elem = tuple_elem(item, 0) first_elem && node_matches_pattern?(first_elem, ^key) end @@ -284,11 +284,11 @@ defmodule Igniter.Common do zipper |> maybe_move_to_block() |> move_right(fn zipper -> - is_function_call(zipper, name, arity) && predicate.(zipper) + function_call?(zipper, name, arity) && predicate.(zipper) end) end - def is_function_call(zipper, name, arity) do + def function_call?(zipper, name, arity) do zipper |> maybe_move_to_block() |> Zipper.subtree() @@ -312,7 +312,7 @@ defmodule Igniter.Common do end def update_nth_argument(zipper, index, func) do - if is_pipeline?(zipper) do + if pipeline?(zipper) do if index == 0 do zipper |> Zipper.down() @@ -373,7 +373,7 @@ defmodule Igniter.Common do end def argument_matches_predicate?(zipper, index, func) do - if is_pipeline?(zipper) do + if pipeline?(zipper) do if index == 0 do zipper |> Zipper.down() @@ -438,7 +438,7 @@ defmodule Igniter.Common do end end - def is_pipeline?(zipper) do + def pipeline?(zipper) do zipper |> Zipper.subtree() |> Zipper.root() @@ -448,6 +448,7 @@ defmodule Igniter.Common do end end + # sobelow_skip ["DOS.StringToAtom"] def move_to_module_using(zipper, module) do split_module = module @@ -660,7 +661,7 @@ defmodule Igniter.Common do end end - def is_tuple?(item) do + def tuple?(item) do item |> Zipper.subtree() |> Zipper.root() diff --git a/lib/config.ex b/lib/config.ex index e2729af..ef0f1f2 100644 --- a/lib/config.ex +++ b/lib/config.ex @@ -1,4 +1,5 @@ defmodule Igniter.Config do + @moduledoc "Codemods and utilities for configuring Elixir applications." require Igniter.Common alias Igniter.Common alias Sourceror.Zipper diff --git a/lib/deps.ex b/lib/deps.ex index ee91de3..7c8e0d4 100644 --- a/lib/deps.ex +++ b/lib/deps.ex @@ -1,36 +1,8 @@ defmodule Igniter.Deps do + @moduledoc "Codemods and utilities for managing dependencies declared in mix.exs" require Igniter.Common - alias Sourceror.Zipper alias Igniter.Common - - def get_dependency_declaration(igniter, name) do - zipper = - igniter - |> Igniter.include_existing_elixir_file("mix.exs") - |> Map.get(:rewrite) - |> Rewrite.source!("mix.exs") - |> Rewrite.Source.get(:quoted) - |> Zipper.zip() - - with {:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project), - {:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0), - true <- Common.node_matches_pattern?(zipper, value when is_list(value)), - {:ok, current_declaration} <- - Common.move_to_list_item(zipper, fn item -> - if Common.is_tuple?(item) do - first_elem = Common.tuple_elem(item, 0) - first_elem && Common.node_matches_pattern?(first_elem, ^name) - end - end) do - current_declaration - |> Zipper.subtree() - |> Zipper.node() - |> Sourceror.to_string() - else - _ -> - nil - end - end + alias Sourceror.Zipper def add_dependency(igniter, name, version) do case get_dependency_declaration(igniter, name) do @@ -60,6 +32,35 @@ defmodule Igniter.Deps do end end + def get_dependency_declaration(igniter, name) do + zipper = + igniter + |> Igniter.include_existing_elixir_file("mix.exs") + |> Map.get(:rewrite) + |> Rewrite.source!("mix.exs") + |> Rewrite.Source.get(:quoted) + |> Zipper.zip() + + with {:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project), + {:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0), + true <- Common.node_matches_pattern?(zipper, value when is_list(value)), + {:ok, current_declaration} <- + Common.move_to_list_item(zipper, fn item -> + if Common.tuple?(item) do + first_elem = Common.tuple_elem(item, 0) + first_elem && Common.node_matches_pattern?(first_elem, ^name) + end + end) do + current_declaration + |> Zipper.subtree() + |> Zipper.node() + |> Sourceror.to_string() + else + _ -> + nil + end + end + defp remove_dependency(igniter, name) do igniter |> Igniter.update_file("mix.exs", fn source -> @@ -72,7 +73,7 @@ defmodule Igniter.Deps do true <- Common.node_matches_pattern?(zipper, value when is_list(value)), current_declaration_index when not is_nil(current_declaration_index) <- Common.find_list_item_index(zipper, fn item -> - if Common.is_tuple?(item) do + if Common.tuple?(item) do first_elem = Common.tuple_elem(item, 0) first_elem && Common.node_matches_pattern?(first_elem, ^name) end diff --git a/lib/formatter.ex b/lib/formatter.ex index 21eeead..baf0789 100644 --- a/lib/formatter.ex +++ b/lib/formatter.ex @@ -1,4 +1,5 @@ defmodule Igniter.Formatter do + @moduledoc "Codemods and utilities for interacting with `.formatter.exs` files" alias Igniter.Common alias Sourceror.Zipper diff --git a/lib/igniter.ex b/lib/igniter.ex index d547a51..7fd501e 100644 --- a/lib/igniter.ex +++ b/lib/igniter.ex @@ -8,10 +8,10 @@ defmodule Igniter do @type t :: %__MODULE__{ rewrite: Rewrite.t(), issues: [String.t()], - tasks: [{String.t() | list(STring.t())}] + tasks: [{String.t() | list(String.t())}] } - def new() do + def new do %__MODULE__{rewrite: Rewrite.new()} end @@ -121,6 +121,185 @@ defmodule Igniter do |> format(path) end + def do_or_dry_run(igniter, argv, opts \\ []) do + igniter = %{igniter | issues: Enum.uniq(igniter.issues)} + title = opts[:title] || "Igniter" + + sources = + igniter.rewrite + |> Rewrite.sources() + + issues = + Enum.flat_map(sources, fn source -> + changed_issues = + if Rewrite.Source.file_changed?(source) do + ["File has been changed since it was originally read."] + else + [] + end + + issues = Enum.uniq(changed_issues ++ Rewrite.Source.issues(source)) + + case issues do + [] -> [] + issues -> [{source, issues}] + end + end) + + case issues do + [_ | _] -> + explain_issues(issues) + :issues + + [] -> + case igniter do + %{issues: []} -> + result_of_dry_run = + sources + |> Enum.filter(fn source -> + Rewrite.Source.updated?(source) + end) + |> case do + [] -> + unless opts[:quiet_on_no_changes?] do + Mix.shell().info("\n#{title}: No proposed changes!\n") + end + + :dry_run_with_no_changes + + sources -> + Mix.shell().info("\n#{title}: Proposed changes:\n") + + Enum.each(sources, fn source -> + if Rewrite.Source.from?(source, :string) do + content_lines = + source + |> Rewrite.Source.get(:content) + |> String.split("\n") + |> Enum.with_index() + + space_padding = + content_lines + |> Enum.map(&elem(&1, 1)) + |> Enum.max() + |> to_string() + |> String.length() + + diffish_looking_text = + Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} -> + line_number = line_number_minus_one + 1 + + "#{String.pad_trailing(to_string(line_number), space_padding)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}" + end) + + Mix.shell().info(""" + Create: #{Rewrite.Source.get(source, :path)} + + #{diffish_looking_text} + """) + else + Mix.shell().info(""" + Update: #{Rewrite.Source.get(source, :path)} + + #{Rewrite.Source.diff(source)} + """) + end + end) + + :dry_run_with_changes + end + + if igniter.tasks != [] do + message = + if result_of_dry_run == :dry_run_with_no_changes do + "The following tasks will be run" + else + "The following tasks will be run after the above changes:" + end + + Mix.shell().info(""" + #{message} + + #{Enum.map_join(igniter.tasks, "\n", fn {task, args} -> "* #{IO.ANSI.red()}#{task}#{IO.ANSI.yellow()} #{Enum.join(args, " ")}#{IO.ANSI.reset()}" end)} + """) + end + + if "--dry-run" in argv || result_of_dry_run == :dry_run_with_no_changes do + result_of_dry_run + else + if "--yes" in argv || + Mix.shell().yes?(opts[:confirmation_message] || "Proceed with changes?") do + sources + |> Enum.any?(fn source -> + Rewrite.Source.updated?(source) + end) + |> if do + igniter.rewrite + |> Rewrite.write_all() + |> case do + {:ok, _result} -> + igniter.tasks + |> Enum.each(fn {task, args} -> + Mix.Task.run(task, args) + end) + + :changes_made + + {:error, error, rewrite} -> + igniter + |> Map.put(:rewrite, rewrite) + |> Igniter.add_issue(error) + |> igniter_issues() + + :issues + end + else + :no_changes + end + else + :changes_aborted + end + end + + igniter -> + igniter_issues(igniter) + :issues + end + end + end + + defp igniter_issues(igniter) do + Mix.shell().info("Issues during code generation") + + igniter.issues + |> Enum.map_join("\n", fn error -> + if is_binary(error) do + "* #{error}" + else + "* #{Exception.format(:error, error)}" + end + end) + |> Mix.shell().info() + end + + defp explain_issues(issues) do + Mix.shell().info("Igniter: Issues found in proposed changes:\n") + + Enum.each(issues, fn {source, issues} -> + Mix.shell().info("Issues with #{Rewrite.Source.get(source, :path)}") + + issues + |> Enum.map_join("\n", fn error -> + if is_binary(error) do + "* #{error}" + else + "* #{Exception.format(:error, error)}" + end + end) + |> Mix.shell().info() + end) + end + defp format(igniter, adding_path \\ nil) do if adding_path && Path.basename(adding_path) == ".formatter.exs" do format(igniter) @@ -179,6 +358,7 @@ defmodule Igniter do end end + # sobelow_skip ["RCE.CodeModule"] defp find_formatter_exs_file_options(path, formatter_exs_files) do case Map.fetch(formatter_exs_files, path) do {:ok, source} -> @@ -249,6 +429,7 @@ defmodule Igniter do nil end + # sobelow_skip ["RCE.CodeModule"] defp eval_file_with_keyword_list(path) do {opts, _} = Code.eval_file(path) diff --git a/lib/install.ex b/lib/install.ex index a64629b..0b89dff 100644 --- a/lib/install.ex +++ b/lib/install.ex @@ -1,23 +1,21 @@ defmodule Igniter.Install do + @moduledoc false @option_schema [ switches: [ - no_network: :boolean, example: :boolean, - dry_run: :boolean + dry_run: :boolean, + yes: :boolean ], aliases: [ d: :dry_run, - n: :no_network, - e: :example + e: :example, + y: :yes ] ] # only supports hex installation at the moment def install(install, argv) do - install_list = - install - |> String.split(",") - |> Enum.map(&String.to_atom/1) + install_list = install_list(install) Application.ensure_all_started(:req) @@ -69,7 +67,7 @@ defmodule Igniter.Install do end dependency_add_result = - Igniter.Tasks.do_or_dry_run(igniter, argv, + Igniter.do_or_dry_run(igniter, argv, title: "Fetching Dependency", quiet_on_no_changes?: true, confirmation_message: confirmation_message @@ -89,7 +87,7 @@ defmodule Igniter.Install do """) if install_dep_now? do - Igniter.Tasks.do_or_dry_run(igniter, (argv ++ ["--yes"]) -- ["--dry-run"], + Igniter.do_or_dry_run(igniter, (argv ++ ["--yes"]) -- ["--dry-run"], title: "Fetching Dependency", quiet_on_no_changes?: true ) @@ -110,7 +108,7 @@ defmodule Igniter.Install do end all_tasks = - Enum.filter(Mix.Task.load_all(), &Spark.implements_behaviour?(&1, Igniter.Mix.Task)) + Enum.filter(Mix.Task.load_all(), &implements_behaviour?(&1, Igniter.Mix.Task)) install_list |> Enum.flat_map(fn install -> @@ -123,8 +121,44 @@ defmodule Igniter.Install do |> Enum.reduce(Igniter.new(), fn task, igniter -> Igniter.compose_task(igniter, task, argv) end) - |> Igniter.Tasks.do_or_dry_run(argv) + |> Igniter.do_or_dry_run(argv) :ok end + + defp implements_behaviour?(module, behaviour) do + :attributes + |> module.module_info() + |> Enum.any?(fn + {:behaviour, ^behaviour} -> + true + + # optimizations, probably extremely minor but this is in a tight loop in some places + {:behaviour, [^behaviour | _]} -> + true + + {:behaviour, [_, ^behaviour | _]} -> + true + + {:behaviour, [_, _, ^behaviour | _]} -> + true + + # never seen a module with three behaviours in real life, let alone four. + {:behaviour, behaviours} when is_list(behaviours) -> + module in behaviours + + _ -> + false + end) + rescue + _ -> + false + end + + # sobelow_skip ["DOS.StringToAtom"] + defp install_list(install) do + install + |> String.split(",") + |> Enum.map(&String.to_atom/1) + end end diff --git a/lib/mix/task.ex b/lib/mix/task.ex index a457658..1e89456 100644 --- a/lib/mix/task.ex +++ b/lib/mix/task.ex @@ -1,4 +1,5 @@ defmodule Igniter.Mix.Task do + @moduledoc "A behaviour for implementing a Mix task that is enriched to be composable with other Igniter tasks." @callback supports_umbrella?() :: boolean() @callback igniter(igniter :: Igniter.t(), argv :: list(String.t())) :: Igniter.t() @@ -18,7 +19,7 @@ defmodule Igniter.Mix.Task do Igniter.new() |> igniter(argv) - |> Igniter.Tasks.do_or_dry_run(argv) + |> Igniter.do_or_dry_run(argv) end def supports_umbrella?, do: false diff --git a/lib/mix/tasks/igniter.install.ex b/lib/mix/tasks/igniter.install.ex index 6794f0d..399e053 100644 --- a/lib/mix/tasks/igniter.install.ex +++ b/lib/mix/tasks/igniter.install.ex @@ -1,7 +1,21 @@ defmodule Mix.Tasks.Igniter.Install do + @moduledoc """ + Install a package or packages, and run any associated installers. + + ## Args + + mix igniter.install package1,package2,package3 + + ## Switches + + * `--dry-run` - `d` - Run the task without making any changes. + * `--yes` - `y` - Automatically answer yes to any prompts. + * `--example` - `e` - Request that installed packages include initial example code. + """ use Mix.Task @impl true + @shortdoc "Install a package or packages, and run any associated installers." def run([install | argv]) do Application.ensure_all_started([:rewrite]) diff --git a/lib/mix/tasks/spark.install.ex b/lib/mix/tasks/spark.install.ex deleted file mode 100644 index 0fdc40c..0000000 --- a/lib/mix/tasks/spark.install.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Mix.Tasks.Spark.Install do - use Igniter.Mix.Task - - def igniter(igniter, _argv) do - igniter - |> Igniter.Formatter.add_formatter_plugin(Spark.Formatter) - |> Igniter.Config.configure("config.exs", :spark, [:formatter, :remove_parens?], true, & &1) - end -end diff --git a/lib/module.ex b/lib/module.ex index 26040f6..e64bcb5 100644 --- a/lib/module.ex +++ b/lib/module.ex @@ -1,9 +1,10 @@ defmodule Igniter.Module do + @moduledoc "Codemods and tools for generating and working with Elixir modules" def module_name(suffix) do Module.concat(module_name_prefix(), suffix) end - def module_name_prefix() do + def module_name_prefix do Mix.Project.get!() |> Module.split() |> :lists.droplast() diff --git a/lib/tasks.ex b/lib/tasks.ex deleted file mode 100644 index c8ddf41..0000000 --- a/lib/tasks.ex +++ /dev/null @@ -1,180 +0,0 @@ -defmodule Igniter.Tasks do - def app_name do - Mix.Project.config()[:app] - end - - def do_or_dry_run(igniter, argv, opts \\ []) do - igniter = %{igniter | issues: Enum.uniq(igniter.issues)} - title = opts[:title] || "Igniter" - - sources = - igniter.rewrite - |> Rewrite.sources() - - issues = - Enum.flat_map(sources, fn source -> - changed_issues = - if Rewrite.Source.file_changed?(source) do - ["File has been changed since it was originally read."] - else - [] - end - - issues = Enum.uniq(changed_issues ++ Rewrite.Source.issues(source)) - - case issues do - [] -> [] - issues -> [{source, issues}] - end - end) - - case issues do - [_ | _] -> - explain_issues(issues) - :issues - - [] -> - if igniter.issues == [] do - result_of_dry_run = - sources - |> Enum.filter(fn source -> - Rewrite.Source.updated?(source) - end) - |> case do - [] -> - unless opts[:quiet_on_no_changes?] do - Mix.shell().info("\n#{title}: No proposed changes!\n") - end - - :dry_run_with_no_changes - - sources -> - Mix.shell().info("\n#{title}: Proposed changes:\n") - - Enum.each(sources, fn source -> - if Rewrite.Source.from?(source, :string) do - content_lines = - source - |> Rewrite.Source.get(:content) - |> String.split("\n") - |> Enum.with_index() - - space_padding = - content_lines - |> Enum.map(&elem(&1, 1)) - |> Enum.max() - |> to_string() - |> String.length() - - diffish_looking_text = - Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} -> - line_number = line_number_minus_one + 1 - - "#{String.pad_trailing(to_string(line_number), space_padding)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}" - end) - - Mix.shell().info(""" - Create: #{Rewrite.Source.get(source, :path)} - - #{diffish_looking_text} - """) - else - Mix.shell().info(""" - Update: #{Rewrite.Source.get(source, :path)} - - #{Rewrite.Source.diff(source)} - """) - end - end) - - :dry_run_with_changes - end - - if igniter.tasks != [] do - message = - if result_of_dry_run in [:dry_run_with_no_changes, :no_changes] do - "The following tasks will be run" - else - "The following tasks will be run after the above changes:" - end - - Mix.shell().info(""" - #{message} - - #{Enum.map_join(igniter.tasks, "\n", fn {task, args} -> "* #{IO.ANSI.red()}#{task}#{IO.ANSI.yellow()} #{Enum.join(args, " ")}#{IO.ANSI.reset()}" end)} - """) - end - - if "--dry-run" in argv || result_of_dry_run == :dry_run_with_no_changes do - result_of_dry_run - else - if "--yes" in argv || - Mix.shell().yes?(opts[:confirmation_message] || "Proceed with changes?") do - sources - |> Enum.any?(fn source -> - Rewrite.Source.updated?(source) - end) - |> if do - igniter.rewrite - |> Rewrite.write_all() - |> case do - {:ok, _result} -> - igniter.tasks - |> Enum.each(fn {task, args} -> - Mix.Task.run(task, args) - end) - - :changes_made - - {:error, error} -> - igniter - |> Igniter.add_issue(error) - |> igniter_issues() - - {:error, error} - end - else - :no_changes - end - else - :changes_aborted - end - end - else - igniter_issues(igniter) - end - end - end - - defp igniter_issues(igniter) do - Mix.shell().info("Issues during code generation") - - igniter.issues - |> Enum.map_join("\n", fn error -> - if is_binary(error) do - "* #{error}" - else - "* #{Exception.format(:error, error)}" - end - end) - |> Mix.shell().info() - end - - defp explain_issues(issues) do - Mix.shell().info("Igniter: Issues found in proposed changes:\n") - - Enum.each(issues, fn {source, issues} -> - Mix.shell().info("Issues with #{Rewrite.Source.get(source, :path)}") - - issues - |> Enum.map_join("\n", fn error -> - if is_binary(error) do - "* #{error}" - else - "* #{Exception.format(:error, error)}" - end - end) - |> Mix.shell().info() - end) - end -end diff --git a/mix.exs b/mix.exs index e5b5282..a102617 100644 --- a/mix.exs +++ b/mix.exs @@ -9,6 +9,7 @@ defmodule Igniter.MixProject do version: @version, elixir: "~> 1.16", start_permanent: Mix.env() == :prod, + aliases: aliases(), docs: docs(), deps: deps() ] @@ -52,7 +53,6 @@ defmodule Igniter.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:spark, "~> 2.0"}, {:rewrite, "~> 0.9"}, {:req, "~> 0.4"}, # Dev/Test dependencies @@ -70,4 +70,11 @@ defmodule Igniter.MixProject do {:doctor, "~> 0.21", only: [:dev, :test]} ] end + + defp aliases do + [ + sobelow: "sobelow --skip", + credo: "credo --strict" + ] + end end diff --git a/mix.lock b/mix.lock index f3a87e5..020f03f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,15 @@ %{ - "ash": {:hex, :ash, "3.0.7", "6c37e092f53b1b21eb89596f600a652b2a601f84378f44fd5dd1cdec72eb1cc2", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9288ddb50fe727096c6f63fd82c631de2505dcd29bdfa50b5dc13c865f0bf434"}, "benchee": {:hex, :benchee, "1.3.0", "f64e3b64ad3563fa9838146ddefb2d2f94cf5b473bdfd63f5ca4d0657bf96694", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "34f4294068c11b2bd2ebf2c59aac9c7da26ffa0068afdf3419f1b176e16c5f81"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, - "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "eflame": {:hex, :eflame, "1.0.1", "0664d287e39eef3c413749254b3af5f4f8b00be71c1af67d325331c4890be0fc", [:mix], [], "hexpm", "e0b08854a66f9013129de0b008488f3411ae9b69b902187837f994d7a99cf04e"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_doc": {:hex, :ex_doc, "0.33.0", "690562b153153c7e4d455dc21dab86e445f66ceba718defe64b0ef6f0bd83ba0", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "3f69adc28274cb51be37d09b03e4565232862a4b10288a3894587b0131412124"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, @@ -23,7 +19,6 @@ "glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"}, "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, @@ -36,17 +31,12 @@ "nimble_ownership": {:hex, :nimble_ownership, "0.3.1", "99d5244672fafdfac89bfad3d3ab8f0d367603ce1dc4855f86a1c75008bce56f", [:mix], [], "hexpm", "4bf510adedff0449a1d6e200e43e57a814794c8b5b6439071274d248d272a549"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "reactor": {:hex, :reactor, "0.8.4", "344d02ba4a0010763851f4e4aa0ff190ebe7e392e3c27c6cd143dde077b986e7", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "49c1fd3c786603cec8140ce941c41c7ea72cc4411860ccdee9876c4ca2204f81"}, "req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"}, "rewrite": {:hex, :rewrite, "0.10.3", "1c998cceac960c3025a1701158d846dee94bc426d95abefd2b4a2e981835ea1c", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "d3ea3179de167ebda56bf81b7e5c2697256a0719fdcc2c0df65ea8173efe3563"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sourceror": {:hex, :sourceror, "1.2.1", "b415255ad8bd05f0e859bb3d7ea617f6c2a4a405f2a534a231f229bd99b89f8b", [:mix], [], "hexpm", "e4d97087e67584a7585b5fe3d5a71bf8e7332f795dd1a44983d750003d5e750c"}, - "spark": {:hex, :spark, "2.1.22", "a36400eede64c51af578de5fdb5a5aaa3e0811da44bcbe7545fce059bd2a990b", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "f764611d0b15ac132e72b2326539acc11fc4e63baa3e429f541bca292b5f7064"}, - "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, - "stream_data": {:hex, :stream_data, "1.0.0", "c1380747a4650902732696861d5cb66ad3cb1cc93f31c2c8498bf87cddbabe2d", [:mix], [], "hexpm", "acd53e27c66c617d466f42ec77a7f59e5751f6051583c621ccdb055b9690435d"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, - "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, } diff --git a/test/config_test.exs b/test/config_test.exs index 2b40f70..db496d9 100644 --- a/test/config_test.exs +++ b/test/config_test.exs @@ -123,3 +123,22 @@ defmodule Igniter.ConfigTest do end end end + +defmodule Mix.Tasks.LiveViewNative.Install do + use Igniter.Mix.Task + + def igniter(igniter, argv) do + Igniter.new() + |> Igniter.Config.configure("fake.exs", :fake, [:foo], %{"b" => ["c", "d"]}, fn zipper -> + Igniter.Common.set_map_key(zipper, "b", ["c", "d"], fn zipper -> + zipper + |> Igniter.Common.prepend_new_to_list(zipper, "c") + |> Igniter.Common.prepend_new_to_list(zipper, "d") + end) + |> case do + {:ok, new_zipper} -> new_zipper + _ -> zipper + end + end) + end +end