Compare commits

..
185 Commits
Author SHA1 Message Date
José Valim 327063cc84 Release v1.16.3 2024-05-21 00:14:25 +02:00
José Valim 795d0a9583 More fixes 2024-05-20 16:51:13 +02:00
José Valim 1150ebad03 Update CHANGELOG 2024-05-06 12:50:25 +02:00
José Valim 18feec4cd8 Add brackets around keyword lists when formatting with when, closes #13503 2024-05-06 12:44:54 +02:00
José Valim 7d5920206b Check that docs are not hidden in suite, closes #13516 2024-05-02 01:26:48 +02:00
José Valim a694447601 Ensure translators are persisted across logger restarts 2024-05-01 11:40:12 +02:00
José Valim 19ae388dda Add .bat/.com disclaimers to System.cmd and Port 2024-04-17 11:29:02 +02:00
José Valim 6b5edea5f1 Fix --dbg handling in bin/elixir, closes #13482 2024-04-08 09:31:20 +02:00
Arne Bedurftig b736c23af9 Fix typo in basic types (#13481)
The exclamation mark was missing in the result of the concatenation.
2024-04-07 20:58:08 +02:00
Theodor Fiedler 63f0000ace Translate :undefined URI port to nil (#13464)
Resolves #13462

When a url string is schema less and the host is followed by a colon
without setting the actual port, `:uri_string.parse/1` returns the
port to be `:undefined`. As long as the url string is parseable,
we should translate `:undefined` to `nil`, in order to ensure we return
a valid URI struct.
2024-04-04 12:43:16 +02:00
José Valim 5e8b187d08 Ensure compile paths are available during compilation, closes #13458 2024-04-01 12:18:01 +02:00
Jean Klingler aea1a47b8d Only infer size in pinned variable when needed (#13423) 2024-03-20 19:00:17 +09:00
Jean Klingler eeba8e992d Fix Enum.slide/3 example in cheatsheet (#13054) 2024-03-16 17:09:52 +01:00
Steven C dd42dd8620 Fix typo around Enum.slide/3 in the Enum cheatsheet (#13053) 2024-03-16 17:09:45 +01:00
José Valim ed1ddfd338 Add parens to private macro example, closes #13411 2024-03-13 12:28:36 +01:00
José Valim d17df054e2 Skip tests if Erlang was compiled without docs, closes #13322 2024-03-10 19:07:22 +01:00
José Valim 41f6ee3f2e Release v1.16.2 2024-03-10 11:54:45 +01:00
José Valim 9d31768f42 Update CHANGELOG 2024-03-07 11:25:06 +01:00
José Valim 558e0ba6a6 Discard mermaid fenced blocks from ansi docs 2024-03-07 11:06:07 +01:00
Jean Klingler ab1e3448f0 Replace deprecated :code.lib_dir/2 usages (#13395) 2024-03-06 13:00:19 +01:00
Jean Klingler 17cd103a28 Logger handles :process_label from OTP27 (#13392) 2024-03-06 13:00:19 +01:00
José Valim 60e6ceae10 Correct task link in docs, closes #13388 2024-03-06 12:59:20 +01:00
José Valim f64ccd0871 Improve capture_log docs with latest Logger 2024-03-05 18:37:17 +01:00
Jonatan Kłosko 6c463b2310 Ensure install-related functions do not crash when Mix is not started (#13391) 2024-03-04 20:45:03 +01:00
H e310dd91d6 Remove duplicate line in formatter_callback doc (#13387) 2024-03-04 15:46:47 +09:00
Jonatan Kłosko 29ef147a22 Remove consolidated when restoring Mix.install/2 dir (#13382) 2024-03-01 09:34:26 +01:00
Jonatan Kłosko 74ab38439e Fix Mix.install_project_dir/0 spec (#13381) 2024-03-01 09:34:26 +01:00
José Valim 5654752026 Add @doc since to install_project_dir 2024-02-29 20:30:48 +01:00
Jonatan Kłosko e78538a42b Add environment variable for reusing Mix.install/2 installation (#13378) 2024-02-29 19:20:17 +01:00
Jonatan Kłosko 079b92e98e Support recompiling local Mix.install/2 dependencies (#13375) 2024-02-29 08:41:11 +01:00
José Valim 325877a224 Clarify the meaning of --overwrite, closes #13371 2024-02-26 20:16:27 +01:00
José Valim 097c4cf672 Emit defmodule tracing event 2024-02-26 18:28:47 +01:00
Łukasz Samson fe16fd3442 Add documentation to Exception callbacks (#13367) 2024-02-24 16:23:16 +01:00
Jean Klingler e520e8ce96 Fix unexpected rounding signs on OPT26- (#13365) 2024-02-24 16:05:09 +01:00
Jean Klingler dd004539c6 Fixes to support 0TP27 (#13351)
* Fix non-deterministic key-value tests

* Fix non-deterministic Enum tests

* Fix :only option when deriving Inspect

* Float.ceil and Float.floor return -0.0 for negative numbers

* Fix non-deterministic Registry doctest

* Simplify check
2024-02-24 16:03:58 +01:00
Jean Klingler 2512cbaff0 Fix doctest example since Inspect.MapSet changed (#13366) 2024-02-24 22:21:18 +09:00
sabiwara a99a9cf47a Fix test due to backport 2024-02-24 00:11:43 +09:00
Jean Klingler 677ad618e9 Fix charlist formatting issue on '\"' (#13364) 2024-02-24 00:02:38 +09:00
Philip Munksgaard ff309747b7 Escape pinned values when computing diff (#13354)
Take these two tests:

```elixir
  test "correctly colored" do
    assert [{:foo}] = [{:bar}]
  end

  test "incorrectly colored" do
    val = [{:foo}]
    assert ^val = [{:bar}]
  end
```

That's because when diffing a pin, we were not converting the underlying
diff context from match to ===.

This fixes #13348
2024-02-17 12:51:27 +01:00
José Valim e5ec5ce7f6 Add API for deleting SCM
This is used by Hex when its application is stopped
to revert its changes to Mix state.
2024-02-15 12:22:53 +01:00
Jean Klingler f3e669c471 Fix :trim_doc example in doc (#13340) 2024-02-13 07:39:31 +09:00
Jean Klingler bf915aa2e7 Fix <> match test (#13327) 2024-02-09 17:03:42 +09:00
Jean KlinglerandJosé Valim ef3e2796ef Update argument error message when matching with <> (#13325)
* Update argument error message when matching with <>

* Update lib/elixir/lib/kernel.ex

Co-authored-by: José Valim <jose.valim@gmail.com>

---------

Co-authored-by: José Valim <jose.valim@gmail.com>
2024-02-09 16:50:08 +09:00
Mitchell Hanberg d8cc841ab0 Include from_brackets metadata in all cases (#13317) 2024-02-05 09:49:40 +01:00
José Valim 7098ac8a16 Preserve . semantics in Path.relative_to, closes #13310 2024-02-01 12:41:26 +01:00
José Valim e0658bb55a Release v1.16.1 2024-01-31 10:24:07 +01:00
José Valim cf8c28c34b Fix autocompletion on Erlang/OTP 26, closes #13307 2024-01-31 10:12:58 +01:00
José Valim 60dcd14390 Fix Rebar3 env var with spaces (#13303) 2024-01-30 20:21:33 +01:00
Steve Johns 8344e218a6 docs: fix grammar errors in syntax reference docs (#13302) 2024-01-30 19:57:23 +01:00
José Valim 740b2d74df Escape rebar3 paths 2024-01-29 10:19:10 +01:00
José Valim da0189e641 Fix docs link
Related to #13284.
2024-01-27 11:46:53 +01:00
José Valim ad778a6f29 Fix capitalize for single codepoint (#13268) 2024-01-19 19:43:03 +01:00
José Valim c35edd1ee8 Improve end_of_expression docs 2024-01-18 08:14:04 +09:00
José Valim bb8ad4406b Improve ast metadata docs 2024-01-18 08:13:57 +09:00
Jean Klingler 111b48dcf4 Resolve relative paths in exunit filter (#13258) 2024-01-18 08:12:32 +09:00
Mitchell Hanberg 6dccefe687 Fix :from_interpolation docs (#13251) 2024-01-18 08:12:03 +09:00
Roman 9ea950c9af Macro anti-patterns: fix incorrect error message in code examples (#13259) 2024-01-18 08:09:13 +09:00
José Valim 7d100fff9f Always log errors at the end of compilation 2024-01-17 23:16:04 +01:00
Roman 6b69c7f5ac Fix the explanation to match the explained code example in docs (#13255)
The description incorrectly states that both processes are initialized with 0, while in the code the second process receives a non-default initial value.
2024-01-15 13:42:51 +01:00
Jean Klingler 691d402341 Fix Code.Normalizer for keyword operand with :do key (#13250) 2024-01-13 17:59:16 +09:00
José Valim b96211a325 Do not assume there is a $HOME, closes #13127 2024-01-10 13:01:55 +01:00
José Valim 17b9ba61d9 Do not crash parallel compiler on external reports, closes #13224 2024-01-10 10:17:03 +01:00
Hussien Liban f83b4e4e84 Update design-anti-patterns.md (#13210)
Not setting an option returns just the integer
2024-01-08 20:39:08 +09:00
Adebisi AdeyeyeandAdebisi Adeyeye 4de331597a Fix typo in docs: initial_valye -> initial_value (#13211)
Co-authored-by: Adebisi Adeyeye <adebisi.adeyeye@proebb.com>
2024-01-08 20:39:08 +09:00
Tomás Grüner 6e295998ef Fix typo in docs: Keywoird -> Keyword (#13205) 2024-01-08 20:39:08 +09:00
Will DouglasandWill Douglas f2dae095f8 Fix typo in docs: for -> force (#13215)
Co-authored-by: Will Douglas <will.cavalcanti@vmtecnologia.io>
2024-01-08 20:39:08 +09:00
Jean Klingler 6f5715fc30 Replace single quotes in charlist in doc (#13233) 2024-01-08 20:39:08 +09:00
José Valim 1ece71a831 Handle Windows separators on mix test (#13232)
Closes #13225.
2024-01-07 23:07:20 +01:00
Ryan B. Harvey 197351dd73 Fix a comment typo in the Code Anti-Patterns doc page (#13219) 2024-01-02 18:03:37 +01:00
José Valim 0d671dafea Improve anti-pattern titles 2024-01-02 16:42:17 +01:00
José Valim 9ae7c39125 Update CHANGELOG, closes #13217 2023-12-31 18:20:20 +01:00
José Valim ba8fb4dff1 Improve yecc/leex warnings, closes #13213 2023-12-28 17:12:51 +01:00
José Valim 194661197e Add more examples to app config anti-pattern (#13204) 2023-12-26 09:57:32 +01:00
Travis Vander Hoop 298acd1cdb Fix typos and tweak language in anti-pattern docs (#13208) 2023-12-26 09:57:32 +01:00
Artem Solomatin 97c608c346 Fix typo in design-anti-patterns doc (#13207) 2023-12-26 09:57:32 +01:00
José Valim f84bc19bf2 Additional clarity on long list of parameters anti-patterns 2023-12-24 14:54:45 +01:00
Alex Martsinovich 91375778cb Fix example in non-assertive map access antipattern (#13201) 2023-12-23 10:31:57 +01:00
José Valim 38ddc98fef Release v1.16.0 2023-12-22 17:45:12 +01:00
José Valim 1b7c73273d Last pass over anti-patterns 2023-12-22 14:31:44 +01:00
José Valim 5bd5e75c3b Extract snippet in elixir_errors to simplify exception handling 2023-12-21 21:14:55 +01:00
José Valim 1cba584ac8 Fix column precision in test 2023-12-21 19:48:44 +01:00
José Valim dfafea092e Consider column in snippets, closes #13199 2023-12-21 19:30:15 +01:00
José Valim 0ae4bfb222 Preserve diagnostics based on source field, closes #13142 2023-12-21 17:22:43 +01:00
José Valim 7db4413476 Add source field to diagnostics 2023-12-21 17:22:43 +01:00
José Valim 4d52d18ef7 Do not reset column state in tests 2023-12-21 17:22:43 +01:00
José Valim 39ff86c551 Indent lists 2023-12-21 15:47:59 +01:00
Tobias Pfeiffer 12fdd8ea9b Document the process anti pattern of sending large data (#13194)
Follow up to/extension of #13173
2023-12-21 15:47:59 +01:00
José Valim 28f8eba9a2 Improve docs for URI.encode/2 2023-12-17 13:05:48 +01:00
José Valim b3b14202d9 Normalize exception handling in diagnostics 2023-12-17 11:40:49 +01:00
José Valim ffa3ef1ccf Remove unused function 2023-12-14 17:19:58 +01:00
José Valim d13f4155af Do not warn unused imports twice, closes #13178 2023-12-14 17:19:58 +01:00
José Valim d23e42e27f Normalize token missing and mismatched delimiter exceptions
Closes #13183.
Closes #13185.
Closes #13186.
Closes #13187.
2023-12-14 17:07:44 +01:00
José Valim 872efb180d Document diagnostic span 2023-12-14 12:07:20 +01:00
José Valim 2e61f32b6d Update CHANGELOG 2023-12-14 11:23:56 +01:00
José Valim 7abb3bdddc Unify position handling and improve docs
See #13179.
See #13184.
2023-12-14 11:15:05 +01:00
Vinícius Müller 67fd9824cc Improve diagnostics for unclosed heredocs (#13182) 2023-12-14 09:24:59 +01:00
José Valim 1878e30bdb Improve docs and support column in IO.warn, closes #13179 2023-12-13 23:31:49 +01:00
Vinícius Müller 88bbffd614 Add info about unclosed delimiters diagnostic to 1.16 changelog (#13180) 2023-12-13 23:31:38 +01:00
Maarten van Vliet a50a2fd983 Fix typo (#13181) 2023-12-14 07:17:11 +09:00
Tobias Pfeiffer c950a57a3e Fix top level functions changelog (enabled --> disabled) (#13177) 2023-12-13 22:56:19 +09:00
Tobias Pfeiffer 80404da8f6 Mention the fixed regression (#13176)
As best as I learned this was introduced in #11420 and fixed in aabe465
It shouldn't affect almost anyone except for scripting usage of
elixir and (probably most notably) benchee benchmarks that don't
call functions in a module.
2023-12-13 14:36:33 +01:00
José Valim 010d516b11 Fix indentation in mix release 2023-12-13 11:56:41 +01:00
José Valim fb42733873 Improve anonymous functions guide 2023-12-13 06:43:20 +01:00
Tobias Pfeiffer 1070b2f434 Mention dangers around Task and sending a lot of data along (#13173)
The `Task` module is one of the coolest modules in elixir and
is probably the first contact and experience of a lot of
beginners with parallelism in elixir. I hence find it worthwhile
to warn about the memory copying and its impacts here as it might
easily lead to unwelcome results, so it's worth pointing out.
2023-12-12 16:45:00 +01:00
Wojtek Mach 9675e2285a elixir.bat: Quote file paths (#13172) 2023-12-12 14:32:06 +01:00
José Valim a09ddbb0a6 Fix typo in CHANGELOG 2023-12-12 12:43:34 +01:00
José Valim 81a12b7774 Release v1.16.0-rc.1 2023-12-12 12:36:20 +01:00
José Valim 88ba7766e0 Use Macro.Env in more warnings 2023-12-11 11:13:39 +01:00
José Valim 6475917b3f Use Macro.Env to record attribute warnings
Closes #13162.
Closes #13164.
2023-12-11 07:34:16 +01:00
José Valim 5b6e04cb4d Clean up failed deletion warning 2023-12-10 09:03:54 +01:00
Daven 2bfb14751f Add warning when deps clean fails (#13161) 2023-12-10 09:03:54 +01:00
José Valim b1998960bb Disable compiler optimizations only in module body 2023-12-09 20:49:40 +11:00
José Valim c4dcbf379f Pass original exception down to details in diagnostic, closes #13142 2023-12-09 20:49:40 +11:00
Himanshu 3b5bd39743 Update case-cond-and-if.md (#13158) 2023-12-08 21:58:51 +11:00
Wojtek Mach 4e4cde1118 Update Windows installer to write Elixir install root to registry (#13157)
We don't need this right now but it could be useful in the future, if
anything to detect if Elixir was installed using this installer.

Demo:

    iex> {:ok, r} = :win32reg.open([:read])
    iex> :win32reg.change_key(r, ~c"\\hklm\\software\\wow6432node\\elixir\\elixir")
    iex> :win32reg.value(r, ~c"installroot")
    {:ok, ~c"C:\\Program Files\\Elixir"}
2023-12-07 10:28:23 +11:00
Wojtek Mach e1db6d8831 Update Windows installer to register in Add/Remove Programs (#13156) 2023-12-07 08:44:39 +11:00
José Valim db8a1cdc7d Revert "Consider surround context until end whenever possible"
This reverts commit a65dae971f.
2023-12-04 22:34:56 +10:00
José Valim d25aacb207 Simplify offset handling in TokenMissingError 2023-12-04 22:08:53 +10:00
Vinícius Müller 106539b5d2 Improve unclosed delimiter messages (#13123) 2023-12-04 22:08:53 +10:00
José Valim d716bc2703 Include both priv and include in releases, closes #13145 2023-11-25 10:34:58 +08:00
José Valim aa0dcb9a70 Fix prying functions with only literals, closes #13133 2023-11-23 22:33:39 +08:00
Zeke Douandc4710n 01366ef526 Add Logger.levels/0 (#13136)
Co-authored-by: c4710n <c4710n@users.noreply.github.com>
2023-11-23 22:09:09 +08:00
Andrea Leopardi 6145599638 Add t/0 types to remaining ExUnit exceptions (#13139) 2023-11-23 12:17:02 +01:00
Andrea Leopardi abd4f54d78 Fix typo in Logger docs 2023-11-23 12:11:07 +01:00
Andrea Leopardi adff7f6d34 Add t/0 types for some ExUnit exceptions (#13134) 2023-11-23 17:15:59 +08:00
Andrea Leopardi 2364f04991 Add callback docs to ExUnit.Formatter (#13135) 2023-11-23 17:15:59 +08:00
Łukasz Samson 46a5c54844 Properly escape \ in Path.wildcard docs (#13137) 2023-11-23 17:15:59 +08:00
Andrea Leopardi 2caacae2e4 Add some specs and types to ExUnit.Formatter (#13130) 2023-11-23 17:15:59 +08:00
José Valim 1ff5e0c88b Improve Logger docs, closes #13119 2023-11-22 09:24:37 +08:00
José Valim c150876e5f Remove warning on non-ambiguous nullary remote call 2023-11-22 08:55:00 +08:00
Wojtek Mach 758da1e311 Update Mix.Task.preferred_cli_env/1 docs (#13114) 2023-11-16 14:00:50 +01:00
Wojtek Mach 250b48aa3b Update Mix.Config mentions (#13115)
ExDoc main emitted these warnings on Elixir main:

```
    warning: documentation references module "Mix.Config" but it is hidden
    │
 49 │   `Mix.Config`, which was specific to Mix and has been deprecated.
    │   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ lib/elixir/lib/config.ex:49: Config (module)

    warning: documentation references module "Mix.Config" but it is hidden
    │
 51 │   You can leverage `Config` instead of `Mix.Config` in three steps. The first
    │   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ lib/elixir/lib/config.ex:51: Config (module)
```
2023-11-16 14:00:50 +01:00
Jean Klingler 0a9ee79fbb Formatter keeps quotes in atom keys (#13108) 2023-11-15 14:03:28 +01:00
Jean Klingler 58b8b93ee6 Auto infer size of matched variable in bitstrings (#13106) 2023-11-15 14:03:28 +01:00
Wojtek Mach aef8673666 Preserve column when translating typespecs (#13101) 2023-11-14 09:59:01 +01:00
José Valim 901ec18aa7 Handle error in Macro.to_string/1, closes #13102 2023-11-14 00:53:00 +01:00
José Valim 56b62b64cb Consider start line in MismatchedDelimiterError 2023-11-13 12:28:48 +01:00
Artem Solomatin abbf9061ea Fix links references (#13099) 2023-11-12 10:07:26 +01:00
José Valim 8f28bd9c3f Fix GenServer cheatsheet link
Closes #13098.
2023-11-11 21:55:42 +01:00
Christopher Keele 68782004a7 Produce better error messages for non-binary mix git deps refspecs. (#13088)
When using git dependencies, a branch/ref/tag specifier
is passed verbatim to `System.cmd/3`. This can lead to
intimidating error messages when they are not provided
as a binary (for instance, an atom like `tag: :stable`):

```
** (ArgumentError) all arguments for System.cmd/3 must be binaries
    (elixir 1.15.6) lib/system.ex:1083: System.cmd/3
    (mix 1.15.6) lib/mix/scm/git.ex:287: Mix.SCM.Git.git!/2
```

This PR adds a check during git opts verification time to provide
better feedback.
2023-11-11 09:20:21 +01:00
José Valim 0a146d10cc Fix link, closes #13095 2023-11-11 09:00:07 +01:00
José Valim beaa33d088 Handle nil values in IO.warn 2023-11-10 12:53:47 +01:00
José Valim 2a94668e6e Let's not deprecate ...foo as the API may be useful
for the type system in the future.

This reverts commit f97d8585e8.
2023-11-10 12:36:38 +01:00
Cameron Duley 56e6494b36 Use in/2 in String.replace_invalid/2 guards (#13093) 2023-11-10 09:09:29 +01:00
Cameron Duley 033a177078 Fix String.replace_invalid/2 perf regressions (#13090) 2023-11-10 09:09:29 +01:00
Minh Daoandminhqdao b9fffa3b41 Fix typo in getting-started guide (#13085)
Co-authored-by: minhqdao <hello@minhdao.de>
2023-11-07 23:05:51 +01:00
Juan Barrios 7be85838fa Update float.ex description of ceil/2 and floor/2 (#13084) 2023-11-07 20:11:24 +01:00
Jacob Swanner 37f8832229 Fix Enum cheatsheet for drop/2 and take/2 with negative index (#13080) 2023-11-07 10:51:31 +01:00
José Valim 7ebf5c3032 Add :emit_warnings to Code.string_to_quoted 2023-11-06 16:31:05 +01:00
José Valim 7d4d42097d Restore code paths in archive.install/escript.install
Closes #13079.
2023-11-06 15:56:05 +01:00
Lucas Francisco da Matta Vegi 33e1b570bf Additional remarks for maintaining research history (#13078)
Similar to what we had already done with other anti-patterns that changed names
2023-11-06 15:56:05 +01:00
José Valim 8920cc1432 Fix case clause error on tokenizer 2023-11-06 13:29:19 +01:00
Erik André Jakobsen 9b480f985b [docs] Clean up sigils intro (#13077) 2023-11-06 13:29:19 +01:00
Łukasz Samson daab3f80a0 Fix crashes when :beam_lib.info(beam) returns error (#13075) 2023-11-05 20:15:33 +01:00
Tony Dang ec8782a486 Fix typo in "Getting Started - Enumerables and Streams" docs (#13073) 2023-11-04 11:14:48 +01:00
Ioannis Kyriazis 6745f775b2 is -> us (#13072) 2023-11-03 23:23:56 +01:00
Łukasz Samson 7923e4f383 Elixir 1.14.5 supports Erlang/OTP 26 (#13071) 2023-11-03 11:54:02 +01:00
Michał Łępicki d280846843 Fix typo: an dread -> and read (#13069) 2023-11-03 08:08:23 +01:00
Rich Morin 178b654a1b Fix typo (#13068) 2023-11-02 21:04:21 +01:00
José Valim 3a516a3f2b Docs to new options and functions 2023-11-02 19:32:25 +01:00
Cameron Duley d5316d558f Add String.replace_invalid/2 (#13067) 2023-11-02 19:32:25 +01:00
Jonatan Kłosko a7216decad Add offset option to File.stream! (#13063) 2023-11-02 17:58:22 +01:00
Michał Łępicki 8d545610d2 Fix Path.absname/2 spec (#13065) 2023-11-02 13:08:52 +01:00
José Valim 23c27b629d Warn if both :applications and :extra_applications are used 2023-11-02 10:19:11 +01:00
Panagiotis Nezis 0220d81513 Support --sparse in archive.install and escript.install (#13059) 2023-11-02 08:57:51 +01:00
José Valim aabe5dad76 Do not use Erlang/OTP 26.1 on CI (#13062)
It has a bug when looking up mismatched module names.
2023-11-02 08:30:01 +01:00
Łukasz Samson 741757f2a7 Lazily evaluate File.cwd! in Path.expand and Path.absname (#13061)
do not crash with File.Error with already absolute paths if File.cwd returns error or nil
2023-11-02 08:11:44 +01:00
José Valim f5e543576e Use explicit/implicit vs manual/automatic 2023-11-01 16:33:17 +01:00
José Valim e6b641a075 Fix typo on docs 2023-11-01 16:11:26 +01:00
rktjmp 3822609621 Restore GenServer introduction mermaid graph (#13058)
Restores graph removed in f5a61d1, with correct request -> reply arrow
ordering.
2023-11-01 16:11:26 +01:00
Panagiotis Nezis 0f65cb065a Additional remarks for application config anti-pattern for Mix tasks (#13057) 2023-11-01 12:42:56 +01:00
José Valim ae19236e08 Bring behaviour section from website 2023-11-01 11:17:59 +01:00
José Valim f422b77aaa Update docs 2023-11-01 10:34:59 +01:00
José Valim 43b3e94506 Add more examples to unrelated clauses 2023-10-31 20:57:30 +01:00
José Valim 2d86cb0027 Handle warnings from unquote functions 2023-10-31 12:00:04 +01:00
José Valim 792d4cc631 Release v1.16.0-rc.0 2023-10-31 09:16:09 +01:00
José Valim f8016bca48 Describe them as potential anti-patterns 2023-10-31 08:52:14 +01:00
José Valim 82818f7126 Improve complex extraction example 2023-10-31 07:49:48 +01:00
José Valim 5c8f9aac64 Fix getting started links 2023-10-30 23:40:15 +01:00
José Valim 1a8bad1bf2 Streamline unrelated introduction 2023-10-30 20:10:53 +01:00
José Valim 4b795871b6 Improve examples and docs 2023-10-30 15:50:53 +01:00
José Valim 3036401c7c Clarify best practices and update anti-patterns list 2023-10-30 15:12:41 +01:00
José Valim 8bbba572de Describe pattern matching as simpler 2023-10-30 09:18:25 +01:00
José Valim 968b51319e Clarify scope of anti-patterns 2023-10-30 08:11:25 +01:00
José Valim 900f25f832 Branch out v1.16 2023-10-29 12:30:32 +01:00
485 changed files with 18415 additions and 43308 deletions
+2 -1
View File
@@ -17,5 +17,6 @@
# Float tests
float_assert: 1
]
],
normalize_bitstring_modifiers: false
]
+134
View File
@@ -0,0 +1,134 @@
name: builds.hex.pm
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
concurrency: builds_txt
jobs:
release_pre_built:
strategy:
fail-fast: true
max-parallel: 1
matrix:
include:
- otp: 24
otp_version: '24.3'
upload_generic_version: upload_generic_version
- otp: 25
otp_version: '25.3'
- otp: 26
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get tags
run: git fetch --tags origin
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Utils.sh
run: |
cat << 'EOF' > utils.sh
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
EOF
chmod +x utils.sh
- name: Upload Docs to S3
if: ${{ matrix.build_docs }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
for f in doc/*; do
if [ -d "$f" ]; then
app=`echo $f | sed s/"doc\/"//`
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge "${surrogate_key}"
fi
done
- name: Upload Precompiled to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}
fi
- name: Update builds txt
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
ref_name=${{ github.ref_name }}
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
touch builds.txt
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
purge builds/elixir/txt
+5 -6
View File
@@ -3,12 +3,12 @@ name: CI for Markdown content
on:
push:
branches:
- "main"
- 'main'
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
pull_request:
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
workflow_dispatch:
jobs:
@@ -18,7 +18,7 @@ jobs:
strategy:
fail-fast: false
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Check out the repository
@@ -27,8 +27,7 @@ jobs:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
uses: DavidAnson/markdownlint-cli2-action@v13.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+10 -14
View File
@@ -11,6 +11,7 @@ on:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
@@ -18,24 +19,22 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "27.1"
deterministic: true
- otp_version: "27.1"
otp_latest: true
- otp_version: "27.0"
- otp_version: "26.0"
otp_latest: true
- otp_version: "25.3"
- otp_version: "25.0"
- otp_version: "24.3"
- otp_version: "24.0"
- otp_version: master
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
@@ -43,9 +42,6 @@ jobs:
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir
run: |
make compile
@@ -70,19 +66,19 @@ jobs:
cd ../elixir/
make docs
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
if: ${{ matrix.otp_latest }}
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["25.3", "26.2", "27.3"]
runs-on: windows-2022
otp_version: ["24", "25", "26.0"]
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
@@ -109,7 +105,7 @@ jobs:
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
with:
+3 -3
View File
@@ -10,7 +10,7 @@ permissions:
jobs:
notify:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v4
@@ -18,8 +18,8 @@ jobs:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+20 -308
View File
@@ -2,20 +2,16 @@ name: Release
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
contents: write
id-token: write
attestations: write
jobs:
create_draft_release:
@@ -24,7 +20,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
@@ -32,326 +27,43 @@ jobs:
--notes '' \
--draft \
${{ github.ref_name }}
- uses: actions/checkout@v4
if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
run: |
ref_name=${{ github.ref_name }}-latest
if ! gh release view $ref_name; then
gh release create \
--latest=false \
--title $ref_name \
--notes "Automated release for latest ${{ github.ref_name }}." \
$ref_name
fi
git tag $ref_name --force
git push origin $ref_name --force
build:
release_pre_built:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
include:
- otp: 24
otp_version: '24.3'
- otp: 25
otp_version: "25.3"
otp_version: '25.3'
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: "Attest docs provenance"
uses: actions/attest-build-provenance@v2
id: attest-docs-provenance
if: ${{ matrix.build_docs }}
with:
subject-path: "Docs.zip"
- name: "Copy docs provenance"
if: ${{ matrix.build_docs }}
run: cp "$ATTESTATION" Docs.zip.sigstore
env:
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}"
- name: Create Docs Hashes
if: ${{ matrix.build_docs }}
run: |
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@v4
if: ${{ matrix.build_docs }}
with:
name: Docs
path: Docs.zip*
sign:
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [25, 26, 27]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@v4
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: "Attest release provenance"
uses: actions/attest-build-provenance@v2
id: attest-provenance
with:
subject-path: ${{ env.RELEASE_FILE }}
- name: "Copy release .zip provenance"
shell: bash
run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore"
env:
ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}"
- name: Create Release Hashes
if: matrix.flavor == 'windows'
shell: pwsh
run: |
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
- name: Create Release Hashes
if: matrix.flavor == 'linux'
shell: bash
run: |
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
upload-release:
needs: [create_draft_release, build, sign]
runs-on: ubuntu-22.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Upload Pre-built
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
else
tag="${{ github.ref_name }}"
fi
gh release upload \
--repo ${{ github.repository }} \
--clobber \
"$tag" \
elixir-otp-*.zip \
elixir-otp-*.zip.sha{1,256}sum \
elixir-otp-*.zip.sigstore \
elixir-otp-*.exe \
elixir-otp-*.exe.sha{1,256}sum \
elixir-otp-*.exe.sigstore \
Docs.zip \
Docs.zip.sha{1,256}sum \
Docs.zip.sigstore
upload-builds-hex-pm:
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
OTP_GENERIC_VERSION: "25"
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Init purge keys file
run: |
touch purge_keys.txt
- name: Upload Precompiled to S3
run: |
ref_name=${{ github.ref_name }}
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
unzip Docs.zip
for f in doc/*; do
if [ -d "$f" ]; then
app=$(echo "$f" | sed s/"doc\/"//)
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${AWS_S3_BUCKET}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "${surrogate_key}" >> ../purge_keys.txt
fi
done
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
done
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
--cache-control "public,max-age=3600" \
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
echo 'builds/elixir/txt' >> purge_keys.txt
- name: Flush cache
if: github.repository == 'elixir-lang/elixir'
run: |
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
for key in $(cat purge_keys.txt); do
purge "${key}"
done
gh release upload --clobber "${{ github.ref_name }}" \
elixir-otp-${{ matrix.otp }}.zip \
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
elixir-otp-${{ matrix.otp }}.exe \
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
- name: Upload Docs to GitHub
if: ${{ matrix.build_docs }}
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
@@ -19,6 +19,8 @@ runs:
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
@@ -32,6 +34,8 @@ runs:
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
@@ -42,7 +46,7 @@ runs:
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
fi
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
- uses: actions/checkout@v4
- uses: actions/checkout@v3
if: ${{ inputs.build_docs }}
with:
repository: elixir-lang/ex_doc
@@ -60,5 +64,6 @@ runs:
if: ${{ inputs.build_docs }}
shell: bash
run: |
git fetch --tags
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
-5
View File
@@ -17,19 +17,14 @@
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
+171 -414
View File
@@ -1,505 +1,262 @@
# Changelog for Elixir v1.18
# Changelog for Elixir v1.16
Elixir v1.18 is an impressive release with improvements across the two main efforts happening within the Elixir ecosystem right now: set-theoretic types and language servers. It also comes with built-in JSON support and adds new capabilities to its unit testing library. Here is a quick break down.
## Code snippets in diagnostics
## Type system improvements
Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings).
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
```elixir
defmodule User do
defstruct [:age, :car_choice]
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
if car in car_choices do
{:ok, car}
else
{:error, :no_choice}
end
end
def drive(%User{}, _car_choices) do
{:error, :not_allowed}
end
end
```
Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument:
```elixir
User.drive({:ok, %User{}}, car_choices)
```
Here is the warning:
With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened:
```
warning: incompatible types given to User.drive/2:
User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)
given types:
{:ok, %User{age: nil, car_choice: nil}}, empty_list()
but expected one of:
dynamic(%User{age: term(), car_choice: term()}), dynamic()
where "car_choices" was given the type:
# type: empty_list()
# from: lib/foo.ex:21:17
car_choices = []
typing violation found at:
** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17:
error: syntax error before: '*'
│
22 │ User.drive({:ok, %User{}}, car_choices)
│ ~
1 │ [1, 2, 3, 4, 5, *]
│ ^
│
└─ lib/foo.ex:22:10: Example.run/0
└─ lib/my_app.ex:1:17
```
> The mismatched arguments are shown in red, if your terminal supports ANSI coloring.
And the next snippet will warn because the `:error` clause will never match, as that's not a valid return type of the `User.drive/2` call:
```elixir
case User.drive(user, car_choices) do
{:ok, car} -> car
:error -> Logger.error("User cannot drive")
end
```
And here is the warning:
For mismatched delimiters, it now shows both delimiters:
```
warning: the following clause will never match:
:error
because it attempts to match on the result of:
User.drive(user, car_choices)
which has type:
dynamic({:ok, term()} or {:error, :no_choice} or {:error, :not_allowed})
typing violation found at:
** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18:
error: unexpected token: )
│
26 │ :error -> Logger.error("User cannot drive")
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 │ [1, 2, 3, 4, 5, 6)
│ │ └ mismatched closing delimiter (expected "]")
│ └ unclosed delimiter
│
└─ lib/foo.ex:26: Example.run/0
└─ lib/my_app.ex:1:18
```
For more details on typing inference and the trade-offs made by the Elixir team, [see our official documentation](https://hexdocs.pm/elixir/1.18/gradual-set-theoretic-types.html#type-inference).
For unclosed delimiters, it now shows where the unclosed delimiter starts:
There are many other improvements to the type system, which we will go in detail within the official release. Meanwhile, here is a list summary of the overall improvements done to the type system:
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
* Type checking of all functions inlined by the compiler found in `Kernel`
* Type checking of all conversion functions inlined by the compiler
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
* Detection of unused clauses in private functions
## ExUnit improvements
ExUnit now supports parameterized tests to run the same test module multiple times under different parameters.
For example, Elixir ships a local, decentralized and scalable key-value process storage called `Registry`. The registry can be partitioned and its implementation differs depending if partitioning is enabled or not. Therefore, during tests, we want to ensure both modes are exercised. With Elixir v1.18, we can achieve this by writing:
```elixir
defmodule Registry.Test do
use ExUnit.Case,
async: true,
parameterize: [
%{partitions: 1},
%{partitions: 8}
]
# ... the actual tests ...
end
```
** (TokenMissingError) token missing on lib/my_app:8:23:
error: missing terminator: )
│
1 │ my_numbers = (1, 2, 3, 4, 5, 6
│ └ unclosed delimiter
...
8 │ IO.inspect(my_numbers)
│ └ missing closing delimiter (expected ")")
│
└─ lib/my_app:8:23
```
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables:
ExUnit also comes with the ability of specifying test groups. While ExUnit supports running tests concurrently, those tests must not have shared state between them. However, in large applications, it may be common for some tests to depend on some shared state, and other tests to depend on a completely separate state. For example, part of your tests may depend on Cassandra, while others depend on Redis. Prior to Elixir v1.18, these tests could not run concurrently, but in v1.18 they might as long as they are assigned to different groups. Tests modules within the same group do not run concurrently, but across groups, they might.
With features like async tests, suite partitioning, and now grouping, Elixir developers have plenty of flexibility to make the most use of their machine resources, both in development and in CI.
## `mix format --migrate`
The `mix format` command now supports an explicit `--migrate` flag, which will convert constructs that have been deprecated in Elixir to their latest version. Because this flag rewrites the AST, it is not guaranteed the migrated format will always be valid when used in combination with macros that also perform AST rewriting.
As of this release, the following migrations are executed:
* Normalize parens in bitstring modifiers - it removes unnecessary parentheses in known bitstring modifiers, for example `<<foo::binary()>>` becomes `<<foo::binary>>`, or adds parentheses for custom modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
* Charlists as sigils - formats charlists as `~c` sigils, for example `'foo'` becomes `~c"foo"`.
* `unless` as negated `if`s - rewrites `unless` expressions using `if` with a negated condition, for example `unless foo do` becomes `if !foo do`.
More migrations may be added in future releases.
## JSON support
This release includes official support for JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards.
### Encoding
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
| **Elixir** | **JSON** |
|-----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types.
If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
```elixir
@derive {JSON.Encoder, only: [...]}
defstruct ...
```
error: undefined variable "unknown_var"
│
5 │ a - unknown_var
│ ^^^^^^^^^^^
│
└─ lib/sample.ex:5:9: Sample.foo/1
```
### Decoding
Otherwise the whole line is underlined:
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
## Language server listeners
4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE.
## Potential incompatibilities
This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25).
Furthermore, in order to support inference of patterns, Elixir will raise if it finds recursive variable definitions. This means patterns that never match, such as this one, will no longer compile:
def foo(x = {:ok, y}, x = y)
However, recursion of root variables (where variables directly point to each other), will also fail to compile:
def foo(x = y, y = z, z = x)
While the definition above could succeed (as long as all three arguments are equal), the cycle is not necessary and could be removed, as below:
def foo(x = y, y = z, z)
You may also prefer to write using guards:
def foo(x, y, z) when x == y and y == z
## v1.18.5 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.18.4 (2025-05-21)
This release includes initial support for Erlang/OTP 28, for those who want to try it out. In such cases, you may use Elixir v1.18.4 precompiled for Erlang/OTP 27, as it is binary compatible with Erlang/OTP 28. Note, however, that Erlang/OTP 28 no longer allows regexes to be defined in the module body and interpolated into an attribute. If you do this:
```elixir
@some_attribute ~r/foo/
def some_fun, do: @some_attribute
```
error: function names should start with lowercase characters or underscore, invalid name CamelCase
│
3 │ def CamelCase do
│ ^^^^^^^^^^^^^^^^
│
└─ lib/sample.ex:3
```
You must rewrite it to:
A huge thank you to Vinícius Müller for working on the new diagnostics.
```elixir
def some_fun, do: ~r/foo/
```
## Revamped documentation
### 1. Enhancements
Elixir's Getting Started guides have been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references.
#### IEx
We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers examples of potential anti-patterns, with context and examples on how to improve their codebases.
* [IEx.Helpers] Add `IEx.Helpers.process_info/1` which prints process information
Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue.
#### Mix
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
* [mix compile] Support the `--no-listeners` option
* [mix local] Retry HTTP requests with disabled middlebox comp mode depending on the failure reason
* [mix local.hex] Install Hex per OTP release
* [mix local.rebar] Install Hex per OTP release
* [mix run] Support the `--no-listeners` option
## v1.16.3 (2024-05-21)
### 2. Bug fixes
### 1. Bug fixes
#### Elixir
* [Kernel] Emit trace events for `@on_definition` callbacks
* [Kernel] Emit trace events for `@on_load` callbacks
* [Kernel] Emit trace events for `super` calls
* [Kernel] Emit trace events for imported function calls
* [Kernel] Optimize map unions to avoid building long lists
* [Kernel] Do not crash when type checking nested bitstrings in patterns
* [Kernel] Do not crash when non-binary bitstring is given as struct default value
* [Kernel] Recompile regexes when escaped from module attributes for Erlang/OTP 28 compatibility
* [Kernel] Preserve backwards compatibility in `elixir_erl`
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
* [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when`
* [Kernel] Only infer size in pinned variable in binary strings when needed
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
#### Logger
* [Logger] Ensure translators are persisted across logger restarts
#### Mix
* [mix deps.get] Ensure git checkout works when there are untracked files in the dependency
* [mix loadpaths] Do not run listeners when not checking the deps
* [mix compile] Ensure compile paths are accessible during compilation
## v1.18.3 (2025-03-06)
## v1.16.2 (2024-03-10)
### 1. Enhancements
#### Elixir
* [JSON] Encode any JSON key to string
* [Kernel] Allow `<<_::3*8>>` in typespecs
* [Code] Emit `:defmodule` tracing event on module definition
#### Mix
* [mix loadpaths] Support `--no-listeners` option
* [Mix] Add `Mix.install_project_dir/0`
* [Mix] Add environment variable for reusing `Mix.install/2` installation
* [Mix.SCM] Add `Mix.SCM.delete/1`
### 2. Bug fixes
#### Elixir
* [CLI] Fix `--no-color` not setting `:ansi_enabled` to false
* [Protocol] Return correct implementation for an invalid struct pointing to `nil`
* [Stream] Do not raise when `Stream.cycle/1` is explicitly halted
#### ExUnit
* [ExUnit.Diff] Fix regression when diffing nested improper lists
* [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character
* [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input
#### IEx
* [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__`
* [IEx.Helpers] Do not purge on `recompile` if IEx is not running
* [IEx.Helpers] Discard mermaid fenced blocks from ansi docs
## v1.18.2 (2025-01-22)
#### ExUnit
* [ExUnit] Properly compared pinned values when building diffs
## v1.16.1 (2024-01-31)
### 1. Bug fixes
#### Elixir
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
* [String] Fix `String.capitalize/1` with a single codepoint
#### IEx
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
* [IEx] Do not assume `$HOME` is set
#### Mix
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
* [mix test] Properly resolve relative paths when running tests from individual files
* [mix test] Properly resolve Windows paths when running tests from individual files
## v1.16.0 (2023-12-22)
### 1. Enhancements
#### EEx
* [EEx] Include relative file information in diagnostics
#### Elixir
* [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors
* [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments
* [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine
* [Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
* [Code] Automatically include columns in parsing options
* [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
* [Code.Fragment] Handle anonymous calls in fragments
* [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
* [File] Add `:offset` option to `File.stream!/2`
* [Kernel] Auto infer size of matched variable in bitstrings
* [Kernel] Preserve column information when translating typespecs
* [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
* [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
* [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
* [Macro] Add `Macro.compile_apply/4`
* [Module] Add support for `@nifs` annotation from Erlang/OTP 25
* [Module] Add support for missing `@dialyzer` configuration
* [String] Update to Unicode 15.1.0
* [String] Add `String.replace_invalid/2`
* [Task] Add `:limit` option to `Task.yield_many/2`
#### Logger
* [Logger] Add `Logger.levels/0`
#### Mix
* [mix] Print compilation lock waiting message to stderr
* [mix] Add an environment variable to optionally disable compilation locking
* [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
* [mix archive.install] Support `--sparse` option
* [mix compile.app] Warn if both `:applications` and `:extra_applications` are used
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
* [mix escript.install] Support `--sparse` option
* [mix release] Include `include/` directory in releases
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
### 2. Bug fixes
#### Elixir
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
* [Code] Keep quotes for atom keys in formatter
* [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
* [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
* [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
* [Kernel] Ensure `dbg` module is a compile-time dependency
* [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
* [Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
* [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
* [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
* [Macro] Address exception on `Macro.to_string/1` for certain ASTs
* [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
* [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`
#### ExUnit
* [ExUnit] Fix crash when diffing bitstring specifiers
* [ExUnit] Raise on incorrectly dedented doctests
#### IEx
* [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values
* [IEx.Pry] Fix prying functions with only literals in their body
#### Mix
* [mix] Track compilation locks per user to avoid permission errors
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
## v1.18.1 (2024-12-24)
### 1. Enhancements
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
* [Kernel] Do not validate clauses of private overridable functions
### 2. Bug fixes
#### Elixir
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
* [Kernel] Ensure diagnostic traces are kept backwards compatible
#### ExUnit
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
#### Mix
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
* [mix deps.loadpaths] Add build lock
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
## v1.18.0 (2024-12-19)
### 1. Enhancements
#### Elixir
* [CLI] Add experimental PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows. Those provide a safer entry point for running Elixir from other platforms
* [Calendar] Add `Duration.to_string/1`
* [Code] Support several migration options in `Code.format_string!/2`
* [Code] Add parenthesis around `--` and `---` in `Code.format_string!/2` to make precedence clearer
* [Code] Include more metadata in `Code.string_to_quoted/2` when `token_metadata: true` to help compute ranges from the AST
* [Code.Fragment] Have `:capture_arg` as its own entry in `Code.Fragment.surround_context/2`
* [Config] Add `Config.read_config/1`
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
* [Exception] Add `MissingApplicationsError` exception to denote missing applications
* [JSON] Add a new `JSON` module with encoding and decoding functionality
* [JSON] Implement `JSON.Encoder` for all Calendar types
* [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well
* [Kernel] Warn on bidirectional confusability in identifiers
* [Kernel] Verify the type of the binary generators
* [Kernel] Track the type of tuples in patterns and inside `elem/2`
* [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier
* [Kernel] Add source, behaviour, and record information to Docs chunk metadata
* [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs
* [Kernel] Allow aliases and imports to be enabled conditionally in module body
* [List] Add `List.ends_with?/2`
* [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks
* [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)`
* [Registry] Add `Registry.lock/3` for local locking
* [PartitionSupervisor] Add `PartitionSupervisor.resize!/2` to resize the number of partitions in a supervisor (up to the limit it was started with)
* [Process] Handle arbitrarily high integer values in `Process.sleep/1`
* [Protocol] Add `@undefined_impl_description` to customize error message when an implementation is undefined
* [Protocol] Add `__deriving__/1` as optional macro callback to `Protocol`, no longer requiring empty implementations
* [String] Inspect special whitespace and zero-width characters using their Unicode representation
* [String] Update Unicode to 16.0
#### ExUnit
* [ExUnit] Support parameterized tests on `ExUnit.Case`
* [ExUnit] Support test groups: tests in the same group never run concurrently
* [ExUnit.Case] Add `test_pid` as a tag
#### IEx
* [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes
* [IEx] Add `:dot_iex` support to `IEx.configure/1`
* [IEx] Add report for normal/shutdown exits in IEx
#### Mix
* [mix compile] Ensure only a single operating system process can compile at a given time
* [mix deps.get] Ensure only a single operating system process can fetch deps at a given time
* [mix format] Add `mix format --migrate` to migrate from deprecated functionality
* [mix format] Add new options and metadata to improve formatting applying by editors and other environments
* [mix test] Taint failure manifest if requiring or compiling tests fail
* [Mix.Project] Add a `:listeners` configuration to listen to compilation events from the current and other operating system processes
* [Mix.Task.Compiler] Add API for fetching all persisted compiler diagnostics
* [Mix.Task.Compiler] Add API for fetching all compiler tasks
### 2. Bug fixes
#### Elixir
* [Code] Fix delimiter metadata for single quoted atoms and remote calls in `Code.string_to_quoted/2`
* [Code.Formatter] Fix formatter adding extra escapes to quoted remote calls
* [Code.Fragment] Properly handle keyword keys as their own entry
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length`
* [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler
* [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation
* [Module] Include module attribute line and name when tracing its aliases
* [Stream] Do not halt streams twice in `Stream.transform/5`
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
#### ExUnit
* [ExUnit.Assertions] Raise if guards are used in `assert/1` with `=`
* [ExUnit.Assertions] Format inserted/deleted maps in list assertions
#### IEx
* [IEx.Helpers] `IEx.Helpers.recompile/0` will reload modules changed by other operating system processes
#### Mix
* [mix compile] Ensure warnings from external resources are emitted with `--all-warnings` when files do not change
* [mix deps.compile] Fix escaping issues when invoking `rebar3` in some cases
* [mix escript] Fix escript layout and support storing `priv` directories
* [mix release] Make `.app` files deterministic in releases
* [Mix.Shell] Fix `Mix.Shell` on Windows when outputting non UTF-8 characters
* [mix archive.install] Restore code paths after `mix archive.install`
* [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
* [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
* [mix escript.install] Restore code paths after `mix escript.install`
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `color/3` is deprecated in favor of `color_doc/3`
* [Inspect.Algebra] `fold_doc/2` is deprecated in favor of `fold/2`
* [Kernel] Deprecate `unless` in favor of `if`. Use `mix format --migrate` to automate the migration
* [Macro] `Macro.struct!/2` is deprecated in favor of `Macro.struct_info!/2`
* [Protocol] Defining `__deriving__/3` inside the `Any` implementation is deprecated, derive it inside the protocol definition itself
### 4. Hard deprecations
#### EEx
* [EEx] `<%#` is deprecated in favor of `<%!--` or `<% #`
* [EEx] `c:EEx.handle_text/2` is deprecated in favor of `c:EEx.handle_text/3`
#### Elixir
* [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module
* [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1`
* [List] `List.zip/1` is deprecated in favor of `Enum.zip/1`
* [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3`
* [Range] Deprecate inferring negative ranges on `Range.new/2`
* [Tuple] `Tuple.append/2` is deprecated, use `Tuple.insert_at/3` instead
* [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
* [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
* [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`
#### Mix
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP`
* [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]`
* [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]`
* [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
* [mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`
### 5. Potential breaking changes
### 4. Hard deprecations
#### Elixir
* [Kernel] Using unquote is deprecated when quote is used inside a pattern or guard
* [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
* [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
* [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
* [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead
## v1.17
#### ExUnit
The CHANGELOG for v1.17 releases can be found [in the v1.17 branch](https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md).
* [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead
#### Mix
* [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
* [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler
## v1.15
The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md).
+35 -31
View File
@@ -5,7 +5,7 @@ MAN_PREFIX ?= $(SHARE_PREFIX)/man
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -17,19 +17,19 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.NOTPARALLEL:
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 25)])' -s erlang halt | grep -q '^true'; \
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 25.0 is required to build Elixir"; \
echo "At least Erlang/OTP 24.0 is required to build Elixir"; \
exit 1; \
fi
endef
@@ -65,21 +65,17 @@ endef
#==> Compilation tasks
APP := lib/elixir/ebin/elixir.app
EEX := lib/eex/ebin/Elixir.EEx.beam
ELIXIR := lib/elixir/ebin/elixir.beam
PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile
compile: erlang elixir
compile: erlang $(APP) elixir
erlang: $(ELIXIR)
$(ELIXIR): $(PARSER) lib/elixir/src/*
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(Q) $(GENERATE_APP) $(VERSION)
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -87,27 +83,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then Mix, and then compile EEx fully
elixir: stdlib $(EEX) mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
"$(MAKE)" unicode; \
fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) "$(MAKE)" app
app: $(APP)
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)";
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -146,7 +144,6 @@ check_reproducible: compile
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
$(Q) rm -rf lib/*/ebin
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
$(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
@@ -157,10 +154,16 @@ check_reproducible: compile
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean: clean_man
clean:
rm -rf ebin
rm -rf lib/*/ebin
rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
@@ -170,14 +173,13 @@ clean: clean_man
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
$(Q) "$(MAKE)" clean_man
#==> Documentation tasks
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
@@ -304,15 +306,17 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1
define BUILD_MANPAGES
man/$(APP).1:
$(Q) cp man/$(APP).1.in man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
$(Q) rm -f man/$(APP).1.bak
endef
man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
clean_man:
rm -f man/elixir.1
+1 -2
View File
@@ -3,10 +3,9 @@ LEGAL NOTICE INFORMATION
All the files in this distribution are copyright to the terms below.
== lib/elixir/src/elixir_json.erl
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
Copyright Ericsson AB 1996-2024
Copyright Ericsson AB 1996-2015
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+35 -48
View File
@@ -1,11 +1,7 @@
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture>
</h1>
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
@@ -16,7 +12,7 @@ For more about Elixir, installation and documentation,
## Policies
New releases are announced in the [announcement mailing list][8].
You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com>
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
and replying to the confirmation email.
All security releases [will be tagged with `[security]`][10]. For more
@@ -29,28 +25,28 @@ All interactions in our official communication channels follow our
For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities
privately at <elixir-security@googlegroups.com>**.
privately at elixir-security@googlegroups.com**.
## Issues tracker management
All currently open bugs related to the Elixir repository are listed
in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short and
medium term. We also do our best to label entries for clarity and to ease
on *actionable items*, including planned enhancements in the short- and
medium-term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as:
* Proposing new features as well as requests for support, help, and
* Proposing new features as well as request for support, help, and
guidance must be done in their own spaces, detailed next.
* Issues we have identified to be outside of Elixir's scope,
such as an upstream bug, will be closed (and requested to be moved
* Issues where we have identified to be outside of Elixir scope,
such as a bug upstream, will be closed (and requested to be moved
elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. We may get things wrong from time to
time and will gladly revisit issues, reopening when necessary.
issues tracker tidy. However, we may get things wrong from time to
time, so we are glad to revisit issues and reopen if necessary.
Keep the tone positive and be kind! For more information, see the
[Code of Conduct][1].
@@ -76,7 +72,7 @@ in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use the community
For general discussions, support, and help, please use many of the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
@@ -103,35 +99,33 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
When updating the repository, you may want to run `make clean` before
recompiling. For deterministic builds, you should set the environment
variable `ERL_COMPILER_OPTIONS=deterministic`.
Additionally, you may choose to run the test suite with `make clean test`.
## Contributing
We invite contributions to Elixir. To contribute, there are a few
We welcome everyone to contribute to Elixir. To do so, there are a few
things you need to know about the code. First, Elixir code is divided
by each application inside the `lib` folder:
in applications inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
If you are changing just one file, you can choose to compile and run tests only
for that particular file for fast development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
@@ -139,27 +133,20 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
Some test files need their `test_helper.exs` to be explicitly required
before, such as:
```sh
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile all (including Erlang modules):
To recompile (including Erlang modules):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with
all files are properly formatted and then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
@@ -169,7 +156,7 @@ you can rebuild the language from scratch with:
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
Similarly, if you can't get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
@@ -179,9 +166,9 @@ With tests running and passing, you are ready to contribute to Elixir and
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
@@ -189,7 +176,7 @@ Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
the repository. If any changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
@@ -209,8 +196,8 @@ a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
to be installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
+7 -9
View File
@@ -2,23 +2,21 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
3. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION" and push it
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
5. Once GitHub actions completes, generate a new tag, and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
6. Wait until GitHub Actions publish artifacts to the draft release
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (after first rc)
### In the new branch
@@ -30,7 +28,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
2. Start new /CHANGELOG.md
+3 -3
View File
@@ -6,11 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.16 | Security patches only
1.16 | Bug fixes and security patches
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
1.12 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.18.5
1.16.3
+8 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.18.5
ELIXIR_VERSION=1.16.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -18,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--color, --no-color Enables or disables ANSI coloring
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone)
--werl Uses Erlang's Windows shell GUI (Windows only)
Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
@@ -112,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="iex"
;;
-v|--no-halt|--color|--no-color)
-v|--no-halt)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -182,6 +182,10 @@ while [ $I -le $LENGTH ]; do
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi
;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
;;
*)
while [ $I -le $LENGTH ]; do
I=$((I + 1))
@@ -214,7 +218,6 @@ SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
@@ -225,7 +228,7 @@ fi
ERTS_BIN=
ERTS_BIN="$ERTS_BIN"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"
if [ -n "$RUN_ERL_PIPE" ]; then
ESCAPED=""
+70 -28
View File
@@ -1,7 +1,8 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.18.5
set ELIXIR_VERSION=1.16.3
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
@@ -24,13 +25,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
@@ -63,11 +64,13 @@ echo ** Standalone options can't be combined with other options.
goto end
:shortversion
echo %ELIXIR_VERSION%
echo !ELIXIR_VERSION!
goto end
:parseopts
setlocal enabledelayedexpansion
rem Parameters for Elixir
set parsElixir=
rem Parameters for Erlang
set parsErlang=
@@ -94,25 +97,48 @@ if "!par!"=="" (
)
shift
set par="!par:"=\"!"
if !endLoop! == 1 (
set parsElixir=!parsElixir! !par!
goto startloop
)
rem ******* EXECUTION OPTIONS **********************
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set useIEx=1 && goto startloop)
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && goto startloop)
rem ******* EVAL PARAMETERS ************************
if ""==!par:-e=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--eval=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--rpc-eval=! (
set "VAR=%~2"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
shift
shift
goto startloop
)
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-e=! (shift && goto startloop)
if ""==!par:--eval=! (shift && goto startloop)
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
if ""==!par:-r=! (shift && goto startloop)
if ""==!par:-pr=! (shift && goto startloop)
if ""==!par:-pa=! (shift && goto startloop)
if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %~1" && shift && goto startloop)
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %~1" && shift && goto startloop)
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %~1" && shift && goto startloop)
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %~1" && shift && goto startloop)
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && goto startloop)
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
@@ -126,19 +152,35 @@ if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1""
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
set endLoop=1
set parsElixir=!parsElixir! !par!
goto startloop
:run
setlocal disabledelayedexpansion
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
)
if defined useIEx (
set beforeExtra=-s elixir start_iex !beforeExtra!
) else (
set beforeExtra=-s elixir start_cli !beforeExtra!
)
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra!
if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
) else (
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
)
exit /B %ERRORLEVEL%
:end
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
+5 -2
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
@@ -16,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
goto end
:run
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
+1 -1
View File
@@ -1,2 +1,2 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
Executable → Regular
+1 -1
View File
@@ -20,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
}
# Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs
& $mixBatPath $newArgs
+2 -2
View File
@@ -317,8 +317,8 @@ defmodule EEx do
## Examples
iex> EEx.tokenize(~c"foo", line: 1, column: 1)
{:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
iex> EEx.tokenize('foo', line: 1, column: 1)
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
## Result
+14 -27
View File
@@ -48,14 +48,8 @@ defmodule EEx.Compiler do
end
end
# TODO: Remove me on Elixir v2.0
# TODO: Deprecate this on Elixir v1.18
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
line: line,
column: column,
file: state.file
)
case expr(t, line, column + 3, state, []) do
{:error, message} ->
{:error, message, %{line: line, column: column}}
@@ -77,10 +71,10 @@ defmodule EEx.Compiler do
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} ->
{:ok, _line, _column, _warnings, tokens} ->
# We ignore warnings because the code will be tokenized
# again later with the right line+column info
token_key(rev_tokens, expr)
token_key(tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
@@ -170,8 +164,8 @@ defmodule EEx.Compiler do
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
defp token_key(tokens, expr) do
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
@@ -305,14 +299,6 @@ defmodule EEx.Compiler do
}
init = state.engine.init(opts)
if function_exported?(state.engine, :handle_text, 2) and
not function_exported?(state.engine, :handle_text, 3) do
IO.warn(
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
)
end
generate_buffer(tokens, init, [], state)
end
@@ -330,7 +316,8 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
@@ -353,6 +340,13 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -365,13 +359,6 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
+9
View File
@@ -43,6 +43,15 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
+54 -12
View File
@@ -126,21 +126,51 @@ defmodule EEx.TokenizerTest do
end
test "EEx comments" do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end)
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end
test "EEx comments with do-end" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 31, line: 1}},
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
{:text, ~c"bar", %{column: 46, line: 1}},
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
{:ok, exprs}
end
test "EEx multi-line comments" do
@@ -291,6 +321,15 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, ~c"\n123", %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do
exprs = [
{:comment, ~c" comment ", %{column: 3, line: 1}},
@@ -345,6 +384,9 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
{:error, message, %{column: 5, line: 1}}
assert EEx.tokenize(~c"<%# true ", @opts) ==
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
message = """
expected closing '--%>' for EEx expression
|
+9 -10
View File
@@ -198,6 +198,15 @@ defmodule EExTest do
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
end
test "embedded code with comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
assert_eval(
"foo\n\nbar\n",
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
)
end
test "embedded code with multi-line comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
@@ -534,16 +543,6 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
+2 -2
View File
@@ -9,8 +9,8 @@
warn_deprecated_function,
warn_obsolete_guard,
warn_exported_vars,
%% Enable this when we require Erlang/OTP 27+
%% warnings_as_errors,
%% warn_missing_spec,
%% warn_untyped_record,
debug_info,
{outdir, "ebin/"}
]}.
+37 -126
View File
@@ -35,74 +35,61 @@ defmodule Access do
iex> nil[:a]
nil
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
to allow values to be set in nested data structures:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
## Maps and structs
While the access syntax is allowed in maps via `map[key]`,
if your map is made of predefined atom keys, you should prefer
to access those atom keys with `map.key` instead of `map[key]`,
as `map.key` will raise if the key is missing (which is not
supposed to happen if the keys are predefined) or if `map` is
`nil`.
supposed to happen if the keys are predefined).
Similarly, since structs are maps and structs have predefined
keys, they only allow the `struct.key` syntax and they do not
allow the `struct[key]` access syntax.
allow the `struct[key]` access syntax. `Access.key/1` can also
be used to construct dynamic access to structs and maps.
In other words, the `map[key]` syntax is loose, returning `nil`
for missing keys, while the `map.key` syntax is strict, raising
for both nil values and missing keys.
In a nutshell, when using `put_in/2` and friends:
To bridge this gap, Elixir provides the `get_in/1` and `get_in/2`
functions, which are capable of traversing nested data structures,
even in the presence of `nil`s:
put_in(struct_or_map.key, :value)
put_in(keyword_or_map[:key], :value)
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users["john"].age)
27
iex> get_in(users["unknown"].age)
nil
When using `put_in/3` and friends:
Notice how, even if no user was found, `get_in/1` returned `nil`.
Outside of `get_in/1`, trying to access the field `.age` on `nil`
would raise.
put_in(struct_or_map, [Access.key!(:key)], :value)
put_in(keyword_or_map, [:key], :value)
The `get_in/2` function takes one step further by allowing
different accessors to be mixed in. For example, given a user
map with the `:name` and `:languages` keys, here is how to
access the name of all programming languages:
This covers the dual nature of maps in Elixir, as they can be
either for structured data or as a key-value store. See the `Map`
module for more information.
iex> languages = [
...> %{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}
...> ]
iex> user = %{name: "john", languages: languages}
iex> get_in(user, [:languages, Access.all(), :name])
["elixir", "c"]
## Nested data structures
This module provides convenience functions for traversing other
structures, like tuples and lists. As we will see next, they can
even be used to update nested data structures.
Both key-based access syntaxes can be used with the nested update
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3`.
If you want to learn more about the dual nature of maps in Elixir,
as they can be either for structured data or as a key-value store,
see the `Map` module.
## Updating nested data structures
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1`
macros to further manipulate values in nested data structures:
For example, to update a map inside another map:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"].age, 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
As shown in the previous section, you can also use the
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3` functions to provide nested
custom accessors. For instance, given a user map with the
`:name` and `:languages` keys, here is how to deeply traverse
the map and convert all language names to uppercase:
This module provides convenience functions for traversing other
structures, like tuples and lists. These functions can be used
in all the `Access`-related functions and macros in `Kernel`.
For instance, given a user map with the `:name` and `:languages`
keys, here is how to deeply traverse the map and convert all
language names to uppercase:
iex> languages = [
...> %{name: "elixir", type: :functional},
@@ -350,7 +337,7 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must access a list element by index, \
you can use Enum.at/2 or the functions in the List module\
you can Enum.at/1 or the functions in the List module\
"""
end
@@ -418,8 +405,8 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must modify a list element by index, \
you can use Access.at/1 or the functions in the List module\
as a whole. If you really must mostify a list element by index, \
you can Access.at/1 or the functions in the List module\
"""
end
@@ -565,7 +552,7 @@ defmodule Access do
{"john", %{user: %{name: "JOHN"}}}
However, it is not possible to remove fields using the dot notation,
as it is implied those fields must also be present. In any case,
as it is implified those fields must also be present. In any case,
`Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically.
@@ -1026,80 +1013,4 @@ defmodule Access do
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
@doc ~S"""
Returns a function that accesses the first element of a list that matches the provided predicate.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
"francine"
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
`find/1` can also be used to pop the first found element out of a list or
a key inside of a list:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
nil
iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
** (RuntimeError) Access.find/1 expected a list, got: %{}
"""
@doc since: "1.17.0"
@spec find((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: term)
def find(predicate) when is_function(predicate, 1) do
fn op, data, next -> find(op, data, predicate, next) end
end
defp find(:get, data, predicate, next) when is_list(data) do
data |> Enum.find(predicate) |> next.()
end
defp find(:get_and_update, data, predicate, next) when is_list(data) do
get_and_update_find(data, [], predicate, next)
end
defp find(_op, data, _predicate, _next) do
raise "Access.find/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_find([], updates, _predicate, _next) do
{nil, :lists.reverse(updates)}
end
defp get_and_update_find([head | rest], updates, predicate, next) do
if predicate.(head) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
else
get_and_update_find(rest, [head | updates], predicate, next)
end
end
end
+5 -5
View File
@@ -207,7 +207,7 @@ defmodule Agent do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -284,7 +284,7 @@ defmodule Agent do
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
"""
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options)
end
@@ -311,7 +311,7 @@ defmodule Agent do
See `start_link/4` for more information.
"""
@spec start(module, atom, [term], GenServer.options()) :: on_start
@spec start(module, atom, [any], GenServer.options()) :: on_start
def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options)
end
@@ -348,7 +348,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get(agent, module, atom, [term], timeout) :: term
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
end
@@ -389,7 +389,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: term
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end
+23 -39
View File
@@ -13,7 +13,7 @@ defmodule Application do
Developers typically interact with the application environment and its
callback module. Therefore those will be the topics we will cover first
before jumping into details about the application resource file and life cycle.
before jumping into details about the application resource file and life-cycle.
## The application environment
@@ -54,12 +54,12 @@ defmodule Application do
You can also change the application environment dynamically by using functions
such as `put_env/3` and `delete_env/2`.
> #### Application environment in libraries {: .info}
> #### Environment in libraries {: .tip}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
> The config files `config/config.exs` and `config/runtime.exs`
> are rarely used by libraries. Libraries typically define their environment
> in the `application/0` function of their `mix.exs`. Configuration files
> are rather used by applications to configure their libraries.
> #### Reading the environment of other applications {: .warning}
>
@@ -258,8 +258,8 @@ defmodule Application do
Application.stop(:ex_unit)
#=> :ok
Stopping an application without a callback module defined, is in practice a
no-op, except for some system tracing.
Stopping an application without a callback module is defined, but except for
some system tracing, it is in practice a no-op.
Stopping an application with a callback module has three steps:
@@ -277,7 +277,7 @@ defmodule Application do
invoked only after termination of the whole supervision tree.
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
will shut down every application in the reverse order they were started.
will shut down every application in the opposite order they had been started.
By default, a SIGTERM from the operating system will automatically translate to
`System.stop/0`. You can also have more explicit control over operating system
@@ -673,6 +673,13 @@ defmodule Application do
> You must use this function to read only your own application
> environment. Do not read the environment of other applications.
> #### Application environment in libraries {: .info}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read our [library guidelines](library-guidelines.md).
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
@@ -789,12 +796,6 @@ defmodule Application do
@doc """
Puts the `value` in `key` for the given `app`.
> #### Compile environment {: .warning}
>
> Do not use this function to change environment variables read
> via `Application.compile_env/2`. The compile environment must
> be exclusively set before compilation, in your config files.
## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
@@ -816,29 +817,16 @@ defmodule Application do
end
@doc """
Puts the environment for multiple applications at the same time.
Puts the environment for multiple apps at the same time.
The given config should not:
* have the same application listed more than once
* have the same key inside the same application listed more than once
If those conditions are not met, this function will raise.
This function receives the same options as `put_env/4`. Returns `:ok`.
## Examples
Application.put_all_env(
my_app: [
key: :value,
another_key: :another_value
],
another_app: [
key: :value
]
)
If those conditions are not met, it will raise.
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@doc since: "1.9.0"
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
@@ -862,10 +850,7 @@ defmodule Application do
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
defp maybe_warn_on_app_env_key(app, key) do
message = fn ->
"passing non-atom as application env key is deprecated, got: #{inspect(key)}"
end
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
end
@@ -904,12 +889,11 @@ defmodule Application do
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:type` - if the application should be started in `:permanent`,
`:temporary`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`). This option requires
Erlang/OTP 26+.
or concurrently. This option requires Erlang/OTP 26+.
"""
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
+1 -1
View File
@@ -67,7 +67,7 @@ defmodule Atom do
## Examples
iex> Atom.to_charlist(:"An atom")
~c"An atom"
'An atom'
"""
@spec to_charlist(atom) :: charlist
+1 -2
View File
@@ -9,8 +9,7 @@ defmodule Behaviour do
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
is documented in `Module`.
`MyModule.behaviour_info(:callbacks)` can be used.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
+1 -1
View File
@@ -2,7 +2,7 @@ defmodule Bitwise do
@moduledoc """
A set of functions that perform calculations on bits.
All bitwise functions work only on integers, otherwise an
All bitwise functions work only on integers; otherwise an
`ArithmeticError` is raised. The functions `band/2`,
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
+3 -31
View File
@@ -70,14 +70,14 @@ defmodule Calendar do
@type zone_abbr :: String.t()
@typedoc """
The time zone UTC offset in ISO seconds for standard time.
The time zone UTC offset in seconds for standard time.
See also `t:std_offset/0`.
"""
@type utc_offset :: integer
@typedoc """
The time zone standard offset in ISO seconds (typically not zero in summer times).
The time zone standard offset in seconds (typically not zero in summer times).
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
"""
@@ -338,34 +338,6 @@ defmodule Calendar do
@doc since: "1.15.0"
@callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """
Shifts date by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """
Shifts naive datetime by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Shifts time by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
# General Helpers
@doc """
@@ -733,7 +705,7 @@ defmodule Calendar do
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
+15 -93
View File
@@ -40,7 +40,7 @@ defmodule Date do
## Using epochs
The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates
The `add/2` and `diff/2` functions can be used for computing dates
or retrieving the number of days between instants. For example, if there
is an interest in computing the number of days from the Unix epoch
(1970-01-01):
@@ -51,9 +51,6 @@ defmodule Date do
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
~D[2010-04-17]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01).
"""
@@ -690,8 +687,6 @@ defmodule Date do
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
## Examples
iex> Date.add(~D[2000-01-03], -2)
@@ -708,7 +703,12 @@ defmodule Date do
@spec add(Calendar.date(), integer()) :: t
def add(%{calendar: Calendar.ISO} = date, days) do
%{year: year, month: month, day: day} = date
{year, month, day} = Calendar.ISO.shift_days({year, month, day}, days)
{year, month, day} =
Calendar.ISO.date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> Calendar.ISO.date_from_iso_days()
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
@@ -757,81 +757,6 @@ defmodule Date do
end
end
@doc """
Shifts given `date` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months and then days:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
When shifting by month, days are rounded down to the nearest valid date.
Raises an `ArgumentError` when called with time scale units.
## Examples
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
iex> Date.shift(~D[2016-01-30], month: -1)
~D[2015-12-30]
iex> Date.shift(~D[2016-01-31], year: 4, day: 1)
~D[2020-02-01]
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
# leap years
iex> Date.shift(~D[2024-02-29], year: 1)
~D[2025-02-28]
iex> Date.shift(~D[2024-02-29], year: 4)
~D[2028-02-29]
# rounding down
iex> Date.shift(~D[2015-01-31], month: 1)
~D[2015-02-28]
"""
@doc since: "1.17.0"
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
def shift(%{calendar: calendar} = date, duration) do
%{year: year, month: month, day: day} = date
{year, month, day} = calendar.shift_date(year, month, day, __duration__!(duration))
%Date{calendar: calendar, year: year, month: month, day: day}
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({unit, _value})
when unit in [:hour, :minute, :second, :microsecond] do
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc false
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
@@ -1073,7 +998,13 @@ defmodule Date do
def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.year_of_era(year, month, day)
# TODO: Remove me on 1.17
# The behaviour implementation already warns on missing callback.
if function_exported?(calendar, :year_of_era, 3) do
calendar.year_of_era(year, month, day)
else
calendar.year_of_era(year)
end
end
@doc """
@@ -1152,17 +1083,8 @@ defmodule Date do
end
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar != Calendar.ISO or year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, #{inspect(calendar)})"
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+20 -188
View File
@@ -28,12 +28,7 @@ defmodule DateTime do
## Time zone database
Many functions in this module require a time zone database.
A time zone database is a record of the UTC offsets that its locales have
used at various times in the past, are using, and are expected to use in the
future.
Because those plans can change, it needs to be periodically updated.
By default, `DateTime` uses the default time zone database returned by
By default, it uses the default time zone database returned by
`Calendar.get_time_zone_database/0`, which defaults to
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
datetimes and returns `{:error, :utc_only_time_zone_database}`
@@ -48,7 +43,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices
To use one of them, first make sure it is added as a dependency in `mix.exs`.
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
@@ -65,7 +60,7 @@ defmodule DateTime do
a date and time at a given time zone". To understand precisely
what we mean, let's see an example.
Imagine someone in Poland who wants to schedule a meeting with someone
Imagine someone in Poland wants to schedule a meeting with someone
in Brazil in the next year. The meeting will happen at 2:30 AM
in the Polish time zone. At what time will the meeting happen in
Brazil?
@@ -88,14 +83,14 @@ defmodule DateTime do
not a problem, because time zone rules do not change for past
events.
To make matters worse, it may be that 2:30 AM in Polish time
To make matters worse, it may be that the 2:30 AM in Polish time
does not actually even exist or it is ambiguous. If a certain
time zone observes "Daylight Saving Time", they will move their
clock forward once a year. When this happens, there is a whole
hour that does not exist. Then, when they move the clock back,
there is a certain hour that will happen twice. So if you want to
schedule a meeting when this shift back happens, you would need to
explicitly say which occurrence of 2:30 AM you mean: the one in
explicitly say which occurence of 2:30 AM you mean: the one in
"Summer Time", which occurs before the shift, or the one
in "Standard Time", which occurs after it. Applications that are
date and time sensitive need to take these scenarios into account
@@ -175,9 +170,6 @@ defmodule DateTime do
truncate the resulting datetime. This is available
since v1.15.0.
The default unit if none gets passed is `:native`,
which results on a default resolution of microseconds.
## Examples
iex> datetime = DateTime.utc_now()
@@ -902,7 +894,7 @@ defmodule DateTime do
-17412508655
"""
@spec to_unix(Calendar.datetime(), :native | System.time_unit()) :: integer
@spec to_unix(Calendar.datetime(), System.time_unit()) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
@@ -1030,12 +1022,9 @@ defmodule DateTime do
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting datetimes which are in the ISO calendar,
attempting to convert datetimes from other calendars will raise.
You can also optionally specify an offset for the formatted string.
If none is given, the one in the given `datetime` is used.
Only supports converting datetimes which are in the ISO calendar.
If another calendar is given, it is automatically converted to ISO.
It raises if not possible.
WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such
@@ -1349,11 +1338,6 @@ defmodule DateTime do
@doc """
Converts the given `datetime` to a string according to its calendar.
Unfortunately, there is no standard that specifies rendering of a
datetime with its complete time zone information, so Elixir uses a
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
@@ -1510,11 +1494,6 @@ defmodule DateTime do
## Examples
iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z])
10
This function also considers timezone offsets:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
@@ -1626,8 +1605,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1656,28 +1633,30 @@ defmodule DateTime do
add(datetime, amount_to_add * 60, :second, time_zone_database)
end
def add(%{calendar: calendar} = datetime, amount_to_add, unit, time_zone_database)
when is_integer(amount_to_add) do
def add(datetime, amount_to_add, unit, time_zone_database) when is_integer(amount_to_add) do
%{
microsecond: {_, precision},
time_zone: time_zone,
utc_offset: utc_offset,
std_offset: std_offset
std_offset: std_offset,
calendar: calendar,
microsecond: {_, precision}
} = datetime
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
result =
datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
# Subtract total offset in order to get UTC and add the integer for the addition
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add - total_offset, ppd)
|> shift_zone_for_iso_days_utc(calendar, precision, datetime.time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
@@ -1690,152 +1669,6 @@ defmodule DateTime do
end
end
@doc """
Shifts given `datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen")
DateTime.shift(dt, hour: 1)
#=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles")
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2)
~U[2016-03-01 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4)
~U[2017-01-29 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25)
~U[2015-12-31 23:35:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4})
~U[2016-01-01 00:05:00.0005Z]
# leap years
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1)
~U[2025-02-28 00:00:00Z]
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4)
~U[2028-02-29 00:00:00Z]
# rounding down
iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1)
~U[2015-02-28 00:00:00Z]
"""
@doc since: "1.17.0"
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: std_offset,
utc_offset: utc_offset,
time_zone: time_zone
} = datetime
{year, month, day, hour, minute, second, {_, precision} = microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
result =
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
result_datetime
{:error, error} ->
raise ArgumentError,
"cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <>
"database #{inspect(time_zone_database)}), reason: #{inspect(error)}"
end
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given datetime with the microsecond field truncated to the given
precision (`:microsecond`, `:millisecond` or `:second`).
@@ -2092,8 +1925,7 @@ defmodule DateTime do
)
case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
when calendar != Calendar.ISO or year in -9999..9999 ->
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
"~U[" <> formatted <> suffix(calendar) <> "]"
_ ->
-542
View File
@@ -1,542 +0,0 @@
defmodule Duration do
@moduledoc """
Struct and functions for handling durations.
A `Duration` struct represents a collection of time scale units,
allowing for manipulation and calculation of durations.
Date and time scale units are represented as integers, allowing for
both positive and negative values.
Microseconds are represented using a tuple `{microsecond, precision}`.
This ensures compatibility with other calendar types implementing time,
such as `Time`, `DateTime`, and `NaiveDateTime`.
## Shifting
The most common use of durations in Elixir's standard library is to
"shift" the calendar types.
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
In the example above, `Date.shift/2` automatically converts the units
into a `Duration` struct, although one can also be given directly:
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
It is important to note that shifting is not an arithmetic operation.
For example, adding `date + 1 month + 1 month` does not yield the same
result as `date + 2 months`. Let's see an example:
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]
As you can see above, the results differ, which explains why operations
with durations are called "shift" rather than "add". This happens because,
once we add one month to `2016-01-31`, we get `2016-02-29`. Then adding
one extra month gives us `2016-03-29` instead of `2016-03-31`.
In particular, when applying durations to `Calendar.ISO` types:
* larger units (such as years and months) are applied before
smaller ones (such as weeks, hours, days, and so on)
* units are collapsed into months (`:year` and `:month`),
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
and microseconds (`:microsecond`) before they are applied
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
Therefore, 4 weeks _are not_ equivalent to 1 month
* in case of non-existing dates, the results are rounded down to the
nearest valid date
As the `shift/2` functions are calendar aware, they are guaranteed to return
valid date/times, considering leap years as well as DST in applicable time zones.
## Intervals
Durations in Elixir can be combined with stream operations to build intervals.
For example, to retrieve the next three Wednesdays starting from 17th April, 2024:
iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]
However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what
you to achieve. Compare the results of both examples below:
# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]
# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]
The second example consistently points to the last day of the month,
as it performs operations on the duration, rather than shifting date
after date.
"""
@moduledoc since: "1.17.0"
@derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]}
defstruct year: 0,
month: 0,
week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
@typedoc """
The duration struct type.
"""
@type t :: %Duration{
year: integer,
month: integer,
week: integer,
day: integer,
hour: integer,
minute: integer,
second: integer,
microsecond: {integer, 0..6}
}
@typedoc """
The unit pair type specifies a pair of a valid duration unit key and value.
"""
@type unit_pair ::
{:year, integer}
| {:month, integer}
| {:week, integer}
| {:day, integer}
| {:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
@typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
"""
@type duration :: t | [unit_pair]
@microseconds_per_second 1_000_000
@doc """
Creates a new `Duration` struct from given `unit_pairs`.
Raises an `ArgumentError` when called with invalid unit pairs.
## Examples
iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
%Duration{year: 1, week: 3, hour: 4, second: 1}
iex> Duration.new!(second: 1, microsecond: {1000, 6})
%Duration{second: 1, microsecond: {1000, 6}}
iex> Duration.new!(month: 2)
%Duration{month: 2}
"""
@spec new!(duration()) :: t
def new!(%Duration{} = duration) do
duration
end
def new!(unit_pairs) do
Enum.each(unit_pairs, &validate_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_unit!({unit, _value})
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
end
defp validate_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}
"""
@spec add(t, t) :: t
def add(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year + d2.year,
month: d1.month + d2.month,
week: d1.week + d2.week,
day: d1.day + d2.day,
hour: d1.hour + d2.hour,
minute: d1.minute + d2.minute,
second: d1.second + d2.second,
microsecond: {m1 + m2, max(p1, p2)}
}
end
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}
"""
@spec subtract(t, t) :: t
def subtract(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year - d2.year,
month: d1.month - d2.month,
week: d1.week - d2.week,
day: d1.day - d2.day,
hour: d1.hour - d2.hour,
minute: d1.minute - d2.minute,
second: d1.second - d2.second,
microsecond: {m1 - m2, max(p1, p2)}
}
end
@doc """
Multiplies `duration` units by given `integer`.
## Examples
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}
"""
@spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%Duration{
year: duration.year * integer,
month: duration.month * integer,
week: duration.week * integer,
day: duration.day * integer,
hour: duration.hour * integer,
minute: duration.minute * integer,
second: duration.second * integer,
microsecond: {ms * integer, p}
}
end
@doc """
Negates `duration` units.
## Examples
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}
"""
@spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do
%Duration{
year: -duration.year,
month: -duration.month,
week: -duration.week,
day: -duration.day,
hour: -duration.hour,
minute: -duration.minute,
second: -duration.second,
microsecond: {-ms, p}
}
end
@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
def from_iso8601(string) when is_binary(string) do
case Calendar.ISO.parse_duration(string) do
{:ok, duration} ->
{:ok, new!(duration)}
error ->
error
end
end
@doc """
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(string) when is_binary(string) do
case from_iso8601(string) do
{:ok, duration} ->
duration
{:error, reason} ->
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
end
end
@doc """
Converts the given `duration` to a human readable representation.
## Options
* `:units` - the units to be used alongside each duration component.
The default units follow the ISO 80000-3 standard:
[
year: "a",
month: "mo",
week: "wk",
day: "d",
hour: "h",
minute: "min",
second: "s"
]
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
## Examples
iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"
By default, this function uses ISO 80000-3 units, which uses "a" for years.
But you can customize all units via the units option:
iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"
You may also choose the separator:
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"
A duration without components is rendered as "0s":
iex> Duration.to_string(Duration.new!([]))
"0s"
Microseconds are rendered as part of seconds with the appropriate precision:
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"
"""
@doc since: "1.18.0"
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
case to_string_year(duration, [], units) do
[] ->
"0" <> Keyword.get(units, :second, "s")
[part] ->
IO.iodata_to_binary(part)
parts ->
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
end
end
defp to_string_part(0, _units, _key, _default, acc),
do: acc
defp to_string_part(x, units, key, default, acc),
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
defp to_string_year(%{year: year} = duration, acc, units) do
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
end
defp to_string_month(%{month: month} = duration, acc, units) do
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
end
defp to_string_week(%{week: week} = duration, acc, units) do
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
end
defp to_string_day(%{day: day} = duration, acc, units) do
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
end
defp to_string_hour(%{hour: hour} = duration, acc, units) do
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
end
defp to_string_minute(%{minute: minute} = duration, acc, units) do
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
end
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
acc
end
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
end
@doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`.
## Examples
iex> Duration.to_iso8601(Duration.new!(year: 3))
"P3Y"
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"P40DT12H42M12S"
iex> Duration.to_iso8601(Duration.new!(second: 30))
"PT30S"
iex> Duration.to_iso8601(Duration.new!([]))
"PT0S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
"PT1.002S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"PT-0.2000S"
"""
@spec to_iso8601(t) :: String.t()
def to_iso8601(%Duration{} = duration) do
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
{[], []} -> "PT0S"
{date, time} -> IO.iodata_to_binary([?P, date, time])
end
end
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
[]
end
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
end
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
[]
end
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
end
defp second_component(%{second: 0, microsecond: {0, _}}) do
[]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do
[second_component(second, ms, p), ?S]
end
defp second_component(second, _ms, 0) do
Integer.to_string(second)
end
defp second_component(second, ms, p) do
total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs()
sign = if total_ms < 0, do: ?-, else: []
[
sign,
Integer.to_string(second),
?.,
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p)
]
end
@compile {:inline, pair: 2}
defp pair(0, _key), do: []
defp pair(num, key), do: [Integer.to_string(num), key]
end
+10 -327
View File
@@ -18,8 +18,7 @@ defmodule Calendar.ISO do
The standard library supports a minimal set of possible ISO 8601 features.
Specifically, the parser only supports calendar dates and does not support
ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
ordinal and week formats.
By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times.
@@ -30,7 +29,7 @@ defmodule Calendar.ISO do
Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`).
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
#### Examples
@@ -231,9 +230,9 @@ defmodule Calendar.ISO do
]
end
defguardp is_year(year) when is_integer(year)
defguardp is_year_BCE(year) when year <= 0
defguardp is_year_CE(year) when year >= 1
defguardp is_year(year) when year in -9999..9999
defguardp is_year_BCE(year) when year in -9999..0
defguardp is_year_CE(year) when year in 1..9999
defguardp is_month(month) when month in 1..12
defguardp is_day(day) when day in 1..31
defguardp is_hour(hour) when hour in 0..23
@@ -664,87 +663,6 @@ defmodule Calendar.ISO do
end
end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms =
case string do
"-" <> _ ->
-ms
_ ->
ms
end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
end
_ ->
{:error, :invalid_time_component}
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
@doc """
Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -891,7 +809,7 @@ defmodule Calendar.ISO do
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
def date_from_iso_days(days) when days in -3_652_059..3_652_424 do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
@@ -1446,7 +1364,7 @@ defmodule Calendar.ISO do
@spec valid_date?(year, month, day) :: boolean
def valid_date?(year, month, day)
when is_integer(year) and is_integer(month) and is_integer(day) do
is_month(month) and day in 1..days_in_month(year, month)
is_year(year) and is_month(month) and day in 1..days_in_month(year, month)
end
@doc """
@@ -1537,230 +1455,6 @@ defmodule Calendar.ISO do
{days, {@parts_per_day - 1, @parts_per_day}}
end
@doc """
Shifts Date by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2))
{2016, 3, 3}
iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1))
{2016, 3, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1))
{2016, 2, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1))
{2020, 2, 1}
"""
@impl true
@spec shift_date(year, month, day, Duration.t()) :: {year, month, day}
def shift_date(year, month, day, duration) do
shift_options = shift_date_options(duration)
Enum.reduce(shift_options, {year, month, day}, fn
{_, 0}, date ->
date
{:month, value}, date ->
shift_months(date, value)
{:day, value}, date ->
shift_days(date, value)
end)
end
@doc """
Shifts NaiveDateTime by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1))
{2016, 1, 3, 1, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30))
{2016, 1, 4, 6, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{2016, 1, 3, 0, 0, 0, {100, 6}}
"""
@impl true
@spec shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do
shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{_, 0}, naive_datetime ->
naive_datetime
{:month, value}, {year, month, day, hour, minute, second, microsecond} ->
{new_year, new_month, new_day} = shift_months({year, month, day}, value)
{new_year, new_month, new_day, hour, minute, second, microsecond}
{time_unit, value}, naive_datetime ->
shift_time_unit(naive_datetime, value, time_unit)
end)
end
@doc """
Shifts Time by Duration units according to its calendar.
## Examples
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2))
{15, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{13, 0, 0, {100, 6}}
"""
@impl true
@spec shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
def shift_time(hour, minute, second, microsecond, duration) do
shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{_, 0}, time ->
time
{time_unit, value}, time ->
shift_time_unit(time, value, time_unit)
end)
end
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
{year, month, day}
end
defp shift_months({year, month, day}, months) do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
new_month when new_month < 1 -> new_month + months_in_year
new_month -> new_month
end
new_day = min(day, days_in_month(new_year, new_month))
{new_year, new_month, new_day}
end
@doc false
def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{year, month, day, hour, minute, second, {ms_value, _}} =
naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> shift_time_unit(value, unit)
|> naive_datetime_from_iso_days()
{year, month, day, hour, minute, second, {ms_value, precision}}
end
def shift_time_unit({hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{_days, day_fraction} =
shift_time_unit({0, time_to_day_fraction(hour, minute, second, microsecond)}, value, unit)
{hour, minute, second, {microsecond, _}} = time_from_day_fraction(day_fraction)
{hour, minute, second, {microsecond, precision}}
end
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
defp shift_time_unit_values({0, _}, {_, original_precision}) do
{0, original_precision}
end
defp shift_time_unit_values({ms_value, ms_precision}, {_, _}) do
{ms_value, ms_precision}
end
defp shift_time_unit_values(value, {_, original_precision}) do
{value, original_precision}
end
defp shift_date_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _precision}
}) do
[
month: year * 12 + month,
day: week * 7 + day
]
end
defp shift_date_options(_duration) do
raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day"
end
defp shift_datetime_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(%Duration{
year: 0,
month: 0,
week: 0,
day: 0,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
second: hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(_duration) do
raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond"
end
## Helpers
@doc false
@@ -1948,22 +1642,11 @@ defmodule Calendar.ISO do
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
defp days_in_previous_years(year) do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
+37 -161
View File
@@ -110,12 +110,30 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO)
end
def utc_now(calendar) do
utc_now(:native, calendar)
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
end
@doc """
@@ -140,20 +158,7 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
end
@doc """
@@ -247,7 +252,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -312,7 +317,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
@@ -443,8 +448,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -463,21 +466,23 @@ defmodule NaiveDateTime do
end
def add(
%{calendar: calendar, microsecond: {_, precision}} = naive_datetime,
%{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
amount_to_add,
unit
)
when is_integer(amount_to_add) do
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add, ppd)
|> from_iso_days(calendar, precision)
end
@@ -566,83 +571,6 @@ defmodule NaiveDateTime do
units1 - units2
end
@doc """
Shifts given `naive_datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1)
~N[2016-02-29 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1)
~N[2020-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1)
~N[2014-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45)
~N[2016-01-31 00:00:45]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6})
~N[2016-01-31 00:00:00.000100]
# leap years
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1)
~N[2025-02-28 00:00:00]
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4)
~N[2028-02-29 00:00:00]
# rounding down
iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1)
~N[2015-02-28 00:00:00]
"""
@doc since: "1.17.0"
@spec shift(Calendar.naive_datetime(), Duration.duration()) :: t
def shift(%{calendar: calendar} = naive_datetime, duration) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = naive_datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given naive datetime with the microsecond field truncated to the
given precision (`:microsecond`, `:millisecond` or `:second`).
@@ -754,9 +682,6 @@ defmodule NaiveDateTime do
@doc """
Converts the given naive datetime to a string according to its calendar.
For redability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
@@ -991,8 +916,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
{:ok, ~N[2000-01-01 13:30:15.005000]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
@@ -1001,11 +924,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
"""
@spec from_erl(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1024,19 +943,13 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
~N[2000-01-01 13:30:15.005000]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
@@ -1063,28 +976,11 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(seconds, microsecond_precision \\ {0, 0}, calendar \\ Calendar.ISO)
def from_gregorian_seconds(seconds, {microsecond, precision}, Calendar.ISO)
when is_integer(seconds) do
{days, seconds} = div_rem(seconds, 24 * 60 * 60)
{hours, seconds} = div_rem(seconds, 60 * 60)
{minutes, seconds} = div_rem(seconds, 60)
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
%NaiveDateTime{
calendar: Calendar.ISO,
year: year,
month: month,
day: day,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microsecond, precision}
}
end
def from_gregorian_seconds(seconds, {microsecond, precision}, calendar)
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
@@ -1103,17 +999,6 @@ defmodule NaiveDateTime do
}
end
defp div_rem(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
if rem >= 0 do
{div, rem}
else
{div - 1, rem + int2}
end
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
@@ -1260,10 +1145,6 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert(
%{
calendar: calendar,
@@ -1465,15 +1346,10 @@ defmodule NaiveDateTime do
calendar: calendar
} = naive_datetime
if calendar != Calendar.ISO or year in -9999..9999 do
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]"
else
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
end
"~N[" <> formatted <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+28 -117
View File
@@ -50,6 +50,7 @@ defmodule Time do
calendar: Calendar.calendar()
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
@@ -115,7 +116,7 @@ defmodule Time do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -355,21 +356,13 @@ defmodule Time do
## Examples
iex> Time.from_erl({23, 30, 15})
{:ok, ~T[23:30:15]}
iex> Time.from_erl({23, 30, 15}, 5000)
{:ok, ~T[23:30:15.005000]}
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -385,8 +378,6 @@ defmodule Time do
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, 5000)
~T[23:30:15.005000]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
@@ -509,8 +500,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -526,26 +515,13 @@ defmodule Time do
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
when is_integer(amount_to_add) do
valid? =
if is_integer(unit),
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
if not valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _precision}} =
Calendar.ISO.shift_time_unit(
{hour, minute, second, microsecond},
amount_to_add,
unit
)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
%Time{
hour: hour,
@@ -556,91 +532,19 @@ defmodule Time do
}
end
@doc """
Shifts given `time` by `duration` according to its calendar.
Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed to seconds and
microseconds before they are applied.
Raises an `ArgumentError` when called with date scale units.
## Examples
iex> Time.shift(~T[01:00:15], hour: 12)
~T[13:00:15]
iex> Time.shift(~T[01:35:00], hour: 6, minute: -15)
~T[07:20:00]
iex> Time.shift(~T[01:15:00], second: 125)
~T[01:17:05]
iex> Time.shift(~T[01:00:15], microsecond: {100, 6})
~T[01:00:15.000100]
iex> Time.shift(~T[01:15:00], Duration.new!(second: 65))
~T[01:16:05]
"""
@doc since: "1.17.0"
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
when unit_pair:
{:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
def shift(%{calendar: calendar} = time, duration) do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{hour, minute, second, microsecond} =
calendar.shift_time(hour, minute, second, microsecond, __duration__!(duration))
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
defp time_to_microseconds(%{
calendar: Calendar.ISO,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _}
}) do
0
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_duration_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
raise ArgumentError,
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({unit, _value})
when unit not in [:hour, :minute, :second, :microsecond] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
defp time_to_microseconds(time) do
iso_days = {0, to_day_fraction(time)}
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
end
@doc """
@@ -807,8 +711,15 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
{:ok, value} = convert(time, calendar)
value
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end
@doc """
+63 -135
View File
@@ -3,14 +3,14 @@ defmodule Code do
Utilities for managing code compilation, code evaluation, and code loading.
This module complements Erlang's [`:code` module](`:code`)
to add behavior which is specific to Elixir. For functions to
to add behaviour which is specific to Elixir. For functions to
manipulate Elixir's AST (rather than evaluating it), see the
`Macro` module.
## Working with files
This module contains three functions for compiling and evaluating files.
Here is a summary of them and their behavior:
Here is a summary of them and their behaviour:
* `require_file/2` - compiles a file and tracks its name. It does not
compile the file again if it has been previously required.
@@ -123,11 +123,6 @@ defmodule Code do
of the imported function/macro. A :remote_function/:remote_macro event
may still be emitted for the imported module/name/arity.
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
imported function or macro is processed inside a `quote/2`. `meta` is the
call AST metadata, `module` is the module the import is from, followed by
the `name` and a list of `arities` of the imported function/macro.
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
@@ -164,7 +159,7 @@ defmodule Code do
`{:ok, value}` or `:error`.
* `:defmodule` - (since v1.16.2) traced as soon as the definition of a module
starts. This is invoked early on in the module life cycle, `Module.open?/1`
starts. This is invoked early on in the module life-cycle, `Module.open?/1`
still returns `false` for such traces
* `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module
@@ -185,7 +180,7 @@ defmodule Code do
defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}")
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok
end
@@ -249,8 +244,8 @@ defmodule Code do
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:infer_signatures,
:relative_paths
:relative_paths,
:warnings_as_errors
]
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@@ -512,9 +507,14 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
Options can be:
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
Additionally, you may also pass an environment as second argument,
so the evaluation happens within that environment.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -659,17 +659,15 @@ defmodule Code do
## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error
reporting
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
the document. Defaults to 98. Note this value is used as
guideline but there are situations where it is not enforced.
See the "Line length" section below for more information
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
@@ -684,34 +682,28 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords.
Migration options (change the AST), see the "Migration formatting" section below:
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
becomes `<<foo::binary>>`, or adds parentheses for custom
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_unless` (since v1.18.0) - when `true`,
rewrites `unless` expressions using `if` with a negated condition, for example
`unless foo, do:` becomes `if !foo, do:`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
## Design principles
The formatter was designed under three principles.
First, the formatter never changes the semantics of the code by default.
First, the formatter never changes the semantics of the code.
This means the input AST and the output AST are almost always equivalent.
The only cases where the formatter will change the AST is when the input AST
would cause *compiler warnings* and the output AST won't. The cases where
the formatter changes the AST can be disabled through formatting options
if desired.
The second principle is to provide as little configuration as possible.
This eases the formatter adoption by removing contention points while
@@ -722,8 +714,8 @@ defmodule Code do
specially because a function is named `defmodule`, `def`, or the like. This
principle mirrors Elixir's goal of being an extensible language where
developers can extend the language with new constructs as if they were
part of the language. When it is absolutely necessary to change behavior
based on the name, this behavior should be configurable, such as the
part of the language. When it is absolutely necessary to change behaviour
based on the name, this behaviour should be configurable, such as the
`:locals_without_parens` option.
## Running the formatter
@@ -806,10 +798,9 @@ defmodule Code do
## Line length
Another point about the formatter is that the `:line_length` configuration
indicates when an expression should be broken over multiple lines but it is
not guaranteed to do so. In many cases, it is not possible for the formatter
to break your code apart, which means it will go over the line length.
For example, if you have a long string:
is a guideline. In many cases, it is not possible for the formatter to break
your code apart, which means it will go over the line length. For example,
if you have a long string:
"this is a very long string that will go over the line length"
@@ -822,15 +813,15 @@ defmodule Code do
The string concatenation makes the code fit on a single line and also
gives more options to the formatter.
This may also appear in keywords such as do/end blocks and operators,
where the `do` keyword may go over the line length because there is no
opportunity for the formatter to introduce a line break in a readable way.
For example, if you do:
This may also appear in do/end blocks, where the `do` keyword (or `->`)
may go over the line length because there is no opportunity for the
formatter to introduce a line break in a readable way. For example,
if you do:
case very_long_expression() do
end
And only the `do` keyword is beyond the line length, Elixir **will not**
And only the `do` keyword is above the line length, Elixir **will not**
emit this:
case very_long_expression()
@@ -868,7 +859,7 @@ defmodule Code do
* Newlines before certain operators (such as the pipeline operators)
and before other operators (such as comparison operators)
The behaviors above are not guaranteed. We may remove or add new
The behaviours above are not guaranteed. We may remove or add new
rules in the future. The goal of documenting them is to provide better
understanding on what to expect from the formatter.
@@ -945,8 +936,9 @@ defmodule Code do
## Code comments
The formatter handles code comments and guarantees a space is always added
between the beginning of the comment (#) and the next character.
The formatter also handles code comments in a way to guarantee a space
is always added between the beginning of the comment (#) and the next
character.
The formatter also extracts all trailing comments to their previous line.
For example, the code below
@@ -958,19 +950,9 @@ defmodule Code do
# world
hello
While the formatter attempts to preserve comments in most situations,
that's not always possible, because code comments are handled apart from
the code representation (AST). While the formatter can preserve code
comments between expressions and function arguments, the formatter
cannot currently preserve them around operators. For example, the following
code will move the code comments to before the operator usage:
foo() ||
# also check for bar
bar()
In some situations, code comments can be seen as ambiguous by the formatter.
For example, the comment in the anonymous function below
Because code comments are handled apart from the code representation (AST),
there are some situations where code comments are seen as ambiguous by the
code formatter. For example, the comment in the anonymous function below
fn
arg1 ->
@@ -999,21 +981,6 @@ defmodule Code do
## Newlines
The formatter converts all newlines in code from `\r\n` to `\n`.
## Migration formatting
As part of the Elixir release cycle, deprecations are being introduced,
emitting warnings which might require existing code to be changed.
In order to reduce the burden on developers when upgrading Elixir to the
next version, the formatter exposes some options, disabled by default,
in order to automate this process.
These options should address most of the typical use cases, but given they
introduce changes to the AST, there is a non-zero risk for meta-programming
heavy projects that relied on a specific AST, or projects that are
re-defining functions from the `Kernel`. In such cases, migrations cannot
be applied blindly and some extra changes might be needed in order to
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@@ -1107,8 +1074,6 @@ defmodule Code do
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
"""
@doc since: "1.14.0"
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@@ -1163,7 +1128,7 @@ defmodule Code do
For example, `"null byte\\t\\x00"` will be kept as is instead of being
converted to a bitstring literal. Note if you set this option to false, the
resulting AST is no longer valid, but it can be useful to analyze/transform
source code, typically in combination with `quoted_to_algebra/2`.
source code, typically in in combination with `quoted_to_algebra/2`.
Defaults to `true`.
* `:existing_atoms_only` - when `true`, raises an error
@@ -1184,7 +1149,7 @@ defmodule Code do
* `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this
option overrides the `:existing_atoms_only` behavior for static
option overrides the `:existing_atoms_only` behaviour for static
atoms but `:existing_atoms_only` is still used for dynamic atoms,
such as atoms with interpolations.
@@ -1253,7 +1218,6 @@ defmodule Code do
It returns the AST if it succeeds,
raises an exception otherwise. The exception is a `TokenMissingError`
in case a token is missing (usually because the expression is incomplete),
`MismatchedDelimiterError` (in case of mismatched opening and closing delimiters) and
`SyntaxError` otherwise.
Check `string_to_quoted/2` for options information.
@@ -1277,7 +1241,7 @@ defmodule Code do
Comments are maps with the following fields:
* `:line` - The line number of the source code
* `:line` - The line number the source code
* `:text` - The full text of the comment, including the leading `#`
@@ -1562,8 +1526,8 @@ defmodule Code do
## Examples
Code.compiler_options(infer_signatures: false)
#=> %{infer_signatures: true}
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
"""
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1592,12 +1556,6 @@ defmodule Code do
:elixir_config.get(key)
end
# TODO: Remove me in Elixir v2.0
def get_compiler_option(:warnings_as_errors) do
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
:ok
end
@doc """
Returns a list with all available compiler options.
@@ -1626,19 +1584,16 @@ defmodule Code do
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
This enables tooling to partially reconstruct the original source code,
for instance, to perform static analysis of code. Therefore, disabling
module. Defaults to `true`.
This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
`:debug_info` is not recommended as it removes the ability of the
Elixir compiler and other tools to provide feedback. If you want to
remove the `:debug_info` while deploying, tools like `mix release`
already do such by default.
Other environments, such as `mix test`, automatically disables this
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
Additionally, `mix test` disables it via the `:test_elixirc_options`
project configuration option.
This option can also be overridden per module using the `@compile` directive.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
@@ -1647,19 +1602,13 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option.
Defaults to `true`.
`mix test` automatically disables this option via the `:test_elixirc_options`
project configuration, as there is typically no need to store infer signatures
for test files.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
warnings, and errors generated by the compiler. Note disabling this option
won't affect runtime warnings and errors. Defaults to `true`.
* `:warnings_as_errors` - causes compilation to fail when warnings are
generated. Defaults to `false`.
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined
@@ -1682,9 +1631,8 @@ defmodule Code do
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
This `:warn` behaviour only exists for compatibility reasons when working
with old dependencies.
It always returns `:ok`. Raises an error for invalid options.
@@ -1705,18 +1653,6 @@ defmodule Code do
:ok
end
# TODO: Remove me in Elixir v2.0
def put_compiler_option(:warnings_as_errors, _value) do
IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
"instead you must pass it as a --warnings-as-errors flag. " <>
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
"[compile: \"compile --warnings-as-errors\"]"
)
:ok
end
def put_compiler_option(:no_warn_undefined, value) do
if value != :all and not is_list(value) do
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
@@ -1746,8 +1682,7 @@ defmodule Code do
:ok
end
# TODO: Remove this option on Elixir v2.0
# TODO: Warn if mode is :warn on Elixir v1.19
# TODO: Make this option have no effect on Elixir v2.0
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
:elixir_config.put(:on_undefined_variable, value)
:ok
@@ -1786,7 +1721,7 @@ defmodule Code do
Returns a list of tuples where the first element is the module name
and the second one is its bytecode (as a binary). A `file` can be
given as a second argument which will be used for reporting warnings
given as second argument which will be used for reporting warnings
and errors.
**Warning**: `string` can be any Elixir code and code can be executed with
@@ -2030,7 +1965,7 @@ defmodule Code do
@doc """
Returns `true` if the module is loaded.
This function doesn't attempt to load the module. For such behavior,
This function doesn't attempt to load the module. For such behaviour,
`ensure_loaded?/1` can be used.
## Examples
@@ -2097,11 +2032,7 @@ defmodule Code do
@spec fetch_docs(module | String.t()) ::
{:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata,
docs :: [doc_element]}
| {:error,
:module_not_found
| :chunk_not_found
| {:invalid_chunk, binary}
| :invalid_beam}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
@@ -2172,9 +2103,6 @@ defmodule Code do
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
{:error, :beam_lib, _} ->
{:error, :invalid_beam}
end
end
+78 -185
View File
@@ -16,7 +16,7 @@ defmodule Code.Formatter do
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
# Operators that are composed of multiple binary operators
@multi_binary_operators [:..//]
@multi_binary_operators [:"..//"]
# Operators that do not have space between operands
@no_space_binary_operators [:.., :"//"]
@@ -31,37 +31,30 @@ defmodule Code.Formatter do
@right_new_line_before_binary_operators [:|, :when]
# Operators that are logical cannot be mixed without parens
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
# Operators with next break fits
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
# Operators that always require parens even
# when they are their own parents as they are not semantically associative
@required_parens_even_when_parent [:--, :---]
# Operators that always require parens on operands
# when they are the parent of another operator with a difference precedence
# Most operators are listed, except comparison, arithmetic, and low precedence
# Operators that always require parens on operands when they are the parent
@required_parens_on_binary_operands [
:|>,
:<<<,
:>>>,
:|>,
:<~,
:~>,
:<<~,
:~>>,
:<~>,
:"<|>",
:in,
:"^^^",
:"//",
:++,
:--,
:+++,
:---,
:<>,
:..
:in,
:++,
:--,
:..,
:<>
]
@locals_without_parens [
@@ -189,10 +182,8 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils =
@@ -217,9 +208,8 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
}
end
@@ -276,7 +266,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -300,7 +290,6 @@ defmodule Code.Formatter do
end
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp quoted_to_algebra(
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
context,
@@ -377,11 +366,6 @@ defmodule Code.Formatter do
tuple_to_algebra(meta, [left, right], :flex_break, state)
end
# (left -> right)
defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
case meta[:delimiter] do
~s['''] ->
@@ -407,7 +391,7 @@ defmodule Code.Formatter do
{@double_heredoc
|> concat(string)
|> concat(@double_heredoc)
|> color_doc(:string, state.inspect_opts)
|> color(:string, state.inspect_opts)
|> force_unfit(), state}
else
string = escape_string(string, @double_quote)
@@ -415,7 +399,7 @@ defmodule Code.Formatter do
{@double_quote
|> concat(string)
|> concat(@double_quote)
|> color_doc(:string, state.inspect_opts), state}
|> color(:string, state.inspect_opts), state}
end
end
@@ -432,7 +416,6 @@ defmodule Code.Formatter do
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
end
# (unquote_splicing(...))
defp quoted_to_algebra(
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
context,
@@ -447,7 +430,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color_doc("nil", nil, state.inspect_opts), state}
{color("nil", nil, state.inspect_opts), state}
end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -464,7 +447,7 @@ defmodule Code.Formatter do
end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color_doc(:atom, state.inspect_opts), state}
|> color(:atom, state.inspect_opts), state}
end
# &1
@@ -487,53 +470,6 @@ defmodule Code.Formatter do
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
end
# disable migrate_unless within defmacro
defp quoted_to_algebra(
{atom, _, [{:unless, _, _}, _]} = ast,
context,
%{migrate_unless: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
end
# rewrite unless as if!
defp quoted_to_algebra(
{:unless, meta, [condition, block]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
end
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
quoted_to_algebra(
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
context,
state
)
end
# condition |> unless(...) => negated(condition) |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra(
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
context,
state
)
end
# ..
defp quoted_to_algebra({:.., _meta, []}, context, state) do
if context in [:no_parens_arg, :no_parens_one_arg] do
@@ -543,13 +479,8 @@ defmodule Code.Formatter do
end
end
# ...
defp quoted_to_algebra({:..., _meta, []}, _context, state) do
{"...", state}
end
# 1..2//3
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
end
@@ -568,6 +499,11 @@ defmodule Code.Formatter do
remote_to_algebra(quoted, context, state)
end
# (left -> right)
defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
# [keyword: :list] (inner part)
# %{:foo => :bar} (inner part)
defp quoted_to_algebra(list, context, state) when is_list(list) do
@@ -581,12 +517,20 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do
{left, state} =
case left_arg do
{:__block__, _, [atom]} when is_atom(atom) ->
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
{:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), state}
{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}
{:__block__, _, [atom]} when is_atom(atom) ->
iodata =
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
[Atom.to_string(atom), ?:]
else
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
end
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -610,11 +554,8 @@ defmodule Code.Formatter do
end
# #PID's and #Ref's may appear on regular AST
# Other foreign structures, such as maps and structs,
# may appear from Macro.to_string, so we stick a limit,
# although they won't be formatted accordingly.
defp quoted_to_algebra(unknown, _context, state) do
{inspect(unknown, printable_limit: :infinity), state}
{inspect(unknown), state}
end
## Blocks
@@ -684,12 +625,12 @@ defmodule Code.Formatter do
Atom.to_string(op)
end
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
with [left, right] <- args,
{_, _} <- augmented_binary_op(fun) do
{_, _} <- Code.Identifier.binary_op(fun) do
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
else
_ -> :error
@@ -716,7 +657,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
when op in @right_new_line_before_binary_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = op_string <> " "
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -753,7 +694,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
when op in @pipeline_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
max_line = line(meta)
@@ -767,7 +708,7 @@ defmodule Code.Formatter do
{{doc, @empty, 1}, state}
{{op, context}, arg}, _args, state ->
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = Atom.to_string(op) <> " "
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
{{concat(op_string, doc), @empty, 1}, state}
@@ -777,7 +718,7 @@ defmodule Code.Formatter do
end
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -790,11 +731,11 @@ defmodule Code.Formatter do
doc =
cond do
op in @no_space_binary_operators ->
op_doc = color_doc(op_string, :operator, state.inspect_opts)
op_doc = color(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators ->
op_doc = color_doc(" " <> op_string <> " ", :operator, state.inspect_opts)
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true ->
@@ -804,7 +745,7 @@ defmodule Code.Formatter do
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
with_next_break_fits(next_break_fits?, right, fn right ->
op_doc = color_doc(" " <> op_string, :operator, state.inspect_opts)
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
right = nest(glue(op_doc, group(right)), nesting, :break)
right = if eol?, do: force_unfit(right), else: right
concat(group(left), group(right))
@@ -836,18 +777,19 @@ defmodule Code.Formatter do
{parent_assoc, parent_prec} = parent_info
with {op, meta, [left, right]} <- operand,
op_info = augmented_binary_op(op),
op_info = Code.Identifier.binary_op(op),
{_assoc, prec} <- op_info do
op_string = Atom.to_string(op)
cond do
# If we have the same operator and it is in the correct side,
# we don't add parens unless it is explicitly required.
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
# If the operator has the same precedence as the parent and is on
# the correct side, we respect the nesting rule to avoid multiple
# nestings. This only applies for left associativity or same operator.
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the operator requires parens (most of them do) or we are mixing logical operators
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
# If the parent requires parens or the precedence is inverted or
# it is in the wrong side, then we *need* parenthesis.
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
(op in @required_parens_logical_binary_operands and
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
@@ -1004,7 +946,7 @@ defmodule Code.Formatter do
)
when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -1051,9 +993,7 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state)
fun_doc =
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1121,7 +1061,7 @@ defmodule Code.Formatter do
fun
|> Atom.to_string()
|> string()
|> color_doc(:call, state.inspect_opts)
|> color(:call, state.inspect_opts)
|> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1484,7 +1424,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} =
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1562,8 +1502,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts)
left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state}
end
@@ -1595,8 +1535,8 @@ defmodule Code.Formatter do
end
defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
end
defp tuple_to_algebra(meta, args, join, state) do
@@ -1606,8 +1546,8 @@ defmodule Code.Formatter do
{args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts)
left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color("}", :tuple, state.inspect_opts)
if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1617,11 +1557,16 @@ defmodule Code.Formatter do
end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
Atom.to_string(atom) |> color(:boolean, inspect_opts)
end
defp atom_to_algebra(nil, _, inspect_opts) do
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
Atom.to_string(nil) |> color(nil, inspect_opts)
end
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
defp atom_to_algebra(:"..//", _, inspect_opts) do
string(":\"..//\"") |> color(:atom, inspect_opts)
end
defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1634,7 +1579,7 @@ defmodule Code.Formatter do
_ -> ":\\\\"
end
string(string) |> color_doc(:atom, inspect_opts)
string(string) |> color(:atom, inspect_opts)
end
defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1647,7 +1592,7 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
@@ -1664,7 +1609,7 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color_doc(:number, inspect_otps)
|> color(:number, inspect_otps)
end
defp float_to_algebra(text, inspect_otps) do
@@ -1672,7 +1617,7 @@ defmodule Code.Formatter do
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_otps)
color(string, :number, inspect_otps)
end
defp insert_underscores("-" <> digits) do
@@ -2251,15 +2196,10 @@ defmodule Code.Formatter do
unary_operator?(quoted) or binary_operator?(quoted)
end
# We convert ..// into two operators for simplicity,
# so we need to augment the binary table.
defp augmented_binary_op(:"//"), do: {:right, 190}
defp augmented_binary_op(op), do: Code.Identifier.binary_op(op)
defp binary_operator?(quoted) do
case quoted do
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
{op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
_ -> false
end
end
@@ -2295,7 +2235,6 @@ defmodule Code.Formatter do
(not interpolated?(entries) and eol_or_comments?(meta, state))
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
meta[:delimiter] == ~s[''']
end
@@ -2437,10 +2376,6 @@ defmodule Code.Formatter do
meta[:closing][:line] || @min_line
end
defp escape_atom(string, char) do
String.replace(string, <<char>>, <<?\\, char>>)
end
## Algebra helpers
# Relying on the inner document is brittle and error prone.
@@ -2486,7 +2421,7 @@ defmodule Code.Formatter do
end
defp get_charlist_quotes(:heredoc, state) do
if state.migrate_charlists_as_sigils do
if state.normalize_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc}
else
{@single_heredoc, @single_heredoc}
@@ -2495,7 +2430,7 @@ defmodule Code.Formatter do
defp get_charlist_quotes({:regular, chunks}, state) do
cond do
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
true -> {@sigil_c_double, @double_quote}
end
@@ -2504,46 +2439,4 @@ defmodule Code.Formatter do
defp has_double_quote?(chunk) do
is_binary(chunk) and chunk =~ @double_quote
end
# Migration rewrites
@bool_operators [
:>,
:>=,
:<,
:<=,
:in
]
@guards [
:is_atom,
:is_boolean,
:is_nil,
:is_number,
:is_integer,
:is_float,
:is_binary,
:is_map,
:is_struct,
:is_non_struct_map,
:is_exception,
:is_list,
:is_tuple,
:is_function,
:is_reference,
:is_pid,
:is_port
]
defp negate_condition(condition) do
case condition do
{neg, _, [condition]} when neg in [:!, :not] -> condition
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
_ -> {:!, [], [condition]}
end
end
end
+14 -162
View File
@@ -31,7 +31,7 @@ defmodule Code.Fragment do
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
{:local_or_var, 'hello_wor'}
## Return values
@@ -320,9 +320,6 @@ defmodule Code.Fragment do
_ ->
{{:local_or_var, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
@@ -365,14 +362,6 @@ defmodule Code.Fragment do
:none
end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
@@ -492,10 +481,10 @@ defmodule Code.Fragment do
defp operator(rest, count, acc, _call_op?) do
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
{:ok, _, _, _, [{:atom, _, _}], []} ->
{:ok, _, _, _, [{:atom, _, _}]} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, _, _, [{_, _, op}], []} ->
{:ok, _, _, _, [{_, _, op}]} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
@@ -558,7 +547,7 @@ defmodule Code.Fragment do
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
## Differences to `cursor_context/2`
@@ -573,13 +562,13 @@ defmodule Code.Fragment do
`local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
* This function returns keywords as `{:keyword, ~c"do"}`
* This function returns keywords as `{:keyword, 'do'}`
* This function never returns `:expr`
@@ -601,9 +590,7 @@ defmodule Code.Fragment do
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
| {:keyword, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
@@ -653,16 +640,7 @@ defmodule Code.Fragment do
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
{rest, _} = strip_spaces(rest, 0)
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
@@ -678,9 +656,6 @@ defmodule Code.Fragment do
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
@@ -708,9 +683,6 @@ defmodule Code.Fragment do
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
@@ -743,16 +715,6 @@ defmodule Code.Fragment do
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset)
@@ -1099,13 +1061,6 @@ defmodule Code.Fragment do
iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
In order to parse the left-side of `->` properly, which appears both
in anonymous functions and do-end blocks, the trailing fragment option
must be given with the rest of the contents:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
## Options
* `:file` - the filename to be reported in case of parsing errors.
@@ -1128,117 +1083,14 @@ defmodule Code.Fragment do
* `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts =
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end
+31 -62
View File
@@ -13,7 +13,7 @@ defmodule Code.Identifier do
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
def unary_op(op) do
cond do
op in [:&, :...] -> {:non_associative, 90}
op in [:&] -> {:non_associative, 90}
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
op in [:@] -> {:non_associative, 320}
true -> :error
@@ -44,6 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
op in [:in] -> {:left, 170}
op in [:"^^^"] -> {:left, 180}
op in [:"//"] -> {:right, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
@@ -71,11 +72,11 @@ defmodule Code.Identifier do
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: binary(), remaining :: binary()}
{escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do
escape(binary, char, limit, <<>>, fun)
escape(binary, char, limit, [], fun)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
@@ -83,96 +84,64 @@ defmodule Code.Identifier do
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
if value = fun.(h) do
value = IO.iodata_to_binary(value)
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
else
escape(t, char, decrement(count), escape_char(h, acc), fun)
end
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
defp escape_char(0), do: [?\\, ?0]
defp escape_char(char, acc)
# Some characters that are confusing (zero-width / alternative spaces) are displayed
# using their unicode representation:
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
@escaped_bom :binary.bin_to_list("\\uFEFF")
defp escape_char(65279), do: @escaped_bom
# BOM
when char == 0xFEFF
# Mathematical invisibles
when char in 0x2061..0x2064
# Bidirectional neutral
when char in [0x061C, 0x200E, 0x200F]
# Bidirectional general (source of vulnerabilities)
when char in 0x202A..0x202E
when char in 0x2066..0x2069
# Interlinear annotations
when char in 0xFFF9..0xFFFC
# Zero-width joiners and non-joiners
when char in [0x200C, 0x200D, 0x034F]
# Non-break space / zero-width space
when char in [0x00A0, 0x200B, 0x2060]
# Line/paragraph separators
when char in [0x2028, 0x2029]
# Spaces
when char in 0x2000..0x200A
when char == 0x205F do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
end
defp escape_char(char, acc)
defp escape_char(char)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<acc::binary, char::utf8>>
<<char::utf8>>
end
defp escape_char(char, acc) when char < 0x100 do
defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
[?\\, ?x, to_hex(a), to_hex(b)]
end
defp escape_char(char, acc) when char < 0x10000 do
defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end
defp escape_char(char, acc) when char < 0x1000000 do
defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
end
defp escape_map(?\a), do: "\\a"
defp escape_map(?\b), do: "\\b"
defp escape_map(?\d), do: "\\d"
defp escape_map(?\e), do: "\\e"
defp escape_map(?\f), do: "\\f"
defp escape_map(?\n), do: "\\n"
defp escape_map(?\r), do: "\\r"
defp escape_map(?\t), do: "\\t"
defp escape_map(?\v), do: "\\v"
defp escape_map(?\\), do: "\\\\"
defp escape_map(?\a), do: [?\\, ?a]
defp escape_map(?\b), do: [?\\, ?b]
defp escape_map(?\d), do: [?\\, ?d]
defp escape_map(?\e), do: [?\\, ?e]
defp escape_map(?\f), do: [?\\, ?f]
defp escape_map(?\n), do: [?\\, ?n]
defp escape_map(?\r), do: [?\\, ?r]
defp escape_map(?\t), do: [?\\, ?t]
defp escape_map(?\v), do: [?\\, ?v]
defp escape_map(?\\), do: [?\\, ?\\]
defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1}
+38 -44
View File
@@ -58,7 +58,7 @@ defmodule Code.Normalizer do
{:.., meta, [left, right]}
else
step = do_normalize(step, state)
{:..//, meta, [left, right, step]}
{:"..//", meta, [left, right, step]}
end
end
@@ -96,7 +96,6 @@ defmodule Code.Normalizer do
end
# Charlists with interpolations
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
@@ -126,17 +125,22 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]}
end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped. However, it should be escaped if applicable.
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
# it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta})
right = maybe_escape_literal(right, state)
{:., meta, [left, right]}
end
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
normalize_args(quoted, state)
end
# left -> right
defp do_normalize({:->, meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
@@ -165,6 +169,9 @@ defmodule Code.Normalizer do
right = normalize_map_args(right, state)
[{:|, pipe_meta, [left, right]}]
[{_, _, _} = call] ->
[do_normalize(call, state)]
args ->
normalize_map_args(args, state)
end
@@ -174,17 +181,16 @@ defmodule Code.Normalizer do
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
with "sigil_" <> _ <- Atom.to_string(sigil),
true <- binary_interpolated?(args),
true <- List.ascii_printable?(modifiers) do
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
when is_list(args) and is_atom(sigil) do
case Atom.to_string(sigil) do
"sigil_" <> _ ->
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
else
_ ->
normalize_call(quoted, state)
end
@@ -289,18 +295,21 @@ defmodule Code.Normalizer do
# Lists
defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do
# It's a charlist, we normalize it as a ~C sigil
string =
# It's a charlist
list =
if state.escape do
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist)
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
else
List.to_string(list)
list
end
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
{:__block__, meta, [list]}
else
meta =
if line = state.parent_meta[:line] do
@@ -347,14 +356,14 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
@@ -364,20 +373,11 @@ defmodule Code.Normalizer do
last_args =
case last_arg do
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
cond do
key_meta[:format] == :keyword ->
[keyword]
block_keyword?(keyword) ->
[
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
{{:__block__, [format: :keyword] ++ meta, args}, value}
end)
]
true ->
[last_arg]
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
end
[] ->
@@ -391,12 +391,6 @@ defmodule Code.Normalizer do
end
end
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
do: block_keyword?(tail)
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false
defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
+11 -5
View File
@@ -27,6 +27,11 @@ defmodule Code.Typespec do
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -283,6 +288,7 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -311,6 +317,10 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
@@ -328,14 +338,10 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
+2 -14
View File
@@ -94,10 +94,10 @@ end
defimpl Collectable, for: List do
def into(list) do
# TODO: Change the behavior so the into always comes last on Elixir v2.0
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
if list != [] do
IO.warn(
"the Collectable protocol is deprecated for non-empty lists. The behavior of " <>
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
@@ -140,10 +140,6 @@ defimpl Collectable, for: BitString do
__acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a binary requires a bitstring, got: #{inspect(other)}"
end
{[binary], fun}
@@ -159,10 +155,6 @@ defimpl Collectable, for: BitString do
_acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a bitstring requires a bitstring, got: #{inspect(other)}"
end
{bitstring, fun}
@@ -180,10 +172,6 @@ defimpl Collectable, for: Map do
_map_acc, :halt ->
:ok
_map_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a map requires {key, value} tuples, got: #{inspect(other)}"
end
{map, fun}
+5 -31
View File
@@ -41,8 +41,7 @@ defmodule Config do
> application environment is effectively a global storage. Also note that
> the `config/config.exs` of a library is not evaluated when the library is
> used as a dependency, as configuration is always meant to configure the
> current project. For more information, see ["Using application configuration for
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
> current project. For more information, read our [library guidelines](library-guidelines.md).
## Migrating from `use Mix.Config`
@@ -83,9 +82,9 @@ defmodule Config do
...
end
The only places where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which are always within
the `Mix.Tasks` namespace.
The only files where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which always within the
`Mix.Tasks` namespace.
## `config/runtime.exs`
@@ -144,7 +143,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
@@ -195,31 +194,6 @@ defmodule Config do
|> put_config()
end
@doc """
Reads the configuration for the given root key.
This function only reads the configuration from a previous
`config/2` or `config/3` call. If `root_key` points to an
application, it does not read its actual application environment.
Its main use case is to make it easier to access and share
configuration values across files.
If the `root_key` was not configured, it returns `nil`.
## Examples
# In config/config.exs
config :my_app, foo: :bar
# In config/dev.exs
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
"""
@doc since: "1.18.0"
def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key]
end
@doc """
Returns the environment this configuration file is executed on.
+1 -1
View File
@@ -312,7 +312,7 @@ defmodule Config.Provider do
"""
the application #{inspect(app)} has a different value set #{path(key, path)} \
during runtime compared to compile time. Since this application environment entry was \
marked as compile time, this difference can lead to different behavior than expected:
marked as compile time, this difference can lead to different behaviour than expected:
* Compile time value #{return_to_text(compile_return)}
* Runtime value #{return_to_text(runtime_return)}
+16 -12
View File
@@ -215,7 +215,10 @@ defmodule DynamicSupervisor do
extra_arguments: [term()]
}
@typedoc "Options given to `start_link/1` and `init/1` functions"
@typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@type init_option ::
{:strategy, strategy()}
| {:max_restarts, non_neg_integer()}
@@ -274,7 +277,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -342,11 +345,9 @@ defmodule DynamicSupervisor do
specified in the child spec given to `start_child/2`. Defaults to
an empty list.
* Any of the standard [GenServer options](`t:GenServer.option/0`)
"""
@doc since: "1.6.0"
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
@spec start_link([option | init_option]) :: Supervisor.on_start()
def start_link(options) when is_list(options) do
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
{sup_opts, start_opts} = Keyword.split(options, keys)
@@ -380,15 +381,9 @@ defmodule DynamicSupervisor do
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
with `:normal` reason.
## Options
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
callback.
"""
@doc since: "1.6.0"
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
@spec start_link(module, term, [option]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end
@@ -1141,6 +1136,15 @@ defmodule DynamicSupervisor do
]
end
@impl true
def format_status(:terminate, [_pdict, state]) do
state
end
def format_status(_, [_pdict, %{mod: mod} = state]) do
[data: [{~c"State", state}], supervisor: [{~c"Callback", mod}]]
end
## Helpers
@compile {:inline, call: 2}
+122 -251
View File
@@ -80,7 +80,7 @@ defprotocol Enumerable do
Returns the accumulator for the next enumeration step.
"""
@type reducer :: (element :: term, element_acc :: term -> acc)
@type reducer :: (element :: term, current_acc :: acc -> updated_acc :: acc)
@typedoc """
The result of the reduce operation.
@@ -269,12 +269,12 @@ defmodule Enum do
After all, if we want to traverse every element on a list, the longer the
list, the more elements we need to traverse, and the longer it will take.
This linear behavior should also be expected on operations like `count/1`,
This linear behaviour should also be expected on operations like `count/1`,
`member?/2`, `at/2` and similar. While Elixir does allow data types to
provide performant variants for such operations, you should not expect it
to always be available, since the `Enum` module is meant to work with a
large variety of data types and not all data types can provide optimized
behavior.
behaviour.
Finally, note the functions in the `Enum` module are eager: they will
traverse the enumerable as soon as they are invoked. This is particularly
@@ -837,7 +837,7 @@ defmodule Enum do
end
def dedup(enumerable) do
reduce(enumerable, [], fn x, acc ->
Enum.reduce(enumerable, [], fn x, acc ->
case acc do
[^x | _] -> acc
_ -> [x | acc]
@@ -1096,10 +1096,6 @@ defmodule Enum do
iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
[2]
iex> Enum.filter(["apple", "pear", "banana"], fn fruit -> String.contains?(fruit, "a") end)
["apple", "pear", "banana"]
iex> Enum.filter([4, 21, 24, 904], fn seconds -> seconds > 1000 end)
[]
Keep in mind that `filter` is not capable of filtering and
transforming an element at the same time. If you would like
@@ -1257,14 +1253,6 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]]
This is frequently used to to transform and filter in one pass, returning empty
lists to exclude results:
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
...> if x != 0, do: [1 / x], else: []
...> end)
[0.25, 0.5]
"""
@spec flat_map(t, (element -> t)) :: list
def flat_map(enumerable, fun) when is_list(enumerable) do
@@ -1274,7 +1262,6 @@ defmodule Enum do
def flat_map(enumerable, fun) do
reduce(enumerable, [], fn entry, acc ->
case fun.(entry) do
[] -> acc
list when is_list(list) -> [list | acc]
other -> [to_list(other) | acc]
end
@@ -1282,8 +1269,6 @@ defmodule Enum do
|> flat_reverse([])
end
# the first clause is an optimization
defp flat_reverse([[elem] | t], acc), do: flat_reverse(t, [elem | acc])
defp flat_reverse([h | t], acc), do: flat_reverse(t, h ++ acc)
defp flat_reverse([], acc), do: acc
@@ -1443,7 +1428,7 @@ defmodule Enum do
)
# Avoid warnings about Dict
dict_module = String.to_atom("Dict")
dict_module = Dict
reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -1543,13 +1528,14 @@ defmodule Enum do
defp into_map(enumerable) when is_list(enumerable), do: :maps.from_list(enumerable)
defp into_map(enumerable), do: enumerable |> Enum.to_list() |> :maps.from_list()
defp into_map(%{} = enumerable, collectable), do: Map.merge(collectable, enumerable)
defp into_map(%{} = enumerable, collectable),
do: Map.merge(collectable, enumerable)
defp into_map(enumerable, collectable) when is_list(enumerable),
do: Map.merge(collectable, :maps.from_list(enumerable))
defp into_map(enumerable, collectable),
do: reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
do: Enum.reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
defp into_protocol(enumerable, collectable) do
{initial, fun} = Collectable.into(collectable)
@@ -1592,7 +1578,7 @@ defmodule Enum do
"""
@spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t()
def into(enumerable, [], transform) do
map(enumerable, transform)
Enum.map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
@@ -1605,9 +1591,9 @@ defmodule Enum do
def into(enumerable, %{} = collectable, transform) do
if map_size(collectable) == 0 do
enumerable |> map(transform) |> :maps.from_list()
enumerable |> Enum.map(transform) |> :maps.from_list()
else
reduce(enumerable, collectable, fn entry, acc ->
Enum.reduce(enumerable, collectable, fn entry, acc ->
{key, val} = transform.(entry)
Map.put(acc, key, val)
end)
@@ -1650,8 +1636,8 @@ defmodule Enum do
If `joiner` is not passed at all, it defaults to an empty string.
All elements in the `enumerable` must be convertible to a string
or be a binary, otherwise an error is raised.
All elements in the `enumerable` must be convertible to a string,
otherwise an error is raised.
## Examples
@@ -1665,7 +1651,7 @@ defmodule Enum do
"ab cdefg h i"
"""
@spec join(t, binary()) :: binary()
@spec join(t, String.t()) :: String.t()
def join(enumerable, joiner \\ "")
def join(enumerable, "") do
@@ -2376,6 +2362,12 @@ defmodule Enum do
the random value. Check its documentation for setting a
different random algorithm or a different seed.
The implementation is based on the
[reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling#Relation_to_Fisher-Yates_shuffle)
algorithm.
It assumes that the sample being returned can fit into memory;
the input `enumerable` doesn't have to, as it is traversed just once.
If a range is passed into the function, this function will pick a
random value between the range limits, without traversing the whole
range (thus executing in constant time and constant memory).
@@ -2394,12 +2386,6 @@ defmodule Enum do
iex> Enum.random(1..1_000)
309
## Implementation
The random functions in this module implement reservoir sampling,
which allows them to sample infinite collections. In particular,
we implement Algorithm L, as described in by Kim-Hung Li in
"Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))".
"""
@spec random(t) :: element
def random(enumerable)
@@ -2427,17 +2413,13 @@ defmodule Enum do
{:ok, count, fun} when is_function(fun, 1) ->
slice_list(fun.(enumerable), random_count(count), 1, 1)
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
fun.(random_count(count), 1)
{:ok, count, fun} when is_function(fun, 3) ->
fun.(random_count(count), 1, 1)
# TODO: Remove deprecation on Elixir v1.20.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
fun.(random_count(count), 1)
{:error, _} ->
take_random(enumerable, 1)
end
@@ -2717,11 +2699,11 @@ defmodule Enum do
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1)
[:a, :f, :b, :c, :d, :e, :g]
# Slide a range of elements towards the head of the list
# Slide a range of elements backward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1)
[:a, :d, :e, :f, :b, :c, :g]
# Slide a range of elements towards the tail of the list
# Slide a range of elements forward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
[:a, :e, :f, :b, :c, :d, :g]
@@ -2751,7 +2733,7 @@ defmodule Enum do
end
# Normalize negative input ranges like Enum.slice/2
def slide(enumerable, first..last//_, insertion_index)
def slide(enumerable, first..last, insertion_index)
when first < 0 or last < 0 or insertion_index < 0 do
count = Enum.count(enumerable)
normalized_first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
@@ -2768,23 +2750,23 @@ defmodule Enum do
end
end
def slide(enumerable, insertion_index.._//_, insertion_index) do
def slide(enumerable, insertion_index.._, insertion_index) do
Enum.to_list(enumerable)
end
def slide(_, first..last//_, insertion_index)
def slide(_, first..last, insertion_index)
when insertion_index > first and insertion_index <= last do
raise ArgumentError,
"insertion index for slide must be outside the range being moved " <>
"(tried to insert #{first}..#{last} at #{insertion_index})"
end
def slide(enumerable, first..last//_, _insertion_index) when first > last do
def slide(enumerable, first..last, _insertion_index) when first > last do
Enum.to_list(enumerable)
end
# Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last)
def slide(enumerable, first..last//_, insertion_index) do
def slide(enumerable, first..last, insertion_index) do
impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4
cond do
@@ -2808,7 +2790,7 @@ defmodule Enum do
# Then at the end, we're going to reassemble and reverse them, and end up with the
# chunks in the correct order.
{_size, pre, post} =
reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
{pre, post} =
cond do
index < start -> {[item | pre], post}
@@ -2920,11 +2902,11 @@ defmodule Enum do
the default from Erlang/OTP 22:
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {11, 22, 33})
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.shuffle([1, 2, 3])
[3, 2, 1]
iex> Enum.shuffle([1, 2, 3])
[2, 1, 3]
iex> Enum.shuffle([1, 2, 3])
[2, 3, 1]
"""
@spec shuffle(t) :: list
@@ -2934,12 +2916,9 @@ defmodule Enum do
[{:rand.uniform(), x} | acc]
end)
shuffle_unwrap(:lists.keysort(1, randomized))
shuffle_unwrap(:lists.keysort(1, randomized), [])
end
defp shuffle_unwrap([{_, h} | rest]), do: [h | shuffle_unwrap(rest)]
defp shuffle_unwrap([]), do: []
@doc """
Returns a subset list of the given `enumerable` by `index_range`.
@@ -3353,7 +3332,7 @@ defmodule Enum do
def sort_by(enumerable, mapper, :desc) when is_function(mapper, 1) do
enumerable
|> reduce([], &[{&1, mapper.(&1)} | &2])
|> Enum.reduce([], &[{&1, mapper.(&1)} | &2])
|> List.keysort(1, :asc)
|> List.foldl([], &[elem(&1, 0) | &2])
end
@@ -3458,8 +3437,6 @@ defmodule Enum do
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.sum_by/2` instead.
## Examples
iex> Enum.sum([1, 2, 3])
@@ -3486,47 +3463,11 @@ defmodule Enum do
reduce(enumerable, 0, &+/2)
end
@doc """
Maps and sums the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.sum_by([%{count: 1}, %{count: 2}, %{count: 3}], fn x -> x.count end)
6
iex> Enum.sum_by(1..3, fn x -> x ** 2 end)
14
iex> Enum.sum_by([], fn x -> x.count end)
0
Filtering can be achieved by returning `0` to ignore elements:
iex> Enum.sum_by([1, -2, 3], fn x -> if x > 0, do: x, else: 0 end)
4
"""
@doc since: "1.18.0"
@spec sum_by(t, (element -> number)) :: number
def sum_by(enumerable, mapper)
def sum_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
sum_by_list(list, mapper, 0)
end
def sum_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 0, fn x, acc -> acc + mapper.(x) end)
end
@doc """
Returns the product of all elements.
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.product_by/2` instead.
## Examples
iex> Enum.product([])
@@ -3543,40 +3484,6 @@ defmodule Enum do
reduce(enumerable, 1, &*/2)
end
@doc """
Maps and computes the product of the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.product_by([%{count: 2}, %{count: 4}, %{count: 3}], fn x -> x.count end)
24
iex> Enum.product_by(1..3, fn x -> x ** 2 end)
36
iex> Enum.product_by([], fn x -> x.count end)
1
Filtering can be achieved by returning `1` to ignore elements:
iex> Enum.product_by([2, -1, 3], fn x -> if x > 0, do: x, else: 1 end)
6
"""
@doc since: "1.18.0"
@spec product_by(t, (element -> number)) :: number
def product_by(enumerable, mapper)
def product_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
product_by_list(list, mapper, 1)
end
def product_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 1, fn x, acc -> acc * mapper.(x) end)
end
@doc """
Takes an `amount` of elements from the beginning or the end of the `enumerable`.
@@ -3681,115 +3588,101 @@ defmodule Enum do
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.take_random(1..10, 2)
[6, 1]
[3, 1]
iex> Enum.take_random(?a..?z, 5)
~c"bkzmt"
'mikel'
"""
@spec take_random(t, non_neg_integer) :: list
def take_random(enumerable, count)
def take_random(_enumerable, 0), do: []
def take_random([], _), do: []
def take_random([h | t], 1), do: take_random_list_one(t, h, 1)
def take_random(enumerable, 1) do
enumerable
|> reduce({0, 0, 1.0, nil}, fn
elem, {idx, idx, w, _current} ->
{jdx, w} = take_jdx_w(idx, w, 1)
{idx + 1, jdx, w, elem}
|> reduce([], fn
x, [current | index] ->
if :rand.uniform(index + 1) == 1 do
[x | index + 1]
else
[current | index + 1]
end
_elem, {idx, jdx, w, current} ->
{idx + 1, jdx, w, current}
x, [] ->
[x | 1]
end)
|> case do
{0, 0, 1.0, nil} -> []
{_idx, _jdx, _w, current} -> [current]
[] -> []
[current | _index] -> [current]
end
end
def take_random(enumerable, count) when count in 0..128 do
def take_random(enumerable, count) when is_integer(count) and count in 0..128 do
sample = Tuple.duplicate(nil, count)
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> put_elem(idx, elem(sample, rand)) |> put_elem(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_index(idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = elem(sample, jdx)
{idx + 1, put_elem(sample, idx, value) |> put_elem(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, put_elem(sample, pos, elem)}
jdx < count ->
{idx + 1, put_elem(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, sample}, reducer)
if count < size do
Tuple.to_list(sample)
else
take_tupled(sample, size, [])
end
{size, sample} = reduce(enumerable, {0, sample}, reducer)
sample |> Tuple.to_list() |> take(Kernel.min(count, size))
end
def take_random(enumerable, count) when is_integer(count) and count >= 0 do
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> Map.put(idx, Map.get(sample, rand)) |> Map.put(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_index(idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = Map.get(sample, jdx)
{idx + 1, Map.put(sample, idx, value) |> Map.put(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, %{sample | pos => elem}}
jdx < count ->
{idx + 1, Map.put(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, %{}}, reducer)
take_mapped(sample, Kernel.min(count, size), [])
{size, sample} = reduce(enumerable, {0, %{}}, reducer)
take_random(sample, Kernel.min(count, size), [])
end
@compile {:inline, take_jdx_w: 3, take_index: 1}
defp take_jdx_w(idx, w, count) do
w = w * :math.exp(:math.log(:rand.uniform()) / count)
jdx = idx + floor(:math.log(:rand.uniform()) / :math.log(1 - w)) + 1
{jdx, w}
end
defp take_random(_sample, 0, acc), do: acc
defp take_index(0), do: 0
defp take_index(idx), do: :rand.uniform(idx + 1) - 1
defp take_tupled(_sample, 0, acc), do: acc
defp take_tupled(sample, position, acc) do
defp take_random(sample, position, acc) do
position = position - 1
take_tupled(sample, position, [elem(sample, position) | acc])
take_random(sample, position, [Map.get(sample, position) | acc])
end
defp take_mapped(_sample, 0, acc), do: acc
defp take_mapped(sample, position, acc) do
position = position - 1
take_mapped(sample, position, [Map.fetch!(sample, position) | acc])
defp take_random_list_one([h | t], current, index) do
if :rand.uniform(index + 1) == 1 do
take_random_list_one(t, h, index + 1)
else
take_random_list_one(t, current, index + 1)
end
end
defp take_random_list_one([], current, _), do: [current]
defp random_index(0), do: 0
defp random_index(idx), do: :rand.uniform(idx + 1) - 1
@doc """
Takes the elements from the beginning of the `enumerable` while `fun` returns
a truthy value.
@@ -3837,9 +3730,6 @@ defmodule Enum do
@doc """
Enumerates the `enumerable`, removing all duplicate elements.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
## Examples
iex> Enum.uniq([1, 2, 3, 3, 2, 1])
@@ -3865,8 +3755,7 @@ defmodule Enum do
considered duplicates if the return value of `fun` is equal for
both of them.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
The first occurrence of each element is kept.
## Example
@@ -3934,13 +3823,15 @@ defmodule Enum do
@doc """
Returns the `enumerable` with each element wrapped in a tuple
alongside its index or according to a given function.
alongside its index.
If an integer offset is given as `fun_or_offset`, it will index from the given
offset instead of from zero.
May receive a function or an integer offset.
If a function is given as `fun_or_offset`, it will index by invoking the function
for each element and index (zero-based) of the enumerable.
If an `offset` is given, it will index from the given offset instead of from
zero.
If a `function` is given, it will index by invoking the function for each
element and index (zero-based) of the enumerable.
## Examples
@@ -3982,10 +3873,6 @@ defmodule Enum do
Zips corresponding elements from two enumerables into a list
of tuples.
Because a list of two-element tuples with atoms as the first
tuple element is a keyword list (`Keyword`), zipping a first list
of atoms with a second list of any kind creates a keyword list.
The zipping finishes as soon as either enumerable completes.
## Examples
@@ -3993,9 +3880,6 @@ defmodule Enum do
iex> Enum.zip([1, 2, 3], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
iex> Enum.zip([:a, :b, :c], [1, 2, 3])
[a: 1, b: 2, c: 3]
iex> Enum.zip([1, 2, 3, 4, 5], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
@@ -4116,7 +4000,7 @@ defmodule Enum do
@doc """
Reduces over two enumerables halting as soon as either enumerable is empty.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(left, right), acc, reducer)
@@ -4140,7 +4024,7 @@ defmodule Enum do
def zip_reduce(left, right, acc, reducer) when is_function(reducer, 3) do
reduce = fn [l, r], acc -> {:cont, reducer.(l, r, acc)} end
R.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
end
@doc """
@@ -4150,7 +4034,7 @@ defmodule Enum do
The reducer will receive 2 args: a list of elements (one from each enum) and the
accumulator.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(enums), acc, reducer)
@@ -4175,7 +4059,7 @@ defmodule Enum do
def zip_reduce([], acc, reducer) when is_function(reducer, 2), do: acc
def zip_reduce(enums, acc, reducer) when is_function(reducer, 2) do
R.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
Stream.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
end
## Helpers
@@ -4440,9 +4324,6 @@ defmodule Enum do
defp flat_map_list([head | tail], fun) do
case fun.(head) do
# the two first clauses are an optimization
[] -> flat_map_list(tail, fun)
[elem] -> [elem | flat_map_list(tail, fun)]
list when is_list(list) -> list ++ flat_map_list(tail, fun)
other -> to_list(other) ++ flat_map_list(tail, fun)
end
@@ -4555,6 +4436,14 @@ defmodule Enum do
[acc | scan_list(rest, acc, fun)]
end
## shuffle
defp shuffle_unwrap([{_, h} | enumerable], t) do
shuffle_unwrap(enumerable, [h | t])
end
defp shuffle_unwrap([], t), do: t
## slice
defp slice_forward(enumerable, start, amount, step) when start < 0 do
@@ -4584,16 +4473,8 @@ defmodule Enum do
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
enumerable |> fun.() |> slice_exact(start, amount, step, count)
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
# TODO: Remove me on v2.0.
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
amount = Kernel.min(amount, count - start)
if step == 1 do
@@ -4603,6 +4484,10 @@ defmodule Enum do
|> take_every_list(amount, step - 1)
end
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
{:error, module} ->
slice_enum(enumerable, module, start, amount, step)
end
@@ -4661,18 +4546,11 @@ defmodule Enum do
defp slice_count_and_fun(enumerable, step) do
case Enumerable.slice(enumerable) do
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:ok, count, fun} when is_function(fun, 3) ->
{count, fun}
# TODO: Remove me on v2.0
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
if step == 1 do
{count, fn start, amount, 1 -> fun.(start, amount) end}
else
@@ -4683,6 +4561,9 @@ defmodule Enum do
end}
end
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:error, module} ->
{list, count} =
enumerable
@@ -4858,16 +4739,6 @@ defmodule Enum do
{:lists.reverse(acc), []}
end
## sum_by
defp sum_by_list([], _, acc), do: acc
defp sum_by_list([h | t], mapper, acc), do: sum_by_list(t, mapper, acc + mapper.(h))
## product_by
defp product_by_list([], _, acc), do: acc
defp product_by_list([h | t], mapper, acc), do: product_by_list(t, mapper, acc * mapper.(h))
## take
defp take_list(_list, 0), do: []
+110 -188
View File
@@ -1,18 +1,11 @@
defmodule Exception do
@moduledoc """
Functions for dealing with throw/catch/exit and exceptions.
Functions to format throw/catch/exit and exceptions.
This module also defines the behaviour required by custom
exceptions. To define your own, see `defexception/1`.
Note that stacktraces in Elixir are only available inside
catch and rescue by using the `__STACKTRACE__/0` variable.
## Formatting functions
Several functions in this module help format exceptions.
Some of these functions expect the stacktrace as argument.
The stacktrace is typically available inside catch and
rescue by using the `__STACKTRACE__/0` variable.
Do not rely on the particular format returned by the
Do not rely on the particular format returned by the `format*`
functions in this module. They may be changed in future releases
in order to better suit Elixir's tool chain. In other words,
by using the functions in this module it is guaranteed you will
@@ -49,7 +42,7 @@ defmodule Exception do
@doc """
Receives the exception struct and must return its message.
Many exceptions have a message field which by default is accessed
Most commonly exceptions have a message field which by default is accessed
by this function. However, if an exception does not have a message field,
this function must be explicitly implemented.
"""
@@ -346,7 +339,7 @@ defmodule Exception do
defp rewrite_arg(arg) do
Macro.prewalk(arg, fn
{:%{}, meta, [__struct__: Range, first: first, last: last, step: step]} ->
{:..//, meta, [first, last, step]}
{:"..//", meta, [first, last, step]}
other ->
other
@@ -407,8 +400,8 @@ defmodule Exception do
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
@@ -1048,6 +1041,44 @@ defmodule ArgumentError do
"""
defexception message: "argument error"
@impl true
def blame(
exception,
[{:erlang, :apply, [module, function, args], _} | _] = stacktrace
) do
message =
cond do
not proper_list?(args) ->
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)} " <>
"with arguments #{inspect(args)}. Arguments (the third argument of apply) must always be a proper list"
# Note that args may be an empty list even if they were supplied
not is_atom(module) and is_atom(function) and args == [] ->
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as module.function(), " <>
"make sure the left-hand side of the dot is a module atom"
not is_atom(module) ->
"you attempted to apply a function on #{inspect(module)}. " <>
"Modules (the first argument of apply) must always be an atom"
not is_atom(function) ->
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)}. " <>
"However, #{inspect(function)} is not a valid function name. Function names (the second argument " <>
"of apply) must always be an atom"
end
{%{exception | message: message}, stacktrace}
end
def blame(exception, stacktrace) do
{exception, stacktrace}
end
defp proper_list?(list) when length(list) >= 0, do: true
defp proper_list?(_), do: false
end
defmodule ArithmeticError do
@@ -1212,7 +1243,7 @@ defmodule SyntaxError do
})
when not is_nil(snippet) and not is_nil(column) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
format_message(file, line, column, snippet)
end
@@ -1225,7 +1256,7 @@ defmodule SyntaxError do
description: description
}) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil)
padded = " " <> String.replace(snippet, "\n", "\n ")
format_message(file, line, column, padded)
@@ -1316,7 +1347,7 @@ defmodule TokenMissingError do
description: description
}) do
snippet =
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
format_message(file, line, column, snippet)
end
@@ -1581,15 +1612,13 @@ defmodule UndefinedFunctionError do
"""
@function_threshold 0.77
@max_suggestions 5
defexception [:module, :function, :arity, :reason, :message]
@impl true
def message(%{message: nil} = exception) do
%{reason: reason, module: module, function: function, arity: arity} = exception
{message, hint_type} = message(reason, module, function, arity)
message <> if(hint_type == :suggest_module, do: hint_for_missing_alias(module), else: "")
{message, _hint_type} = message(reason, module, function, arity)
message
end
def message(%{message: message}) do
@@ -1650,25 +1679,12 @@ defmodule UndefinedFunctionError do
end
defp hint(module, function, arity, :suggest_function) do
hint_for_behaviour(module, function, arity) <>
hint_for_loaded_module(module, function, arity)
behaviour_hint(module, function, arity) <>
hint_for_loaded_module(module, function, arity, nil)
end
@max_suggestions 5
defp hint(module, function, arity, :suggest_module) do
hint_for_missing_module(module, function, arity)
end
defp hint_for_missing_alias(module) do
with "Elixir." <> rest <- Atom.to_string(module),
false <- rest =~ "." do
". Make sure the module name is correct and has been specified in full (or that an alias has been defined)"
else
_ -> ""
end
end
@doc false
def hint_for_missing_module(module, function, arity) do
downcased_module = downcase_module_name(module)
stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "")
@@ -1689,9 +1705,9 @@ defmodule UndefinedFunctionError do
["\n * ", Exception.format_mfa(module, function, arity)]
end)
". Did you mean:\n#{suggestions}\n"
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
else
hint_for_missing_alias(module)
""
end
end
@@ -1708,7 +1724,7 @@ defmodule UndefinedFunctionError do
end
@doc false
def hint_for_loaded_module(module, function, arity) do
def hint_for_loaded_module(module, function, arity, exports) do
cond do
macro_exported?(module, function, arity) ->
". However, there is a macro with the same name and arity. " <>
@@ -1718,7 +1734,7 @@ defmodule UndefinedFunctionError do
", #{message}"
true ->
IO.iodata_to_binary(did_you_mean(module, function))
IO.iodata_to_binary(did_you_mean(module, function, exports))
end
end
@@ -1729,39 +1745,11 @@ defmodule UndefinedFunctionError do
end
end
defp hint_for_behaviour(module, function, arity) do
case behaviours_for(module) do
[] ->
""
@function_threshold 0.77
@max_suggestions 5
behaviours ->
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
nil -> ""
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
end
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> ""
end
defp behaviours_for(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
end
defp expects_callback?(behaviour, function, arity) do
callbacks =
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
Enum.member?(callbacks, {function, arity})
end
## Shared helpers across hints
defp did_you_mean(module, function) do
exports = exports_for(module)
defp did_you_mean(module, function, exports) do
exports = exports || exports_for(module)
result =
case Keyword.take(exports, [function]) do
@@ -1791,6 +1779,35 @@ defmodule UndefinedFunctionError do
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
end
defp behaviour_hint(module, function, arity) do
case behaviours_for(module) do
[] ->
""
behaviours ->
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
nil -> ""
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
end
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> ""
end
defp behaviours_for(module) do
:attributes
|> module.module_info()
|> Keyword.get(:behaviour, [])
end
defp expects_callback?(behaviour, function, arity) do
callbacks =
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
Enum.member?(callbacks, {function, arity})
end
defp exports_for(module) do
if function_exported?(module, :__info__, 1) do
module.__info__(:macros) ++ module.__info__(:functions)
@@ -1799,7 +1816,8 @@ defmodule UndefinedFunctionError do
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> []
UndefinedFunctionError ->
[]
end
defp deprecated_functions_for(module) do
@@ -1810,7 +1828,8 @@ defmodule UndefinedFunctionError do
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError -> []
UndefinedFunctionError ->
[]
end
end
@@ -1997,23 +2016,8 @@ defmodule Protocol.UndefinedError do
@impl true
def message(%{protocol: protocol, value: value, description: description}) do
inspected =
value
|> inspect(pretty: true)
# Indent only lines with contents on them
|> String.replace(~r/^(?=.+)/m, " ")
"protocol #{inspect(protocol)} not implemented for type " <>
value_type(value) <>
maybe_description(description) <>
maybe_available(protocol) <>
"""
Got value:
#{inspected}
"""
"protocol #{inspect(protocol)} not implemented for #{inspect(value)} of type " <>
value_type(value) <> maybe_description(description) <> maybe_available(protocol)
end
defp value_type(%{__struct__: struct}), do: "#{inspect(struct)} (a struct)"
@@ -2070,17 +2074,10 @@ defmodule KeyError do
defp message(key, term) do
message = "key #{inspect(key)} not found"
cond do
term == nil ->
message
is_atom(term) and is_atom(key) ->
message <>
" in: #{inspect(term)} (if instead you want to invoke #{inspect(term)}.#{key}(), " <>
"make sure to add parentheses after the function name)"
true ->
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
if term != nil do
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
else
message
end
end
@@ -2089,6 +2086,11 @@ defmodule KeyError do
{exception, stacktrace}
end
def blame(exception = %{term: nil}, stacktrace) do
message = message(exception.key, exception.term)
{%{exception | message: message}, stacktrace}
end
def blame(exception, stacktrace) do
%{term: term, key: key} = exception
message = message(key, term)
@@ -2158,56 +2160,6 @@ defmodule UnicodeConversionError do
end
end
defmodule MissingApplicationsError do
@moduledoc """
An exception that is raised when an application depends on one or more
missing applications.
This exception is used by Mix and other tools. It can also be used by library authors
when their library only requires an external application (like a dependency) for a subset
of features.
The fields of this exception are public. See `t:t/0`.
*Available since v1.18.0.*
## Examples
unless Application.spec(:plug, :vsn) do
raise MissingApplicationsError,
description: "application :plug is required for testing Plug-related functionality",
apps: [{:plug, "~> 1.0"}]
end
"""
@moduledoc since: "1.18.0"
@type t() :: %__MODULE__{
apps: [{Application.app(), Version.requirement()}, ...],
description: String.t()
}
defexception apps: [], description: "missing applications found"
@impl true
def message(%__MODULE__{apps: apps, description: description}) do
# We explicitly format these as tuples so that they're easier to copy-paste
# into dependencies.
formatted_apps =
Enum.map(apps, fn {app_name, requirement} ->
~s(\n {#{inspect(app_name)}, "#{requirement}"})
end)
"""
#{description}
To address this, include these applications as your dependencies:
#{formatted_apps}\
"""
end
end
defmodule Enum.OutOfBoundsError do
@moduledoc """
An exception that is raised when a function expects an enumerable to have
@@ -2355,39 +2307,9 @@ defmodule ErlangError do
@doc false
def normalize(:badarg, stacktrace) do
case stacktrace do
[{:erlang, :apply, [module, function, args], _} | _] when not is_atom(module) ->
message =
cond do
is_map(module) and is_atom(function) and is_map_key(module, function) ->
"you attempted to apply a function named #{inspect(function)} on a map/struct. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
if is_function(module[function]) do
"If you are trying to invoke an anonymous function in a map/struct, " <>
"add a dot between the function name and the parenthesis: map.#{function}.()"
else
"If you are using the dot syntax, ensure there are no parentheses " <>
"after the field name, such as map.#{function}"
end
is_atom(function) and args == [] ->
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
"If you are using the dot syntax, such as module.function(), " <>
"make sure the left-hand side of the dot is an atom representing a module"
true ->
"you attempted to apply a function on #{inspect(module)}. " <>
"Modules (the first argument of apply) must always be an atom"
end
%ArgumentError{message: message}
_ ->
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
:error -> %ArgumentError{}
end
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
:error -> %ArgumentError{}
end
end
@@ -2530,7 +2452,7 @@ defmodule ErlangError do
{:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])}
general = extra[:general] ->
{:ok, reason, ": " <> IO.chardata_to_string(general)}
{:ok, reason, ": " <> general}
true ->
:error
+29 -44
View File
@@ -101,7 +101,6 @@ defmodule File do
@type posix :: :file.posix()
@type io_device :: :file.io_device()
@type file_descriptor :: :file.fd()
@type stat_options :: [time: :local | :universal | :posix]
@type mode ::
:append
@@ -759,7 +758,7 @@ defmodule File do
Note: The command `mv` in Unix-like systems behaves differently depending on
whether `source` is a file and the `destination` is an existing directory.
We have chosen to explicitly disallow this behavior.
We have chosen to explicitly disallow this behaviour.
## Examples
@@ -823,7 +822,7 @@ defmodule File do
The function receives arguments for `source_file` and `destination_file`. It should
return `true` if the existing file should be overwritten, `false` if otherwise.
The default callback returns `true`. On earlier versions, this callback could be
given as third argument, but such behavior is now deprecated.
given as third argument, but such behaviour is now deprecated.
"""
@spec cp(Path.t(), Path.t(), on_conflict: on_conflict_callback) :: :ok | {:error, posix}
@@ -896,7 +895,7 @@ defmodule File do
Note: The command `cp` in Unix-like systems behaves differently depending on
whether `destination` is an existing directory or not. We have chosen to
explicitly disallow this behavior. If `source` is a `file` and `destination`
explicitly disallow this behaviour. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
## Options
@@ -905,7 +904,7 @@ defmodule File do
The function receives arguments for `source` and `destination`. It should return
`true` if the existing file should be overwritten, `false` if otherwise. The default
callback returns `true`. On earlier versions, this callback could be given as third
argument, but such behavior is now deprecated.
argument, but such behaviour is now deprecated.
* `:dereference_symlinks` - (since v1.14.0) By default, this function will copy symlinks
by creating symlinks that point to the same location. This option forces symlinks to be
@@ -1364,11 +1363,6 @@ defmodule File do
{:ok, _} ->
{:ok, :regular}
{:error, :eio} when major == :win32 ->
# unix domain socket returns `{:error, :eio}`
# on other platforms the result is `{:ok, :regular}`
{:ok, :regular}
{:error, reason} ->
{:error, reason}
end
@@ -1395,6 +1389,13 @@ defmodule File do
@doc ~S"""
Opens the given `path`.
In order to write and read files, one must use the functions
in the `IO` module. By default, a file is opened in `:binary` mode,
which requires the functions `IO.binread/2` and `IO.binwrite/2`
to interact with the file. A developer may pass `:utf8` as an
option when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
`modes_or_function` can either be a list of modes or a function. If it's a
list, it's considered to be a list of modes (that are documented below). If
it's a function, then it's equivalent to calling `open(path, [],
@@ -1403,8 +1404,8 @@ defmodule File do
The allowed modes:
* `:binary` - opens the file in binary mode, disabling special handling of
Unicode sequences (default mode).
* `:binary` - opens the file in binary mode, disabling special handling of Unicode sequences
(default mode).
* `:read` - the file, which must exist, is opened for reading.
@@ -1445,33 +1446,19 @@ defmodule File do
This function returns:
* `{:ok, io_device | file_descriptor}` - the file has been opened in
the requested mode. We explore the differences between these two results
in the following section
* `{:ok, io_device}` - the file has been opened in the requested mode.
* `{:error, reason}` - the file could not be opened due to `reason`.
`io_device` is actually the PID of the process which handles the file.
This process monitors the process that originally opened the file (the
owner process). If the owner process terminates, the file is closed and
the process itself terminates too. If any process to which the `io_device`
is linked terminates, the file will be closed and the process itself will
be terminated.
## IO devices
An `io_device` returned from this call can be used as an argument to the
`IO` module functions.
By default, this function returns an IO device. An `io_device` is
a process which handles the file and you can interact with it using
the functions in the `IO` module. By default, a file is opened in
`:binary` mode, which requires the functions `IO.binread/2` and
`IO.binwrite/2` to interact with the file. A developer may pass `:utf8`
as a mode when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
Given the IO device is a file, if the owner process terminates,
the file is closed and the process itself terminates too. If any
process to which the `io_device` is linked terminates, the file
will be closed and the process itself will be terminated.
## File descriptors
When the `:raw` or `:ram` modes are given, this function returns
a low-level file descriptors. This avoids creating a process but
requires using the functions in the [`:file`](`:file`) module to
interact with it.
* `{:error, reason}` - the file could not be opened.
## Examples
@@ -1480,9 +1467,8 @@ defmodule File do
File.close(file)
"""
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device | file_descriptor} | {:error, posix}
@spec open(Path.t(), (io_device | file_descriptor -> res)) :: {:ok, res} | {:error, posix}
when res: var
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device} | {:error, posix}
@spec open(Path.t(), (io_device -> res)) :: {:ok, res} | {:error, posix} when res: var
def open(path, modes_or_function \\ [])
def open(path, modes) when is_list(modes) do
@@ -1516,8 +1502,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) ::
{:ok, res} | {:error, posix}
@spec open(Path.t(), [mode | :ram], (io_device -> res)) :: {:ok, res} | {:error, posix}
when res: var
def open(path, modes, function) when is_list(modes) and is_function(function, 1) do
case open(path, modes) do
@@ -1539,8 +1524,8 @@ defmodule File do
See `open/2` for the list of available modes.
"""
@spec open!(Path.t(), [mode | :ram]) :: io_device | file_descriptor
@spec open!(Path.t(), (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram]) :: io_device
@spec open!(Path.t(), (io_device -> res)) :: res when res: var
def open!(path, modes_or_function \\ []) do
case open(path, modes_or_function) do
{:ok, io_device_or_function_result} ->
@@ -1559,7 +1544,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open!(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram], (io_device -> res)) :: res when res: var
def open!(path, modes, function) do
case open(path, modes, function) do
{:ok, function_result} ->
+13 -13
View File
@@ -60,19 +60,19 @@ defmodule File.Stat do
defstruct keys
@type t :: %__MODULE__{
size: non_neg_integer() | :undefined,
type: :device | :directory | :regular | :other | :symlink | :undefined,
access: :read | :write | :read_write | :none | :undefined,
atime: :calendar.datetime() | integer() | :undefined,
mtime: :calendar.datetime() | integer() | :undefined,
ctime: :calendar.datetime() | integer() | :undefined,
mode: non_neg_integer() | :undefined,
links: non_neg_integer() | :undefined,
major_device: non_neg_integer() | :undefined,
minor_device: non_neg_integer() | :undefined,
inode: non_neg_integer() | :undefined,
uid: non_neg_integer() | :undefined,
gid: non_neg_integer() | :undefined
size: non_neg_integer(),
type: :device | :directory | :regular | :other | :symlink,
access: :read | :write | :read_write | :none,
atime: :calendar.datetime() | integer(),
mtime: :calendar.datetime() | integer(),
ctime: :calendar.datetime() | integer(),
mode: non_neg_integer(),
links: non_neg_integer(),
major_device: non_neg_integer(),
minor_device: non_neg_integer(),
inode: non_neg_integer(),
uid: non_neg_integer(),
gid: non_neg_integer()
}
@doc """
+21 -21
View File
@@ -139,8 +139,6 @@ defmodule Float do
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
iex> Float.parse(".12")
:error
iex> Float.parse("pi")
:error
iex> Float.parse("1.7976931348623159e+308")
@@ -212,7 +210,7 @@ defmodule Float do
## Known issues
The behavior of `floor/2` for floats can be surprising. For example:
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
@@ -220,7 +218,7 @@ defmodule Float do
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behavior above.
which explains the behaviour above.
## Examples
@@ -256,7 +254,7 @@ defmodule Float do
The operation is performed on the binary floating point, without a
conversion to decimal.
The behavior of `ceil/2` for floats can be surprising. For example:
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
@@ -264,7 +262,7 @@ defmodule Float do
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behavior above.
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -309,7 +307,7 @@ defmodule Float do
## Known issues
The behavior of `round/2` for floats can be surprising. For example:
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
@@ -317,8 +315,8 @@ defmodule Float do
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
@@ -577,23 +575,27 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples
iex> Float.to_charlist(7.0)
~c"7.0"
'7.0'
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) do
:erlang.float_to_list(float, [:short])
def to_charlist(float) when is_float(float) do
:io_lib_format.fwrite_g(float)
end
@doc """
@@ -612,8 +614,6 @@ defmodule Float do
For a configurable representation, use `:erlang.float_to_binary/2`.
Inlined by the compiler.
## Examples
iex> Float.to_string(7.0)
@@ -621,8 +621,8 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) do
:erlang.float_to_binary(float, [:short])
def to_string(float) when is_float(float) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
@doc false
+3 -5
View File
@@ -22,7 +22,7 @@ defmodule GenEvent do
This approach has some shortcomings (it provides no back-pressure for example)
but can still replace GenEvent for low-profile usages of it. [This blog post
by José
Valim](https://dashbit.co/blog/replacing-genevent-by-a-supervisor-plus-genserver)
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
has more detailed information on this approach.
### GenStage
@@ -46,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) ::
{:ok, state}
| {:ok, state, :hibernate}
| {:error, reason :: term}
when state: term
| {:error, reason :: any}
when state: any
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state}
@@ -507,8 +507,6 @@ defmodule GenEvent do
{:ok, states, [name, handlers, hib]}
end
# Keeping deprecated format_status/2 since the current implementation is not
# compatible with format_status/1 and GenEvent is deprecated anyway
@doc false
def format_status(opt, status_data) do
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
+23 -37
View File
@@ -400,7 +400,7 @@ defmodule GenServer do
or is suspended, the parent PID, the debugger state, and the state of
the behaviour module, which includes the callback module state
(as returned by `:sys.get_state/2`). It's possible to change how this
status is represented by defining the optional `c:GenServer.format_status/1`
status is represented by defining the optional `c:GenServer.format_status/2`
callback.
* `:sys.trace/3` - prints all the system events to `:stdio`.
* `:sys.statistics/3` - manages collection of process statistics.
@@ -415,7 +415,7 @@ defmodule GenServer do
Let's see how we could use those functions for debugging the stack server
we defined earlier.
iex> {:ok, pid} = Stack.start_link("")
iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
@@ -523,8 +523,8 @@ defmodule GenServer do
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
| {:stop, reason :: any}
when state: any
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
@@ -759,39 +759,22 @@ defmodule GenServer do
when old_vsn: term | {:down, term}
@doc """
This function is called by a `GenServer` process in the following situations:
Invoked in some cases to retrieve a formatted version of the `GenServer` status:
* [`:sys.get_status/1,2`](`:sys.get_status/1`) is invoked to get the `GenServer` status.
* The `GenServer` process terminates abnormally and logs an error.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenServer`; in such cases, `reason` is `:normal`
This callback is used to limit the status of the process returned by
[`:sys.get_status/1,2`](`:sys.get_status/1`) or sent to logger.
* the `GenServer` terminates abnormally and logs an error; in such cases,
`reason` is `:terminate`
The callback gets a map `status` describing the current status and shall return
a map `new_status` with the same keys, but it may transform some values.
Two possible use cases for this callback is to remove sensitive information
from the state to prevent it from being printed in log files, or to compact
large irrelevant status items that would only clutter the logs.
## Example
@impl GenServer
def format_status(status) do
Map.new(status, fn
{:state, state} -> {:state, Map.delete(state, :private_key)}
{:message, {:password, _}} -> {:message, {:password, "redacted"}}
key_value -> key_value
end)
end
This callback can be useful to control the *appearance* of the status of the
`GenServer`. For example, it can be used to return a compact representation of
the `GenServer`'s state to avoid having large state terms printed.
`pdict_and_state` is a two-elements list `[pdict, state]` where `pdict` is a
list of `{key, value}` tuples representing the current process dictionary of
the `GenServer` and `state` is the current state of the `GenServer`.
"""
@doc since: "1.17.0"
@callback format_status(status :: :gen_server.format_status()) ::
new_status :: :gen_server.format_status()
# TODO: Remove this on v2.0
@doc deprecated: "Use format_status/1 callback instead"
@callback format_status(reason, pdict_and_state :: list) :: term
when reason: :normal | :terminate
@@ -800,7 +783,6 @@ defmodule GenServer do
handle_info: 2,
handle_cast: 2,
handle_call: 3,
format_status: 1,
format_status: 2,
handle_continue: 2
@@ -845,7 +827,7 @@ defmodule GenServer do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -945,7 +927,7 @@ defmodule GenServer do
end
defmacro __before_compile__(env) do
if not Module.defines?(env.module, {:init, 1}) do
unless Module.defines?(env.module, {:init, 1}) do
message = """
function init/1 required by behaviour GenServer is not implemented \
(in module #{inspect(env.module)}).
@@ -1018,7 +1000,7 @@ defmodule GenServer do
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, term, options) :: on_start
@spec start_link(module, any, options) :: on_start
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:link, module, init_arg, options)
end
@@ -1028,7 +1010,7 @@ defmodule GenServer do
See `start_link/3` for more information.
"""
@spec start(module, term, options) :: on_start
@spec start(module, any, options) :: on_start
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:nolink, module, init_arg, options)
end
@@ -1120,6 +1102,10 @@ defmodule GenServer do
nil ->
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
# TODO: remove this clause when we require Erlang/OTP 25+
pid when pid == self() ->
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
pid ->
try do
:gen.call(pid, :"$gen_call", request, timeout)
+5 -5
View File
@@ -258,13 +258,13 @@ end
defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
@@ -274,7 +274,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, module.size(dict)}
end
@@ -286,7 +286,7 @@ end
defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -303,7 +303,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end
end
+5 -5
View File
@@ -272,19 +272,19 @@ end
defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.size(set)}
end
@@ -296,7 +296,7 @@ end
defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
@@ -313,7 +313,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end
end
+112 -158
View File
@@ -139,85 +139,6 @@ defprotocol Inspect do
# Handle structs in Any
@fallback_to_any true
@impl true
defmacro __deriving__(module, options) do
info = Macro.struct_info!(module, __CALLER__)
fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__])
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == Enum.sort(only) and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
filtered_guard =
quote do
var!(field) in unquote(filtered_fields)
end
field_guard =
if optional == [] do
filtered_guard
else
optional_map =
for field <- optional, into: %{} do
default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil)
{field, default}
end
quote do
unquote(filtered_guard) and
not case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
unquote(field_guard),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
@doc """
Converts `term` into an algebra document.
@@ -234,7 +155,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -254,7 +175,7 @@ defimpl Inspect, for: BitString do
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color_doc(IO.iodata_to_binary(inspected), :string, opts)
color(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
@@ -265,12 +186,12 @@ defimpl Inspect, for: BitString do
end
defp inspect_bitstring("", opts) do
color_doc("<<>>", :binary, opts)
color("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, opts) do
left = color_doc("<<", :binary, opts)
right = color_doc(">>", :binary, opts)
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
@@ -307,7 +228,7 @@ end
defimpl Inspect, for: List do
def inspect([], opts) do
color_doc("[]", :list, opts)
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
@@ -338,26 +259,19 @@ defimpl Inspect, for: List do
lists
end
open = color_doc("[", :list, opts)
sep = color_doc(",", :list, opts)
close = color_doc("]", :list, opts)
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
color(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
@@ -367,22 +281,9 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@@ -400,9 +301,9 @@ end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color_doc("{", :tuple, opts)
sep = color_doc(",", :tuple, opts)
close = color_doc("}", :tuple, opts)
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
@@ -421,7 +322,7 @@ defimpl Inspect, for: Map do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color_doc(" => ", :map, opts)
sep = color(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
@@ -438,9 +339,9 @@ defimpl Inspect, for: Map do
end
defp map_container_doc(list, name, opts, fun) do
open = color_doc("%" <> name <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
end
end
@@ -448,7 +349,7 @@ end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color_doc(inspected, :number, opts)
color(inspected, :number, opts)
end
defp base_to_value(base) do
@@ -486,47 +387,33 @@ defimpl Inspect, for: Float do
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
Float.to_charlist(float)
:io_lib_format.fwrite_g(float)
end
color_doc(IO.iodata_to_binary(formatted), :number, opts)
color(IO.iodata_to_binary(formatted), :number, opts)
end
end
defimpl Inspect, for: Regex do
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
case translate_options(regex_opts, []) do
:error ->
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
translated_opts ->
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color_doc(source, :regex, opts)
end
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
end
defp translate_options([:dotall, {:newline, :anycrlf} | t], acc),
do: translate_options(t, [?s | acc])
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
defp translate_options([:unicode, :ucp | t], acc), do: translate_options(t, [?u | acc])
defp translate_options([:caseless | t], acc), do: translate_options(t, [?i | acc])
defp translate_options([:extended | t], acc), do: translate_options(t, [?x | acc])
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, regex.opts])
color(source, :regex, opts)
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
@@ -562,7 +449,7 @@ defimpl Inspect, for: Function do
match?(@elixir_compiler ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__()}>"
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
@@ -647,6 +534,73 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == Enum.sort(only) and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
optional? =
if optional == [] do
false
else
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
quote do
case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
var!(field) in unquote(filtered_fields) and not unquote(optional?),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
def inspect(%module{} = struct, opts) do
try do
{module.__struct__(), module.__info__(:struct)}
@@ -668,9 +622,9 @@ defimpl Inspect, for: Any do
end
def inspect(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc(">", :map, opts)
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
+73 -123
View File
@@ -17,8 +17,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
When `:as_lists` all lists will be printed as lists.
@@ -333,7 +333,6 @@ defmodule Inspect.Algebra do
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
# Elixir + Inspect.Opts conveniences
# These have the _doc suffix.
@doc """
Converts an Elixir term to an algebra document
@@ -439,7 +438,7 @@ defmodule Inspect.Algebra do
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ [])
when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
@@ -456,7 +455,7 @@ defmodule Inspect.Algebra do
container_each(collection, inspect_opts.limit, inspect_opts, fun, [], break == :maybe)
flex? = simple? or break == :flex
docs = fold(docs, &join(&1, &2, flex?, separator))
docs = fold_doc(docs, &join(&1, &2, flex?, separator))
case flex? do
true -> group(concat(concat(left, nest(docs, 1)), right))
@@ -526,32 +525,6 @@ defmodule Inspect.Algebra do
container_doc(left, docs, right, inspect, fun, separator: separator)
end
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use color_doc/3 instead"
def color(doc, key, opts) do
color_doc(doc, key, opts)
end
@doc ~S"""
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color_doc(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color_doc(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, ansi(precolor)), doc_color(empty(), ansi(postcolor)))
else
doc
end
end
defp ansi(color) do
color
|> IO.ANSI.format_fragment(true)
|> IO.iodata_to_binary()
end
# Algebra API
@doc """
@@ -615,21 +588,6 @@ defmodule Inspect.Algebra do
doc_cons(doc1, doc2)
end
@doc ~S"""
Disable any rendering limit while rendering the given document.
## Examples
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
iex> Inspect.Algebra.format(doc, 10)
["hello", "\n", "world"]
iex> doc = Inspect.Algebra.no_limit(doc)
iex> Inspect.Algebra.format(doc, 10)
["hello", " ", "world"]
"""
@doc since: "1.14.0"
@spec no_limit(t) :: t
def no_limit(doc) do
doc_limit(doc, :infinity)
end
@@ -646,16 +604,21 @@ defmodule Inspect.Algebra do
"""
@spec concat([t]) :: t
def concat(docs) when is_list(docs) do
fold(docs, &concat(&1, &2))
fold_doc(docs, &concat(&1, &2))
end
@doc ~S"""
Colors a document with the given color (preceding the document itself).
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color(t, binary) :: t
def color(doc, color) when is_doc(doc) and is_binary(color) do
doc_color(doc, color)
@doc since: "1.4.0"
@spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
else
doc
end
end
@doc ~S"""
@@ -955,10 +918,6 @@ defmodule Inspect.Algebra do
@spec line(t, t) :: t
def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use fold/2 instead"
def fold_doc(docs, folder_fun), do: fold(docs, folder_fun)
@doc ~S"""
Folds a list of documents into a document using the given folder function.
@@ -970,22 +929,21 @@ defmodule Inspect.Algebra do
iex> docs = ["A", "B", "C"]
iex> docs =
...> Inspect.Algebra.fold(docs, fn doc, acc ->
...> Inspect.Algebra.fold_doc(docs, fn doc, acc ->
...> Inspect.Algebra.concat([doc, "!", acc])
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
"""
@doc since: "1.18.0"
@spec fold([t], (t, t -> t)) :: t
def fold(docs, folder_fun)
@spec fold_doc([t], (t, t -> t)) :: t
def fold_doc(docs, folder_fun)
def fold([], _folder_fun), do: empty()
def fold([doc], _folder_fun), do: doc
def fold_doc([], _folder_fun), do: empty()
def fold_doc([doc], _folder_fun), do: doc
def fold([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold(docs, folder_fun))
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold_doc(docs, folder_fun))
@doc ~S"""
Formats a given document for a given width.
@@ -1015,17 +973,20 @@ defmodule Inspect.Algebra do
#
# * flat - represents a document with breaks as flats (a break may fit, as it may break)
# * break - represents a document with breaks as breaks (a break always fits, since it breaks)
#
# The following modes are exclusive to fitting:
#
# * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode
# * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode
#
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
@spec fits(
@spec fits?(
width :: non_neg_integer() | :infinity,
column :: non_neg_integer(),
break? :: boolean(),
entries
) :: :fit | :no_fit | :break_next
) :: boolean()
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -1034,68 +995,63 @@ defmodule Inspect.Algebra do
#
# In case we have groups and the group fits, we need to consider the group
# parent without the child breaks, hence {:tail, b?, t} below.
defp fits(w, k, b?, _) when k > w and b?, do: :no_fit
defp fits(_, _, _, []), do: :fit
defp fits(w, k, _, {:tail, b?, t}), do: fits(w, k, b?, t)
defp fits?(w, k, b?, _) when k > w and b?, do: false
defp fits?(_, _, _, []), do: true
defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t)
## Flat no break
defp fits(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
## Breaks no flat
defp fits(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, x} | t])
when x == :doc_line or (is_tuple(x) and elem(x, 0) == :doc_break) do
case fits(w, k, b?, [{i, :flat, x} | t]) do
:no_fit -> :break_next
fits -> fits
end
end
defp fits?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true
## Breaks
defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit
defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit
defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true
defp fits(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
## Catch all
defp fits(w, _, _, [{i, _, :doc_line} | t]), do: fits(w, i, false, t)
defp fits(w, k, b?, [{_, _, :doc_nil} | t]), do: fits(w, k, b?, t)
defp fits(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits(w, i, b?, t)
defp fits(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits(w, k + l, b?, t)
defp fits(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits(w, k + byte_size(s), b?, t)
defp fits(_, _, _, [{_, _, doc_force(_)} | _]), do: :no_fit
defp fits(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits(w, k + byte_size(s), true, t)
defp fits(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(w, _, _, [{i, _, :doc_line} | t]), do: fits?(w, i, false, t)
defp fits?(w, k, b?, [{_, _, :doc_nil} | t]), do: fits?(w, k, b?, t)
defp fits?(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, b?, t)
defp fits?(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits?(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, b?, t)
defp fits?(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), b?, t)
defp fits?(_, _, _, [{_, _, doc_force(_)} | _]), do: false
defp fits?(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), true, t)
defp fits?(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits?(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits?(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits?(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits?(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits?(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits?(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits?(w, k, b?, [{i, m, x} | t])
@spec format(
width :: non_neg_integer() | :infinity,
@@ -1106,14 +1062,10 @@ defmodule Inspect.Algebra do
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x}, {i, m, y} | t])
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [c | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, k, [{i, :flat_no_break, doc_fits(x, :enabled)} | t]),
do: format(w, k, [{i, :break_no_flat, x} | t])
defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)
@@ -1121,7 +1073,7 @@ defmodule Inspect.Algebra do
defp format(w, k, [{i, m, doc_break(s, :flex)} | t]) do
k = k + byte_size(s)
if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do
if w == :infinity or m == :flat or fits?(w, k, true, t) do
[s | format(w, k, t)]
else
[indent(i) | format(w, i, t)]
@@ -1151,17 +1103,11 @@ defmodule Inspect.Algebra do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, :break_no_flat, doc_group(x, _)} | t]) do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
fits = if w == :infinity, do: :fit, else: fits(w, k, false, [{i, :flat, x}])
case fits do
:fit -> format(w, k, [{i, :flat, x} | t])
:no_fit -> format(w, k, [{i, :break, x} | t])
:break_next -> format(w, k, [{i, :flat_no_break, x} | t])
if w == :infinity or fits?(w, k, false, [{i, :flat, x}]) do
format(w, k, [{i, :flat, x} | t])
else
format(w, k, [{i, :break, x} | t])
end
end
@@ -1190,6 +1136,10 @@ defmodule Inspect.Algebra do
defp apply_nesting(_, _, :reset), do: 0
defp apply_nesting(i, _, j), do: i + j
defp ansi(color) do
IO.ANSI.format_fragment(color, true)
end
defp indent(0), do: @newline
defp indent(i), do: @newline <> :binary.copy(" ", i)
end
+17 -20
View File
@@ -161,7 +161,7 @@ defmodule Integer do
@doc since: "1.4.0"
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
@@ -189,16 +189,13 @@ defmodule Integer do
@spec digits(integer, pos_integer) :: [integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
case integer do
0 -> [0]
_integer -> digits(integer, base, [])
end
do_digits(integer, base, [])
end
defp digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp digits(integer, base, acc),
do: digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
@doc """
Returns the integer represented by the ordered `digits`.
@@ -220,16 +217,16 @@ defmodule Integer do
"""
@spec undigits([integer], pos_integer) :: integer
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
undigits(digits, base, 0)
do_undigits(digits, base, 0)
end
defp undigits([], _base, acc), do: acc
defp do_undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
@doc """
Parses a text representation of an integer.
@@ -365,25 +362,25 @@ defmodule Integer do
## Examples
iex> Integer.to_charlist(123)
~c"123"
'123'
iex> Integer.to_charlist(+456)
~c"456"
'456'
iex> Integer.to_charlist(-789)
~c"-789"
'-789'
iex> Integer.to_charlist(0123)
~c"123"
'123'
iex> Integer.to_charlist(100, 16)
~c"64"
'64'
iex> Integer.to_charlist(-100, 16)
~c"-64"
'-64'
iex> Integer.to_charlist(882_681_651, 36)
~c"ELIXIR"
'ELIXIR'
"""
@spec to_charlist(integer, 2..36) :: charlist
+17 -40
View File
@@ -12,8 +12,6 @@ defmodule IO do
(as shown in typespecs). For more information on chardata, see the
"IO data" section below.
The functions of this module use UNIX-style naming where possible.
## IO devices
An IO device may be an atom or a PID. In case it is an atom,
@@ -130,17 +128,8 @@ defmodule IO do
@doc """
Reads from the IO `device`.
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes. This should be the preferred mode for reading
non-textual inputs.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
The `device` is iterated by the given number of characters, line by line if
`:line` is given, or until `:eof`.
It returns:
@@ -156,10 +145,8 @@ defmodule IO do
@spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata
def read(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def read(device, :all) do
IO.warn("IO.read(device, :all) is deprecated, use IO.read(device, :eof) instead")
with :eof <- read(device, :eof) do
with [_ | _] = opts <- :io.getopts(device),
false <- Keyword.get(opts, :binary, true) do
@@ -188,14 +175,14 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes. This should be the preferred mode for reading
non-textual inputs.
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
* if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars`
can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`,
with the difference that `:all` returns `""` on end of file, while `:eof` returns
`:eof` itself.
It returns:
@@ -213,9 +200,8 @@ defmodule IO do
@spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def binread(device, :all) do
IO.warn("IO.binread(device, :all) is deprecated, use IO.binread(device, :eof) instead")
with :eof <- binread(device, :eof), do: ""
end
@@ -296,8 +282,6 @@ defmodule IO do
By default, the `device` is the standard output. It returns `:ok`
if it succeeds.
Trivia: `puts` is shorthand for `put string`.
## Examples
IO.puts("Hello World!")
@@ -394,7 +378,7 @@ defmodule IO do
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
if :elixir_config.warn(key, stacktrace) do
warn(message.(), stacktrace)
warn(message, stacktrace)
else
:ok
end
@@ -424,7 +408,7 @@ defmodule IO do
end
@doc """
Inspects and writes the given `item` to the standard output.
Inspects and writes the given `item` to the device.
It's important to note that it returns the given `item` unchanged.
This makes it possible to "spy" on values by inserting an
@@ -440,7 +424,6 @@ defmodule IO do
The label will be printed before the inspected `item`.
See `Inspect.Opts` for a full list of remaining formatting options.
To print to other IO devices, see `IO.inspect/3`
## Examples
@@ -573,8 +556,6 @@ defmodule IO do
for instance, `{:error, :estale}` if reading from an
NFS volume
Trivia: `gets` is shorthand for `get string`.
## Examples
To display "What is your name?" as a prompt and await user input:
@@ -606,9 +587,8 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of characters
or line by line if `:line` is given. In case `:line` is given,
"\r\n" is automatically normalized to "\n".
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
This reads from the IO as UTF-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
@@ -661,17 +641,14 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of bytes or line
by line if `:line` is given. In case `:line` is given, "\r\n"
is automatically normalized to "\n". Passing the number of bytes
should be the preferred mode for reading non-textual inputs.
The `device` is iterated by the given number of bytes or line by line if
`:line` is given. This reads from the IO device as a raw binary.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
This reads from the IO device as a raw binary. Therefore,
do not use this function on IO devices in Unicode mode as
it will return the wrong result.
Finally, do not use this function on IO devices in Unicode
mode as it will return the wrong result.
`binstream/0` has been introduced in Elixir v1.12.0,
while `binstream/2` has been available since v1.0.0.
+2 -2
View File
@@ -268,7 +268,7 @@ defmodule IO.ANSI do
The named sequences are represented by atoms.
It will also append an `IO.ANSI.reset/0` to the chardata when a conversion is
performed. If you don't want this behavior, use `format_fragment/2`.
performed. If you don't want this behaviour, use `format_fragment/2`.
An optional boolean parameter can be passed to enable or disable
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
@@ -299,7 +299,7 @@ defmodule IO.ANSI do
## Examples
iex> IO.ANSI.format_fragment([:bright, ~c"Word"], true)
iex> IO.ANSI.format_fragment([:bright, 'Word'], true)
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
"""
+9 -1
View File
@@ -221,6 +221,10 @@ defmodule IO.ANSI.Docs do
### Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
end
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
write_quote(lines, indent, options, true)
write_empty_quote_line(options)
@@ -350,7 +354,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
if !no_wrap, do: newline_after_block(options)
unless no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
@@ -361,6 +365,10 @@ defmodule IO.ANSI.Docs do
### Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
end
# Blank line between code blocks
defp process_code(["", " " <> line | rest], code, indent, options) do
process_code(rest, [line, "" | code], indent, options)
+5 -3
View File
@@ -1,9 +1,11 @@
defmodule IO.StreamError do
defexception [:reason]
defexception [:reason, :message]
@impl true
def message(%{reason: reason}) do
"error during streaming: #{inspect(reason)}"
def exception(opts) do
reason = opts[:reason]
formatted = IO.iodata_to_binary(:file.format_error(reason))
%IO.StreamError{message: "error during streaming: #{formatted}", reason: reason}
end
end
-430
View File
@@ -1,430 +0,0 @@
defprotocol JSON.Encoder do
@moduledoc """
A protocol for custom JSON encoding of data structures.
If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields or skip some fields via the
`:except` option:
@derive JSON.Encoder
defstruct ...
> #### Leaking Private Information {: .error}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.
Finally, if you don't own the struct you want to encode to JSON,
you may use `Protocol.derive/3` placed outside of any module:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)
"""
@undefined_impl_description """
the protocol must be explicitly implemented.
If you have a struct, you can derive the implementation specifying \
which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields, although this should be \
used carefully to avoid accidentally leaking private information \
when new fields are added:
@derive JSON.Encoder
defstruct ...
Finally, if you don't own the struct you want to encode to JSON, \
you may use Protocol.derive/3 placed outside of any module:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)\
"""
@impl true
defmacro __deriving__(module, opts) do
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
fields = fields_to_encode(fields, opts)
vars = Macro.generate_arguments(length(fields), __MODULE__)
kv = Enum.zip(fields, vars)
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
io = if io == [], do: "{}", else: io ++ [?}]
quote do
defimpl JSON.Encoder, for: unquote(module) do
def encode(%{unquote_splicing(kv)}, encoder) do
unquote(io)
end
end
end
end
defp fields_to_encode(fields, opts) do
cond do
only = Keyword.get(opts, :only) ->
case only -- fields do
[] ->
only
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
except = Keyword.get(opts, :except) ->
case except -- fields do
[] ->
fields -- [:__struct__ | except]
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
true ->
fields -- [:__struct__]
end
end
@doc """
A function invoked to encode the given term to `t:iodata/0`.
"""
def encode(term, encoder)
end
defimpl JSON.Encoder, for: Atom do
def encode(value, encoder) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
end
end
defimpl JSON.Encoder, for: Map do
def encode(value, encoder) do
case :maps.next(:maps.iterator(value)) do
:none ->
"{}"
{key, value, iterator} ->
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
defp next(iterator, encoder) do
case :maps.next(iterator) do
:none ->
"}"
{key, value, iterator} ->
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
# Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol.
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
def encode(value, _encoder) do
[?", @for.to_iso8601(value), ?"]
end
end
defmodule JSON.DecodeError do
@moduledoc """
The exception raised by `JSON.decode!/1`.
"""
defexception [:message, :offset, :data]
end
defmodule JSON do
@moduledoc ~S"""
JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
standards.
## Encoding
Elixir built-in data structures are encoded to JSON as follows:
| **Elixir** | **JSON** |
|----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures.
## Decoding
Elixir built-in data structures are decoded from JSON as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
"""
@moduledoc since: "1.18.0"
@type encoder :: (term(), encoder() -> iodata())
@type decode_error_reason ::
{:unexpected_end, non_neg_integer()}
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@doc ~S"""
Decodes the given JSON.
Returns `{:ok, decoded}` or `{:error, reason}`.
## Examples
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
{:ok, [nil, 123, "string", %{"key" => "value"}]}
## Error reasons
The error tuple will have one of the following reasons.
* `{:unexpected_end, offset}` if `binary` contains incomplete JSON value
* `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
* `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape
"""
@spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()}
def decode(binary) when is_binary(binary) do
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
if rest == "" do
{:ok, decoded}
else
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
end
end
end
@doc ~S"""
Decodes the given JSON with the given decoders.
Returns `{decoded, acc, rest}` or `{:error, reason}`.
See `decode/1` for the error reasons.
## Decoders
All decoders are optional. If not provided, they will fall back to
implementations used by the `decode/1` function:
* for `array_start`: `fn _ -> [] end`
* for `array_push`: `fn elem, acc -> [elem | acc] end`
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
* for `object_start`: `fn _ -> [] end`
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
* for `float`: `&String.to_float/1`
* for `integer`: `&String.to_integer/1`
* for `string`: `&Function.identity/1`
* for `null`: the atom `nil`
For streaming decoding, see Erlang's `:json` module.
"""
@spec decode(binary(), term(), keyword()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
:error, {:invalid_byte, byte} ->
{:error, {:invalid_byte, offset(__STACKTRACE__), byte}}
:error, {:unexpected_sequence, bytes} ->
{:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}}
end
end
defp offset(stacktrace) do
with [{_, _, _, opts} | _] <- stacktrace,
%{cause: %{position: position}} <- opts[:error_info] do
position
else
_ -> 0
end
end
@doc ~S"""
Decodes the given JSON but raises an exception in case of errors.
Returns the decoded content. See `decode/1` for possible errors.
## Examples
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
[nil, 123, "string", %{"key" => "value"}]
"""
@spec decode!(binary()) :: term()
def decode!(binary) when is_binary(binary) do
case decode(binary) do
{:ok, decoded} ->
decoded
{:error, {:unexpected_end, offset}} ->
raise JSON.DecodeError,
message: "unexpected end of JSON binary at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:invalid_byte, offset, byte}} ->
raise JSON.DecodeError,
message: "invalid byte #{byte} at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:unexpected_sequence, offset, bytes}} ->
raise JSON.DecodeError,
message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}",
data: binary,
offset: offset
end
end
@doc ~S"""
Encodes the given term to JSON as a binary.
The second argument is a function that is recursively
invoked to encode a term.
> #### IO and performance {: .tip}
>
> If you need to encode data to be sent over the network
> or written to the filesystem, consider using the more
> efficient `encode_to_iodata!/2`.
## Examples
iex> JSON.encode!([123, "string", %{key: "value"}])
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode!(term(), encoder()) :: binary()
def encode!(term, encoder \\ &protocol_encode/2) do
IO.iodata_to_binary(encoder.(term, encoder))
end
@doc ~S"""
Encodes the given term to JSON as an iodata.
This is the most efficient format if the JSON is going to be
used for IO purposes.
The second argument is a function that is recursively
invoked to encode a term.
## Examples
iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}])
iex> IO.iodata_to_binary(data)
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode_to_iodata!(term(), encoder()) :: iodata()
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
encoder.(term, encoder)
end
@doc """
This is the default encode implementation passed to `encode!/1`.
This function is most typically passed as second argument to
`encode!/2` and `encode_to_iodata!/2`. The default implementation
is an optimized dispatch to the `JSON.Encoder` protocol.
"""
@spec protocol_encode(term(), encoder()) :: iodata()
def protocol_encode(value, encoder) when is_atom(value) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
do: :elixir_json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder)
def protocol_encode(value, encoder),
do: JSON.Encoder.encode(value, encoder)
end
+267 -547
View File
File diff suppressed because it is too large Load Diff
+13 -35
View File
@@ -9,7 +9,6 @@ defmodule Kernel.CLI do
compile: [],
no_halt: false,
compiler_options: [],
warnings_as_errors: false,
errors: [],
verbose_compile: false,
profile: nil,
@@ -196,7 +195,7 @@ defmodule Kernel.CLI do
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@@ -295,16 +294,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]})
end
defp parse_argv([~c"--color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, true)
parse_argv(t, config)
end
defp parse_argv([~c"--no-color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, false)
parse_argv(t, config)
end
## Compiler
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
@@ -326,7 +315,8 @@ defmodule Kernel.CLI do
end
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | warnings_as_errors: true})
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
parse_argv(t, %{config | compiler_options: compiler_options})
end
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
@@ -337,8 +327,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | profile: :time})
end
## IEx
defp parse_argv([~c"--dbg", backend | t], %{mode: :iex} = config) do
case backend do
~c"pry" ->
@@ -356,21 +344,6 @@ defmodule Kernel.CLI do
defp parse_argv([~c"--dot-iex", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
defp parse_argv([~c"--remsh", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
## Erlang flags
defp parse_argv([~c"--boot", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--boot-var", _, _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--cookie", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--hidden" | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl-config", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-otp-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-sasl-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--name", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--sname", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--vm-args", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--pipe-to", _, _ | t], config), do: parse_argv(t, config)
## Fallback
defp parse_argv([h | t], %{mode: :elixirc} = config) do
@@ -489,7 +462,8 @@ defmodule Kernel.CLI do
defp process_command({:compile, patterns}, config) do
# If ensuring the dir returns an error no files will be found.
_ = :filelib.ensure_path(config.output)
# TODO: Use :filelib.ensure_path/1 once we require Erlang/OTP 25+
_ = :filelib.ensure_dir(:filename.join(config.output, "."))
case filter_multiple_patterns(patterns) do
{:ok, []} ->
@@ -509,11 +483,15 @@ defmodule Kernel.CLI do
]
end
output = IO.chardata_to_string(config.output)
profile_opts =
if config.profile do
[profile: config.profile]
else
[]
end
opts =
verbose_opts ++
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
output = IO.chardata_to_string(config.output)
opts = verbose_opts ++ profile_opts
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
{:ok, _, _} -> :ok
+26 -41
View File
@@ -8,7 +8,6 @@ defmodule Kernel.LexicalTracker do
@moduledoc false
@timeout :infinity
@behaviour :gen_server
@warn_key 0
@doc """
Returns all references in this lexical scope.
@@ -17,24 +16,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
"""
def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta})
module
end
@doc """
Invoked during module expansion to annotate an import
must be warned if unused.
"""
def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module})
module
end
# Internal API
# Starts the tracker and returns its PID.
@@ -58,6 +39,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
end
@doc false
def add_alias(pid, module, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_alias, module, meta, warn})
end
@doc false
def remote_dispatch(pid, module, mode) when is_atom(module) do
:gen_server.cast(pid, {:remote_dispatch, module, mode})
@@ -107,12 +93,16 @@ defmodule Kernel.LexicalTracker do
@doc false
def collect_unused_imports(pid) do
:gen_server.call(pid, :unused_imports, @timeout)
unused(pid, :unused_imports)
end
@doc false
def collect_unused_aliases(pid) do
:gen_server.call(pid, :unused_aliases, @timeout)
unused(pid, :unused_aliases)
end
defp unused(pid, tag) do
:gen_server.call(pid, tag, @timeout)
end
# Callbacks
@@ -133,17 +123,11 @@ defmodule Kernel.LexicalTracker do
@doc false
def handle_call(:unused_aliases, _from, state) do
aliases = for {alias, meta} when is_list(meta) <- state.aliases, do: {alias, meta}
{:reply, Enum.sort(aliases), state}
{:reply, Enum.sort(state.aliases), state}
end
def handle_call(:unused_imports, _from, state) do
imports =
for {module, %{@warn_key => _} = map} <- state.imports do
{module, Map.delete(map, @warn_key)}
end
{:reply, Enum.sort(imports), state}
{:reply, Enum.sort(state.imports), state}
end
def handle_call(:references, _from, state) do
@@ -187,8 +171,8 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | imports: imports, references: references}}
end
def handle_cast({:alias_dispatch, module}, state) do
{:noreply, put_in(state.aliases[module], :used)}
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
end
def handle_cast({:import_quoted, module, function, arities}, state) do
@@ -226,19 +210,20 @@ defmodule Kernel.LexicalTracker do
end
def handle_cast({:add_import, module, fas, meta, warn}, state) do
keys = if warn, do: [@warn_key, module | fas], else: [module | fas]
{:noreply, put_in(state.imports[module], Map.from_keys(keys, meta))}
end
def handle_cast({:warn_alias, alias, meta}, %{aliases: aliases} = state) do
case aliases do
%{^alias => :used} -> {:noreply, state}
%{} -> {:noreply, %{state | aliases: Map.put(aliases, alias, meta)}}
if warn do
imports = for module_or_fa <- [module | fas], do: {module_or_fa, meta}, into: %{}
{:noreply, put_in(state.imports[module], imports)}
else
{:noreply, state}
end
end
def handle_cast({:warn_import, module}, state) do
{:noreply, put_in(state.imports[module][@warn_key], true)}
def handle_cast({:add_alias, module, meta, warn}, state) do
if warn do
{:noreply, put_in(state.aliases[module], meta)}
else
{:noreply, state}
end
end
@doc false
+79 -84
View File
@@ -18,36 +18,23 @@ defmodule Kernel.ParallelCompiler do
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
task
end
defp inner_async(fun) do
case :erlang.get(:elixir_compiler_info) do
{compiler_pid, file_pid} ->
ref = make_ref()
file = :erlang.get(:elixir_compiler_file)
dest = :erlang.get(:elixir_compiler_dest)
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
{_parent, cache} = Module.ParallelChecker.get()
{_parent, checker} = Module.ParallelChecker.get()
task =
Task.async(fn ->
Module.ParallelChecker.put(compiler_pid, cache)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
receive do
^ref -> fun.()
end
end)
send(compiler_pid, {:async, task.pid})
{ref, task}
Task.async(fn ->
send(compiler_pid, {:async, self()})
Module.ParallelChecker.put(compiler_pid, checker)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
fun.()
end)
:undefined ->
raise ArgumentError,
@@ -65,29 +52,41 @@ defmodule Kernel.ParallelCompiler do
"""
@doc since: "1.16.0"
def pmap(collection, fun) when is_function(fun, 1) do
parent = self()
ref = make_ref()
# We spawn a series of tasks for parallel processing.
# The tasks are waiting until we give the go ahead.
refs_tasks =
# The tasks notify themselves to the compiler.
tasks =
Enum.map(collection, fn item ->
inner_async(fn -> fun.(item) end)
async(fn ->
send(parent, {ref, self()})
receive do
^ref -> fun.(item)
end
end)
end)
# Then the tasks notify us. This is important because if
# we wait before the tasks notify the compiler, we may be
# released as there is nothing else running.
on =
for %{pid: pid} <- tasks do
receive do
{^ref, ^pid} -> pid
end
end
# Notify the compiler we are waiting on the tasks.
{compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info)
defining = :elixir_module.compiler_modules()
on = Enum.map(refs_tasks, fn {_ref, %{pid: pid}} -> pid end)
send(compiler_pid, {:waiting, :pmap, self(), ref, file_pid, on, defining, :raise})
# Now we allow the tasks to run. This step is not strictly
# necessary but it makes compilation more deterministic by
# only allowing tasks to run once we are waiting.
tasks =
Enum.map(refs_tasks, fn {ref, task} ->
send(task.pid, ref)
task
end)
Enum.each(on, &send(&1, ref))
# Await tasks and notify the compiler they are done. We could
# have the tasks report directly to the compiler, which in turn
@@ -158,10 +157,6 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@@ -208,10 +203,6 @@ defmodule Kernel.ParallelCompiler do
* `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@@ -247,19 +238,36 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
schedulers =
Keyword.get_lazy(options, :max_concurrency, fn ->
max(:erlang.system_info(:schedulers_online), 2)
end)
{:ok, cache} = Module.ParallelChecker.start_link(schedulers)
schedulers = max(:erlang.system_info(:schedulers_online), 2)
{:ok, checker} = Module.ParallelChecker.start_link(schedulers)
{status, modules_or_errors, info} =
try do
spawn_workers(schedulers, cache, files, output, options)
outcome = spawn_workers(schedulers, checker, files, output, options)
{outcome, Code.get_compiler_option(:warnings_as_errors)}
else
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
when r_warnings != [] or c_warnings != [] ->
message =
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp)
{:ok, write_module_binaries(outcome, output, beam_timestamp), info}
{{:error, errors, info}, true} ->
%{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info
info = %{info | runtime_warnings: [], compile_warnings: []}
{:error, c_warnings ++ r_warnings ++ errors, info}
{{:error, errors, info}, _} ->
{:error, errors, info}
after
Module.ParallelChecker.stop(cache)
Module.ParallelChecker.stop(checker)
end
# TODO: Require this to be set from Elixir v1.19
@@ -281,7 +289,6 @@ defmodule Kernel.ParallelCompiler do
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
beam_timestamp: Keyword.get(options, :beam_timestamp),
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
@@ -339,40 +346,38 @@ defmodule Kernel.ParallelCompiler do
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
runtime_warnings = maybe_check_modules(result, dependent_modules, state)
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
{{:ok, result, info}, state}
end
defp maybe_check_modules(result, runtime_modules, state) do
%{profile: profile, checker: checker} = state
compiled_modules =
for {{:module, module}, binary} when is_binary(binary) <- result,
do: module
profile(
state,
fn ->
num_modules = length(compiled_modules) + length(runtime_modules)
"group pass check of #{num_modules} modules"
end,
fn -> Module.ParallelChecker.verify(state.checker, runtime_modules) end
)
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile(%{profile: :none}, _what, fun), do: fun.()
defp profile(%{profile: {:time, _, _}}, what, fun) do
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
what = if is_binary(what), do: what, else: what.()
IO.puts(:stderr, "[profile] Finished #{what} in #{time}ms")
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
result
end
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
fun.()
end
## Compiler worker spawning
# We already have n=schedulers currently running, don't spawn new ones
@@ -408,13 +413,13 @@ defmodule Kernel.ParallelCompiler do
end
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, errors, state) do
%{output: output, dest: dest, checker: cache} = state
%{output: output, dest: dest, checker: checker} = state
parent = self()
file = Path.expand(file)
{pid, ref} =
:erlang.spawn_monitor(fn ->
Module.ParallelChecker.put(parent, cache)
Module.ParallelChecker.put(parent, checker)
:erlang.put(:elixir_compiler_info, {parent, self()})
:erlang.put(:elixir_compiler_file, file)
@@ -456,19 +461,14 @@ defmodule Kernel.ParallelCompiler do
end)
[] = files
cycle_return =
profile(state, "cycle resolution", fn -> each_cycle_return(state.each_cycle.()) end)
cycle_return = each_cycle_return(state.each_cycle.())
state = cycle_timing(result, state)
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -503,15 +503,13 @@ defmodule Kernel.ParallelCompiler do
# Finally, note there is no difference between hard and raise, the
# difference is where the raise is happening, inside the compiler
# or in the caller.
deadlocked =
profile(state, "deadlock resolution", fn ->
waiting_list = Map.to_list(waiting)
waiting_list = Map.to_list(waiting)
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
end)
deadlocked =
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
if deadlocked do
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
@@ -742,9 +740,6 @@ defmodule Kernel.ParallelCompiler do
end
defp return_error(warnings, errors, state, fun) do
# Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
+33 -67
View File
@@ -112,24 +112,26 @@ defmodule Kernel.SpecialForms do
%User{age: age} = user
An update operation specific for structs is also available:
%User{user | age: 28}
The advantage of structs is that they validate that the given
keys are part of the defined struct. The example below will fail
because there is no key `:full_name` in the `User` struct:
%User{full_name: "john doe"}
An update operation specific for structs is also available:
The syntax above will guarantee the given keys are valid at
compilation time and it will guarantee at runtime the given
argument is a struct, failing with `BadStructError` otherwise.
%User{user | age: 28}
Once again, the syntax above will guarantee the given keys
are valid at compilation time and it will guarantee at runtime
the given argument is a struct, failing with `BadStructError`
otherwise. The map update syntax can also be used for updating
structs, and it is useful when you want to update any struct,
regardless of their name, as long as they have matching fields:
%{user | age: 28}
Although structs are maps, by default structs do not implement
any of the protocols implemented for maps. Check
`Kernel.defprotocol/2` for more information on how structs
can be used with protocols for polymorphic dispatch. Also
see `Kernel.struct/2` and `Kernel.struct!/2` for examples on
how to create and update structs dynamically.
## Pattern matching on struct names
@@ -548,7 +550,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the alias
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
"""
@@ -672,7 +674,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the import
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
## Ambiguous function/macro names
@@ -886,7 +888,7 @@ defmodule Kernel.SpecialForms do
At first, there is nothing in this example that actually reveals it is a
macro. But what is happening is that, at compilation time, `squared(5)`
becomes `5 * 5`. The argument `5` is duplicated in the produced code, we
can see this behavior in practice though because our macro actually has
can see this behaviour in practice though because our macro actually has
a bug:
import Math
@@ -913,7 +915,7 @@ defmodule Kernel.SpecialForms do
my_number.() * my_number.()
Which invokes the function twice, explaining why we get the printed value
twice! In the majority of the cases, this is actually unexpected behavior,
twice! In the majority of the cases, this is actually unexpected behaviour,
and that's why one of the first things you need to keep in mind when it
comes to macros is to **not unquote the same value more than once**.
@@ -1046,7 +1048,7 @@ defmodule Kernel.SpecialForms do
end
end
require ContextHygiene
require Hygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
@@ -1442,7 +1444,7 @@ defmodule Kernel.SpecialForms do
[elixir: :prolog]
Given the grandparents of Erlang and Prolog were nil, those values were
filtered out. If you don't want this behavior, a simple option is to
filtered out. If you don't want this behaviour, a simple option is to
move the filter inside the do-block:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
@@ -1492,9 +1494,7 @@ defmodule Kernel.SpecialForms do
## The `:reduce` option
*Available since Elixir v1.8*.
While the `:into` option allows us to customize the comprehension behavior
While the `:into` option allows us to customize the comprehension behaviour
to a given data type, such as putting all of the values inside a map or inside
a binary, it is not always enough.
@@ -1539,7 +1539,7 @@ defmodule Kernel.SpecialForms do
@doc """
Combine matching clauses.
One of the ways to understand `with` is to show which code
One of the ways to understand with is to show which code
patterns it improves. Imagine you have a map where the fields
`width` and `height` are optional and you want to compute its
area, as `{:ok, area}` or return `:error`. We could implement
@@ -1622,7 +1622,7 @@ defmodule Kernel.SpecialForms do
iex> width
nil
The behavior of any expression in a clause is the same as if it was
The behaviour of any expression in a clause is the same as if it was
written outside of `with`. For example, `=` will raise a `MatchError`
instead of returning the non-matched value:
@@ -1661,7 +1661,7 @@ defmodule Kernel.SpecialForms do
...> end
{:error, :wrong_data}
The `else` block works like a `case`: it can have multiple clauses,
The `else` block works like a `case` clause: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` (such as
`width` in this example) are not available in the `else` block.
@@ -1754,15 +1754,6 @@ defmodule Kernel.SpecialForms do
"""
defmacro unquote(:__block__)(args), do: error!([args])
@doc """
Internal special form for cursor position.
This is the special form used whenever we need to represent
the cursor position in Elixir's AST. See `Code.Fragment` for
more information.
"""
defmacro unquote(:__cursor__)(args), do: error!([args])
@doc """
Capture operator. Captures or creates an anonymous function.
@@ -1786,15 +1777,6 @@ defmodule Kernel.SpecialForms do
&local_function/1
Note that `&local_function/1` creates a local capture, but
`&__MODULE__.local_function/1` or `&imported_function/1` create a remote
capture. For more information, refer to the ["Functions" section in the Erlang Reference Manual](https://www.erlang.org/doc/system/eff_guide_functions.html#function-calls).
Whether a capture is local or remote has implications when using hot code
reloading: local captures dispatch to the version of the module that existed
at the time they were created, while remote captures dispatch to the current
version of the module.
See also `Function.capture/3`.
## Anonymous functions
@@ -1894,11 +1876,6 @@ defmodule Kernel.SpecialForms do
@doc ~S"""
Matches the given expression against the given clauses.
`case/2` relies on pattern matching and guards to choose
which clause to execute. If your logic cannot be expressed
within patterns and guards, consider using `if/2` or `cond/1`
instead.
## Examples
case File.read(file) do
@@ -1929,9 +1906,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This clause would match any value (x = 10)"
If you find yourself nesting `case` expressions inside
`case` expressions, consider using `with/1`.
## Variable handling
Note that variables bound in a clause do not leak to the outer context:
@@ -1991,20 +1965,17 @@ defmodule Kernel.SpecialForms do
Evaluates the expression corresponding to the first clause that
evaluates to a truthy value.
## Examples
The following example has a single clause that always evaluates
to true:
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
If all clauses evaluate to `nil` or `false`, `cond` raises an error.
Raises an error if all conditions evaluate to `nil` or `false`.
For this reason, it may be necessary to add a final always-truthy condition
(anything non-`false` and non-`nil`), which will always match:
(anything non-`false` and non-`nil`), which will always match.
## Examples
cond do
1 + 1 == 1 ->
@@ -2016,9 +1987,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This will"
If your `cond` has two clauses, and the last one falls back to
`true`, you may consider using `if/2` instead.
"""
defmacro cond(clauses), do: error!([clauses])
@@ -2332,13 +2300,11 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@doc """
Checks if there is a message matching any of the given clauses in the current
process mailbox.
Checks if there is a message matching the given clauses
in the current process mailbox.
If there is no matching message, the current process waits until a matching
message arrives or until after a given timeout value.
Any new and existing messages that do not match will remain in the mailbox.
In case there is no such message, the current process hangs
until a message arrives or waits until a given timeout value.
## Examples
@@ -2351,8 +2317,8 @@ defmodule Kernel.SpecialForms do
IO.puts(:stderr, "Unexpected message received")
end
An optional `after` clause can be given in case no matching message is
received during the given timeout period, specified in milliseconds:
An optional `after` clause can be given in case the message was not
received after the given timeout period, specified in milliseconds:
receive do
{:selector, number, name} when is_integer(number) ->
+45 -54
View File
@@ -118,7 +118,7 @@ defmodule Kernel.Typespec do
case spec_to_signature(expr) do
{name, arity} ->
# store doc only once in case callback has multiple clauses
if not :ets.member(set, {kind, name, arity}) do
unless :ets.member(set, {kind, name, arity}) do
{line, doc} = get_doc_info(set, :doc, line)
store_doc(set, kind, name, arity, line, :doc, doc, %{})
end
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_module.get_cached_env(pos),
:elixir_locals.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
defp collect_defined_type_pairs(type_typespecs) do
fun = fn {_kind, expr, pos}, type_pairs ->
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
case type_to_signature(expr) do
{name, arity} = type_pair ->
@@ -290,9 +290,8 @@ defmodule Kernel.Typespec do
:lists.filter(fun, types)
end
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
when is_list(meta) do
caller = :elixir_module.get_cached_env(pos)
defp translate_type({kind, {:"::", _, [{name, _, args}, definition]}, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
state = clean_local_state(state)
args =
@@ -321,7 +320,7 @@ defmodule Kernel.Typespec do
invalid_args = :lists.filter(&(not valid_variable_ast?(&1)), args)
if invalid_args != [] do
unless invalid_args == [] do
invalid_args = :lists.join(", ", :lists.map(&Macro.to_string/1, invalid_args))
message =
@@ -336,7 +335,7 @@ defmodule Kernel.Typespec do
IO.warn(message, caller)
end
{{kind, {name, arity}, meta, type, export}, state}
{{kind, {name, arity}, caller.line, type, export}, state}
end
defp valid_variable_ast?({variable_name, _, context})
@@ -349,17 +348,17 @@ defmodule Kernel.Typespec do
defp underspecified?(_kind, _arity, _spec), do: false
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, guard, caller, state)
end
defp translate_spec({kind, spec, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, [], caller, state)
end
defp translate_spec(kind, {:"::", _, [{name, meta, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" and is_list(meta) and (is_list(args) or is_atom(args)) do
defp translate_spec(kind, {:"::", meta, [{name, _, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" do
translate_spec(kind, meta, name, args, return, guard, caller, state)
end
@@ -381,7 +380,7 @@ defmodule Kernel.Typespec do
ensure_no_defaults!(args)
state = clean_local_state(state)
if not Keyword.keyword?(guard) do
unless Keyword.keyword?(guard) do
error = "expected keywords as guard in type specification, got: #{Macro.to_string(guard)}"
compile_error(caller, error)
end
@@ -402,7 +401,7 @@ defmodule Kernel.Typespec do
ensure_no_unused_local_vars!(caller, state.local_vars)
arity = length(args)
{{kind, {name, arity}, meta, spec}, state}
{{kind, {name, arity}, caller.line, spec}, state}
end
# TODO: Remove char_list type by v2.0
@@ -493,24 +492,6 @@ defmodule Kernel.Typespec do
state}
end
defp typespec(
{:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]},
_,
_,
state
)
when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do
location = location(meta)
prod_location = location(prod_meta)
{{:type, location, :binary,
[
{:op, prod_location, :*, {:integer, prod_location, size},
{:integer, prod_location, unit}},
{:integer, location, 0}
]}, state}
end
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
when is_atom(ctx) and is_integer(size) and size >= 0 do
location = location(meta)
@@ -586,23 +567,27 @@ defmodule Kernel.Typespec do
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
case Macro.expand(name, %{caller | function: {:__info__, 1}}) do
module when is_atom(module) ->
struct_info = Macro.struct_info!(module, caller)
struct =
module
|> Macro.struct!(caller)
|> Map.delete(:__struct__)
|> Map.to_list()
if not Keyword.keyword?(fields) do
unless Keyword.keyword?(fields) do
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
end
types =
:lists.map(
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
fn
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
end,
struct_info
:lists.sort(struct)
)
fun = fn {field, _} ->
if not Enum.any?(struct_info, &(&1.field == field)) do
unless Keyword.has_key?(struct, field) do
compile_error(
caller,
"undefined field #{inspect(field)} on struct #{inspect(module)}"
@@ -611,7 +596,7 @@ defmodule Kernel.Typespec do
end
:lists.foreach(fun, fields)
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
_ ->
compile_error(
@@ -645,7 +630,7 @@ defmodule Kernel.Typespec do
)
fun = fn {field, _} ->
if not Keyword.has_key?(fields, field) do
unless Keyword.has_key?(fields, field) do
compile_error(caller, "undefined field #{field} on record #{inspect(tag)}")
end
end
@@ -686,7 +671,14 @@ defmodule Kernel.Typespec do
when is_list(args) do
{args, state} = fn_args(meta, args, vars, caller, state)
{spec, state} = typespec(return, vars, caller, state)
{{:type, location(meta), :fun, [args, spec]}, state}
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, location(meta), :fun, fun_args}, state}
end
# Handle type operator
@@ -762,7 +754,7 @@ defmodule Kernel.Typespec do
) do
remote = Module.get_attribute(caller.module, attr)
if not (is_atom(remote) and remote != nil) do
unless is_atom(remote) and remote != nil do
message =
"invalid remote in typespec: #{Macro.to_string(orig)} (@#{attr} is #{inspect(remote)})"
@@ -925,11 +917,11 @@ defmodule Kernel.Typespec do
typespec({nil, [], []}, vars, caller, state)
end
defp typespec([{:..., _, _}], vars, caller, state) do
defp typespec([{:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], []}, vars, caller, state)
end
defp typespec([spec, {:..., _, _}], vars, caller, state) do
defp typespec([spec, {:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], [spec]}, vars, caller, state)
end
@@ -956,20 +948,19 @@ defmodule Kernel.Typespec do
## Helpers
# This is a modified backport of Macro.expand/2 because we want to expand
# This is a backport of Macro.expand/2 because we want to expand
# aliases but we don't them to become compile-time references.
defp expand_remote({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp expand_remote({:__aliases__, _, _} = alias, env) do
case :elixir_aliases.expand(alias, env) do
receiver when is_atom(receiver) ->
receiver
[head | tail] ->
case Macro.expand(head, env) do
head when is_atom(head) ->
:elixir_aliases.concat([head | tail])
aliases ->
aliases = :lists.map(&Macro.expand_once(&1, env), aliases)
_ ->
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
case :lists.all(&is_atom/1, aliases) do
true -> :elixir_aliases.concat(aliases)
false -> alias
end
end
end
+20 -23
View File
@@ -103,7 +103,7 @@ defmodule Kernel.Utils do
def defstruct(module, fields, bootstrapped?, env) do
{set, bag} = :elixir_module.data_tables(module)
if :ets.member(set, {:elixir, :struct}) do
if :ets.member(set, :__struct__) do
raise ArgumentError,
"defstruct has already been called for " <>
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
@@ -119,10 +119,8 @@ defmodule Kernel.Utils do
mapper = fn
{key, val} when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, false, :none)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
@@ -131,7 +129,6 @@ defmodule Kernel.Utils do
end
key when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
{key, nil}
other ->
@@ -166,24 +163,23 @@ defmodule Kernel.Utils do
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
body =
case bootstrapped? do
true ->
case enforce_keys do
[] ->
quote line: 0, generated: true do
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
quote do
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote line: 0, generated: true do
quote do
{map, keys} =
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
{%{map | key => val}, List.delete(keys, key)}
end)
@@ -201,10 +197,10 @@ defmodule Kernel.Utils do
end
false ->
quote line: 0, generated: true do
quote do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
unquote(escaped_struct),
@__struct__,
kv
)
end
@@ -212,13 +208,17 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ field is used for expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# Store all field metadata to go into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
{struct, :lists.reverse(derive), quote(do: kv), body}
error_keys ->
raise ArgumentError,
@@ -289,7 +289,7 @@ defmodule Kernel.Utils do
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`, but only once, and then reuse the unquoted form throughout the expression.
`value`, but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable references it should be aware of, and rewrites it into a new expression
@@ -330,11 +330,8 @@ defmodule Kernel.Utils do
quote do
case Macro.Env.in_guard?(__CALLER__) do
true ->
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
@@ -399,8 +396,8 @@ defmodule Kernel.Utils do
end
end
defp literal_quote(ast, args) do
{:quote, [], [args, [do: ast]]}
defp literal_quote(ast) do
{:quote, [], [[do: ast]]}
end
defp literal_unquote(ast) do
+4 -37
View File
@@ -6,7 +6,7 @@ defmodule Keyword do
The second element, known as the *value*, can be any term.
Keywords are mostly used to work with optional values. For a general introduction
to keywords and how they compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
to keywords and how the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
guide.
## Examples
@@ -102,9 +102,6 @@ defmodule Keyword do
@type key :: atom
@type value :: any
@typedoc since: "1.17.0"
@type default :: any
@type t :: [{key, value}]
@type t(value) :: [{key, value}]
@@ -391,7 +388,7 @@ defmodule Keyword do
3
"""
@spec get(t, key, default) :: value | default
@spec get(t, key, value) :: value
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> value
@@ -960,36 +957,6 @@ defmodule Keyword do
:lists.sort(left) === :lists.sort(right)
end
@doc """
Intersects two keyword lists, returning a keyword with the common keys.
By default, it returns the values of the intersected keys in `keyword2`.
The keys are returned in the order found in `keyword1`.
## Examples
iex> Keyword.intersect([a: 1, b: 2], [b: "b", c: "c"])
[b: "b"]
iex> Keyword.intersect([a: 1, b: 2], [b: 2, c: 3], fn _k, v1, v2 ->
...> v1 + v2
...> end)
[b: 4]
"""
@doc since: "1.17.0"
@spec intersect(keyword, keyword, (key, value, value -> value)) :: keyword
def intersect(keyword1, keyword2, fun \\ fn _key, _v1, v2 -> v2 end)
def intersect([{k, v1} | keyword1], keyword2, fun) do
case :lists.keyfind(k, 1, keyword2) do
{_, v2} -> [{k, fun.(k, v1, v2)} | intersect(keyword1, keyword2, fun)]
false -> intersect(keyword1, keyword2, fun)
end
end
def intersect([], _keyword2, _fun), do: []
@doc """
Merges two keyword lists into one.
@@ -1322,7 +1289,7 @@ defmodule Keyword do
{1, []}
"""
@spec pop(t, key, default) :: {value | default, t}
@spec pop(t, key, value) :: {value, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
@@ -1438,7 +1405,7 @@ defmodule Keyword do
{1, [a: 2]}
"""
@spec pop_first(t, key, default) :: {value | default, t}
@spec pop_first(t, key, value) :: {value, t}
def pop_first(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keytake(key, 1, keywords) do
{:value, {^key, value}, rest} -> {value, rest}
+50 -61
View File
@@ -107,7 +107,7 @@ defmodule List do
charlists in IEx when you encounter them, which shows you the type, description
and also the raw representation in one single summary.
The rationale behind this behavior is to better support
The rationale behind this behaviour is to better support
Erlang libraries which may return text as charlists
instead of Elixir strings. In Erlang, charlists are the default
way of handling strings, while in Elixir it's binaries. One
@@ -633,39 +633,27 @@ defmodule List do
[other]
end
@deprecated "Use Enum.zip/1 instead"
# We keep the old implementation because it also supported lists
# of tuples, even though this was not included in its @spec.
@doc """
Zips corresponding elements from each list in `list_of_lists`.
The zipping finishes as soon as any list terminates.
## Examples
iex> List.zip([[1, 2], [3, 4], [5, 6]])
[{1, 3, 5}, {2, 4, 6}]
iex> List.zip([[1, 2], [3], [5, 6]])
[{1, 3, 5}]
"""
@spec zip([list]) :: [tuple]
def zip([]), do: []
def zip(list_of_lists) when is_list(list_of_lists), do: do_zip(list_of_lists, [])
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
def zip(list_of_lists) when is_list(list_of_lists) do
do_zip(list_of_lists, [])
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
@doc ~S"""
Checks if `list` is a charlist made only of printable ASCII characters.
@@ -907,7 +895,7 @@ defmodule List do
end
@doc """
Returns `true` if `list` starts with the given `prefix` list, otherwise returns `false`.
Returns `true` if `list` starts with the given `prefix` list; otherwise returns `false`.
If `prefix` is an empty list, it returns `true`.
@@ -936,34 +924,6 @@ defmodule List do
def starts_with?(list, []) when is_list(list), do: true
def starts_with?(list, [_ | _]) when is_list(list), do: false
@doc """
Returns `true` if `list` ends with the given `suffix` list, otherwise returns `false`.
If `suffix` is an empty list, it returns `true`.
### Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
iex> List.ends_with?([1, 2], [1, 2, 3])
false
iex> List.ends_with?([:alpha], [])
true
iex> List.ends_with?([], [:alpha])
false
"""
@doc since: "1.18.0"
@spec ends_with?(nonempty_list, nonempty_list) :: boolean
@spec ends_with?(list, []) :: true
@spec ends_with?([], nonempty_list) :: false
def ends_with?(list, suffix) do
:lists.suffix(suffix, list)
end
@doc """
Converts a charlist to an atom.
@@ -1133,7 +1093,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list, charlists: :as_lists)}
#{inspect(list)}
"""
else
result when is_binary(result) ->
@@ -1185,7 +1145,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list, charlists: :as_lists)}
#{inspect(list)}
"""
else
result when is_list(result) ->
@@ -1412,4 +1372,33 @@ defmodule List do
defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc])
end
# zip
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
end
+1 -1
View File
@@ -58,6 +58,6 @@ end
defimpl List.Chars, for: Float do
def to_charlist(term) do
Float.to_charlist(term)
:io_lib_format.fwrite_g(term)
end
end
+126 -306
View File
@@ -146,6 +146,8 @@ defmodule Macro do
* `:delimiter` - contains the opening delimiter for sigils, strings,
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
* `:format` - set to `:keyword` when an atom is defined as a keyword
* `:do` - contains metadata about the `do` location in a function call with
`do`-`end` blocks (when `:token_metadata` is true)
@@ -154,21 +156,12 @@ defmodule Macro do
* `:end_of_expression` - denotes when the end of expression effectively
happens (when `:token_metadata` is true). This is only available for
expressions inside "blocks of code", which are either direct children
of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line
character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surround by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
direct children of a `__block__`, and it is either the location of a
newline or of the `;` character. The last expression of `__block__`
does not have this metadata.
* `:indentation` - indentation of a sigil heredoc
* `:parens` - denotes a node was surrounded by parens for grouping.
This entry may appear multiple times in the same metadata if
multiple pairs are used for grouping
The following metadata keys are private:
* `:alias` - Used for alias hygiene.
@@ -377,7 +370,7 @@ defmodule Macro do
Note the arguments are not unique. If you later on want
to access the same variables, you can invoke this function
with the same inputs. Use `generate_unique_arguments/2` to
generate unique arguments that can't be overridden.
generate a unique arguments that can't be overridden.
## Examples
@@ -501,8 +494,10 @@ defmodule Macro do
"""
@doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) ::
[{atom, [counter: integer], context}, ...]
@spec generate_unique_arguments(pos_integer, context) :: [
{atom, [counter: integer], context},
...
]
when context: atom
def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2)
@@ -782,23 +777,23 @@ defmodule Macro do
## Options
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
* `:unquote` - when true, this function leaves `unquote/1` and
`unquote_splicing/1` statements unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Defaults to false.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
* `:prune_metadata` - when true, removes metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
it should only be used when escaping ASTs. Defaults to false.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
different MD5 of the module bytecode, because the AST contains metadata,
As an example, `ExUnit` stores the AST of every assertion, so when
an assertion fails we can show code snippets to users. Without this
option, each time the test module is compiled, we get a different
MD5 of the module bytecode, because the AST contains metadata,
such as counters, specific to the compilation environment. By pruning
the metadata, we ensure that the module is deterministic and reduce
the amount of data `ExUnit` needs to keep around. Only the minimal
amount of metadata is kept, such as `:line`, `:no_parens` and `:delimiter`.
amount of metadata is kept, such as `:line` and `:no_parens`.
## Comparison to `quote/2`
@@ -820,8 +815,7 @@ defmodule Macro do
iex> value = {:a, :b, :c}
iex> quote do: unquote(value)
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
Did you forget to escape term using Macro.escape/1?
{:a, :b, :c}
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
@@ -834,36 +828,30 @@ defmodule Macro do
:elixir_quote.escape(expr, kind, unquote)
end
# TODO: Deprecate me on Elixir v1.22
@doc deprecated: "Use Macro.struct_info!/2 instead"
def struct!(module, env) when is_atom(module) do
pairs =
for %{field: field, default: default} <- struct_info!(module, env), do: {field, default}
:maps.from_list([__struct__: module] ++ pairs)
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Expands the struct given by `module` in the given `env`.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
It will raise `CompileError` if the struct is not available.
From Elixir v1.12, calling this function also adds an export
dependency on the given struct.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
@doc since: "1.8.0"
@spec struct!(module, Macro.Env.t()) ::
%{required(:__struct__) => module, optional(atom) => any}
when module: module()
def struct!(module, env) when is_atom(module) do
if module == env.module do
Module.get_attribute(module, :__struct__)
end ||
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
{:ok, struct} -> struct
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@doc """
@@ -908,8 +896,8 @@ defmodule Macro do
defp find_invalid(bin) when is_binary(bin), do: nil
defp find_invalid(fun) when is_function(fun) do
if Function.info(fun, :env) != {:env, []} or
Function.info(fun, :type) != {:type, :external} do
unless Function.info(fun, :env) == {:env, []} and
Function.info(fun, :type) == {:type, :external} do
{:error, fun}
end
end
@@ -1138,10 +1126,6 @@ defmodule Macro do
`Code.quoted_to_algebra/2` as a lower level function
with more control around formatting.
If the AST contains invalid nodes, this function will
attempt to inspect them, to aid debugging, although
the elements won't be formatted accordingly.
## Examples
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
@@ -1149,10 +1133,9 @@ defmodule Macro do
"""
@spec to_string(t()) :: String.t()
# TODO: Allow line_length to be configurable on v1.17
def to_string(tree) do
doc =
Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98)
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
IO.iodata_to_binary(doc)
end
@@ -1501,7 +1484,7 @@ defmodule Macro do
:error
end
defp op_call({:..//, _, [left, middle, right]} = ast, fun) do
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
left = op_to_string(left, fun, :.., :left)
middle = op_to_string(middle, fun, :.., :right)
right = op_to_string(right, fun, :"//", :right)
@@ -1730,7 +1713,7 @@ defmodule Macro do
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
This is used when you want to programatically invoke a macro at
compile-time.
"""
@doc since: "1.16.0"
@@ -1751,9 +1734,7 @@ defmodule Macro do
If the expression cannot be expanded, it returns the expression
itself. This function does not traverse the AST, only the root
node is expanded. The expansion happens as if it was expanded by
the Elixir compiler and therefore compilation tracers will be invoked
and deprecation warnings will be emitted during the expansion.
node is expanded.
`expand_once/2` performs the expansion just once. Check `expand/2`
to perform expansion until the node can no longer be expanded.
@@ -1824,23 +1805,23 @@ defmodule Macro do
elem(do_expand_once(ast, env), 0)
end
defp do_expand_once({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp do_expand_once({:__aliases__, meta, _} = original, env) do
case :elixir_aliases.expand_or_concat(original, env) do
receiver when is_atom(receiver) ->
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
[head | tail] ->
{head, _} = do_expand_once(head, env)
aliases ->
aliases = :lists.map(&elem(do_expand_once(&1, env), 0), aliases)
case is_atom(head) do
case :lists.all(&is_atom/1, aliases) do
true ->
receiver = :elixir_aliases.concat([head | tail])
receiver = :elixir_aliases.concat(aliases)
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
false ->
{alias, false}
{original, false}
end
end
end
@@ -1851,13 +1832,13 @@ defmodule Macro do
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
do: {:filename.dirname(env.file), true}
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
env = update_in(env.versioned_vars, &maybe_escape_map/1)
{maybe_escape_map(env), true}
end
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
when is_atom(atom) and is_atom(field) and env.context != :match do
when is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{maybe_escape_map(Map.get(env, field)), true}
else
@@ -1865,39 +1846,56 @@ defmodule Macro do
end
end
defp do_expand_once({name, meta, context} = original, _env)
when is_atom(name) and is_list(meta) and is_atom(context) do
defp do_expand_once({atom, meta, context} = original, _env)
when is_atom(atom) and is_list(meta) and is_atom(context) do
{original, false}
end
defp do_expand_once({name, meta, args} = original, env)
when is_atom(name) and is_list(args) and is_list(meta) do
defp do_expand_once({atom, meta, args} = original, env)
when is_atom(atom) and is_list(args) and is_list(meta) do
arity = length(args)
case Macro.Env.expand_import(env, meta, name, arity) do
{:macro, _receiver, expander} ->
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
if special_form?(atom, arity) do
{original, false}
else
module = env.module
{:function, Kernel, op} when op in [:+, :-] and arity == 1 ->
case expand_once(hd(args), env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
extra =
if function_exported?(module, :__info__, 1) do
[{module, module.__info__(:macros)}]
else
[]
end
{:function, _receiver, _name} ->
{original, false}
s = :elixir_env.env_to_ex(env)
{:error, :not_found} ->
{original, false}
expand =
:elixir_dispatch.expand_import(meta, {atom, length(args)}, args, s, env, extra, true)
{:error, other} ->
:elixir_errors.file_error(meta, env, :elixir_dispatch, {:import, other, name, arity})
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(module)
# We don't want the line to propagate yet, but generated might!
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
{:ok, Kernel, op, [arg]} when op in [:+, :-] ->
case expand_once(arg, env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
end
{:ok, _receiver, _name, _args} ->
{original, false}
:error ->
{original, false}
end
end
end
# Expand possible macro require invocation
defp do_expand_once({{:., _, [left, name]}, meta, args} = original, env) when is_atom(name) do
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
{receiver, _} = do_expand_once(left, env)
case is_atom(receiver) do
@@ -1905,10 +1903,16 @@ defmodule Macro do
{original, false}
true ->
case Macro.Env.expand_require(env, meta, receiver, name, length(args)) do
{:macro, _receiver, expander} ->
s = :elixir_env.env_to_ex(env)
name_arity = {right, length(args)}
expand = :elixir_dispatch.expand_require(meta, receiver, name_arity, args, s, env)
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(env.module)
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
:error ->
{original, false}
@@ -1952,7 +1956,7 @@ defmodule Macro do
@spec operator?(name :: atom(), arity()) :: boolean()
def operator?(name, arity)
def operator?(:..//, 3),
def operator?(:"..//", 3),
do: true
# Code.Identifier treats :// as a binary operator for precedence
@@ -1963,8 +1967,8 @@ defmodule Macro do
def operator?(name, 1) when is_atom(name),
do: Identifier.unary_op(name) != :error
def operator?(:.., 0), do: true
def operator?(:..., 0), do: true
def operator?(:.., 0),
do: true
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
@@ -2155,8 +2159,9 @@ defmodule Macro do
@doc """
Converts the given argument to a string with the underscore-slash format.
The argument must either be an atom, representing an Elixir module,
or a string representing a module without the `Elixir.` prefix.
The argument must either be an atom or a string.
If an atom is given, it is assumed to be an Elixir module,
so it is converted to a string and then processed.
This function was designed to format language identifiers/tokens with the underscore-slash format,
that's why it belongs to the `Macro` module. Do not use it as a general
@@ -2190,7 +2195,7 @@ defmodule Macro do
"Foo.Bar"
"""
@spec underscore(atom() | String.t()) :: String.t()
@spec underscore(module() | atom() | String.t()) :: String.t()
def underscore(atom_or_string)
def underscore(atom) when is_atom(atom) do
@@ -2334,15 +2339,6 @@ defmodule Macro do
as a key (`:key`), or as the function name of a remote call
(`:remote_call`).
## Options
* `:escape` - a two-arity function used to escape a quoted
atom content, if necessary. The function receives the atom
content as string and a quote delimiter character, which
should always be escaped. By default the content is escaped
such that the inspected sequence would be parsed as the
given atom.
## Examples
### As a literal
@@ -2391,14 +2387,14 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
def inspect_atom(source_format, atom, opts \\ [])
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
def inspect_atom(source_format, atom)
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
def inspect_atom(:literal, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2410,7 +2406,7 @@ defmodule Macro do
end
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?:, ?", escaped, ?"])
_ ->
@@ -2418,7 +2414,7 @@ defmodule Macro do
end
end
def inspect_atom(:key, atom, opts) when is_atom(atom) do
def inspect_atom(:key, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2426,7 +2422,7 @@ defmodule Macro do
IO.iodata_to_binary([?", binary, ?", ?:])
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?", escaped, ?", ?:])
_ ->
@@ -2434,7 +2430,7 @@ defmodule Macro do
end
end
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
def inspect_atom(:remote_call, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case inner_classify(atom) do
@@ -2446,22 +2442,13 @@ defmodule Macro do
if type in [:not_callable, :alias] do
binary
else
inspect_atom_escape(opts, binary, ?")
elem(Code.Identifier.escape(binary, ?"), 0)
end
IO.iodata_to_binary([?", escaped, ?"])
end
end
defp inspect_atom_escape(opts, string, char) do
if escape = opts[:escape] do
escape.(string, char)
else
{escaped, _} = Code.Identifier.escape(string, char)
escaped
end
end
# Classifies the given atom into one of the following categories:
#
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
@@ -2488,7 +2475,7 @@ defmodule Macro do
#
defp inner_classify(atom) when is_atom(atom) do
cond do
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
:not_callable
# <|>, ^^^, and ~~~ are deprecated
@@ -2586,13 +2573,13 @@ defmodule Macro do
header = dbg_format_header(env)
quote do
to_debug = unquote(dbg_ast_to_debuggable(code, env))
to_debug = unquote(dbg_ast_to_debuggable(code))
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
end
end
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
@@ -2625,7 +2612,7 @@ defmodule Macro do
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
# Logic operators.
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast, _env)
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
when op in unquote(dbg_decomposed_binary_operators) do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
@@ -2633,22 +2620,11 @@ defmodule Macro do
quote do
unquote(acc_var) = []
unquote(dbg_boolean_tree(ast, acc_var, result_var))
{:logic_op, Enum.reverse(unquote(acc_var)), unquote(result_var)}
{:logic_op, Enum.reverse(unquote(acc_var))}
end
end
defp dbg_ast_to_debuggable({:__block__, _meta, exprs} = ast, _env) when exprs != [] do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
quote do
unquote(acc_var) = []
unquote(dbg_block(ast, acc_var, result_var))
{:block, Enum.reverse(unquote(acc_var)), unquote(result_var)}
end
end
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
clauses_returning_index =
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
{:->, meta, [left, {right, index}]}
@@ -2666,7 +2642,7 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
modified_clauses =
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
hd(
@@ -2687,104 +2663,8 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:if, meta, [condition_ast, clauses]} = ast, env) do
case Macro.Env.lookup_import(env, {:if, 2}) do
[macro: Kernel] ->
condition_result_var = unique_var(:condition_result, __MODULE__)
quote do
unquote(condition_result_var) = unquote(condition_ast)
result = unquote({:if, meta, [condition_result_var, clauses]})
{:if, unquote(escape(ast)), unquote(escape(condition_ast)),
unquote(condition_result_var), result}
end
_ ->
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
end
defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do
{opts, clauses} = List.pop_at(args, -1)
acc_var = unique_var(:acc, __MODULE__)
modified_clauses =
Enum.flat_map(clauses, fn
# We only detail assignments and pattern-matching clauses that
# can be helpful to understand how the result is constructed.
{:<-, _meta, [left, right]} ->
modified_left =
case left do
{:when, meta, [pattern, guard]} -> {:when, meta, [{pattern, quote(do: _)}, guard]}
pattern -> {pattern, quote(do: _)}
end
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(modified_left) <- {value, unquote(acc_var)}
]
end
{:=, _meta, [left, right]} ->
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(left) = value
]
end
# Other expressions like side effects are omitted.
expr ->
[expr]
end)
modified_opts =
Enum.map(opts, fn
{:do, do_block} ->
{:do, {do_block, acc_var}}
{:else, else_block} ->
clauses =
Enum.map(else_block, fn
{:->, meta, [[{:when, meta2, [pattern, guard]}], right]} ->
{:->, meta, [[{:when, meta2, [{pattern, acc_var}, guard]}], {right, acc_var}]}
{:->, meta, [[left], right]} ->
{:->, meta, [[{left, acc_var}], {right, acc_var}]}
invalid ->
invalid
end)
error_clause =
quote generated: true do
{other, _acc} -> raise WithClauseError, term: other
end
{:else, clauses ++ error_clause}
invalid ->
invalid
end)
modified_with_ast = {:with, meta, modified_clauses ++ [modified_opts]}
quote do
unquote(acc_var) = []
{value, acc} = unquote(modified_with_ast)
{:with, unquote(escape(ast)), Enum.reverse(acc), value}
end
end
# Any other AST.
defp dbg_ast_to_debuggable(ast, _env) do
defp dbg_ast_to_debuggable(ast) do
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
@@ -2815,18 +2695,6 @@ defmodule Macro do
end
end
defp dbg_block({:__block__, meta, exprs}, acc_var, result_var) do
modified_exprs =
Enum.map(exprs, fn expr ->
quote do
unquote(result_var) = unquote(expr)
unquote(acc_var) = [{unquote(escape(expr)), unquote(result_var)} | unquote(acc_var)]
end
end)
{:__block__, meta, modified_exprs}
end
# Made public to be called from Macro.dbg/3, so that we generate as little code
# as possible and call out into a function as soon as we can.
@doc false
@@ -2864,27 +2732,15 @@ defmodule Macro do
{[first_formatted | rest_formatted], result}
end
defp dbg_format_ast_to_debug({:logic_op, components, value}, options) do
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
{_ast, final_value} = List.last(components)
formatted =
Enum.map(components, fn {ast, value} ->
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
end)
{formatted, value}
end
defp dbg_format_ast_to_debug({:block, components, value}, options) do
formatted =
[
dbg_maybe_underline("Code block", options),
":\n(\n",
Enum.map(components, fn {ast, value} ->
[" ", dbg_format_ast_with_value(ast, value, options)]
end),
")\n"
]
{formatted, value}
{formatted, final_value}
end
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
@@ -2920,42 +2776,6 @@ defmodule Macro do
{formatted, value}
end
defp dbg_format_ast_to_debug(
{:if, ast, condition_ast, condition_result, result},
options
) do
formatted = [
dbg_maybe_underline("If condition", options),
":\n",
dbg_format_ast_with_value(condition_ast, condition_result, options),
?\n,
dbg_maybe_underline("If expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:with, ast, clauses, result}, options) do
formatted_clauses =
Enum.map(clauses, fn {clause_ast, clause_result} ->
dbg_format_ast_with_value(clause_ast, clause_result, options)
end)
formatted = [
dbg_maybe_underline("With clauses", options),
":\n",
formatted_clauses,
?\n,
dbg_maybe_underline("With expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do
{dbg_format_ast_with_value(code_ast, value, options), value}
end
+43 -357
View File
@@ -6,15 +6,9 @@ defmodule Macro.Env do
`__ENV__/0`. Inside macros, the caller environment can be
accessed as `__CALLER__/0`.
The majority of the functions in this module are provided
for low-level tools, which need to integrate with the Elixir
compiler, such as language servers and embedded languages.
For regular usage in Elixir code and macros, you must use
the `Macro` module instead. In particular, avoid modifying
the `Macro.Env` struct directly and prefer to use high-level
constructs, such as a `import`, `aliases`, and so forth to
build your own environment. For example, to build a custom
environment, you can define a function such as:
An instance of `Macro.Env` must not be modified by hand. If you need to
create a custom environment to pass to `Code.eval_quoted/3`, use the
following trick:
def make_custom_env do
import SomeModule, only: [some_function: 2], warn: false
@@ -22,9 +16,10 @@ defmodule Macro.Env do
__ENV__
end
## Struct fields
You may then call `make_custom_env()` to get a struct with the desired
imports and aliases included.
The `Macro.Env` struct contains the following fields:
It contains the following fields:
* `context` - the context of the environment; it can be `nil`
(default context), `:guard` (inside a guard) or `:match` (inside a match)
@@ -102,10 +97,8 @@ defmodule Macro.Env do
]
# Define the __struct__ callbacks by hand for bootstrap reasons.
{_struct, [], escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(escaped_struct)
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(:elixir_quote.escape(struct, false, :none))
def __struct__(unquote(kv)), do: unquote(body)
@doc """
@@ -170,13 +163,43 @@ defmodule Macro.Env do
[file: file, line: line]
end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the alias for the given atom.
Returns `{:ok, alias}` if the alias exists, `:error`
otherwise.
## Examples
iex> alias Foo.Bar, as: Baz
iex> Baz
Foo.Bar
iex> Macro.Env.fetch_alias(__ENV__, :Baz)
{:ok, Foo.Bar}
iex> Macro.Env.fetch_alias(__ENV__, :Unknown)
:error
"""
@doc since: "1.13.0"
@spec fetch_alias(t, atom) :: {:ok, atom} | :error
def fetch_alias(env, atom)
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the macro alias for the given atom.
Returns `{:ok, macro_alias}` if the alias exists, `:error`
otherwise.
A macro alias is only used inside quoted expansion. See
`fetch_alias/2` for a more general example.
"""
@doc since: "1.13.0"
@spec fetch_macro_alias(t, atom) :: {:ok, atom} | :error
def fetch_macro_alias(env, atom)
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
@@ -189,14 +212,6 @@ defmodule Macro.Env do
`{:function | :macro, module}`. The elements in the list
are in no particular order and the order is not guaranteed.
> #### Use only for introspection {: .warning}
>
> This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes
> but to do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`.
## Examples
iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
@@ -288,337 +303,8 @@ defmodule Macro.Env do
%{env | tracers: [tracer | tracers]}
end
trace_option = """
`:trace` - when set to `false`, it disables compilation tracers and
lexical tracker. This option must only be used by language servers and
other tools that need to introspect code without affecting how it is compiled.
Disabling tracer inside macros or regular code expansion is extremely
discouraged as it blocks the compiler from accurately tracking dependencies\
"""
@doc """
Defines the given `module` as required in the environment.
It does not check or assert the module is available.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:require` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.required?(env, Integer)
false
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Integer)
iex> Macro.Env.required?(env, Integer)
true
If the `:as` option is given, it will also define an alias:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
env = :elixir_aliases.require(meta, module, opts, env, trace)
result = :elixir_aliases.alias(meta, module, false, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
@doc """
Defines the given `module` as imported in the environment.
It assumes `module` is available. This is used by tools which
need to mimic the Elixir compiler. The appropriate `:import`
compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports
* #{trace_option}
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], List)
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}]
It accepts the same options as `Kernel.SpecialForm.import/2`:
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], Integer, only: :macros)
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[{:macro, Integer}]
## Info callback override
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], SomeModule, [info_callback: fn :functions -> [{:flatten, 1}]; :macros -> [{:some, 2}]; end])
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, SomeModule}]
iex> Macro.Env.lookup_import(env, {:some, 2})
[{:macro, SomeModule}]
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
{warnings, opts} = Keyword.pop(opts, :emit_warnings, true)
{info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1)
result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
maybe_define_error(result, :elixir_import)
end
@doc """
Defines the given `as` an alias to `module` in the environment.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:alias` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.expand_alias(env, [], [:Baz])
:error
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
iex> Macro.Env.expand_alias(env, [], [:Baz, :Bat])
{:alias, Foo.Bar.Bat}
If no `:as` option is given, the alias will be inferred from the module:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar)
iex> Macro.Env.expand_alias(env, [], [:Bar])
{:alias, Foo.Bar}
If it is not possible to infer one, an error is returned:
iex> Macro.Env.define_alias(__ENV__, [line: 10], :an_atom)
{:error,
"alias cannot be inferred automatically for module: :an_atom, " <>
"please use the :as option. Implicit aliasing is only supported with Elixir modules"}
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
result = :elixir_aliases.alias(meta, module, true, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
defp maybe_define_error({:ok, _info, env}, _mod),
do: {:ok, env}
defp maybe_define_error({:error, reason}, mod),
do: {:error, Kernel.to_string(mod.format_error(reason))}
@doc """
Expands an alias given by the alias segments.
It returns `{:alias, alias}` if the segments is a list
of atoms and an alias was found. Returns `:error` otherwise.
This expansion may emit the `:alias_expansion` trace event
but it does not emit the `:alias_reference` one.
## Options
* #{trace_option}
## Examples
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList])
{:alias, List}
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList, :Nested])
{:alias, List.Nested}
If there is no alias or the alias starts with `Elixir.`
(which disables aliasing), then `:error` is returned:
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:Elixir, MyList])
:error
iex> Macro.Env.expand_alias(__ENV__, [], [:AnotherList])
:error
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_aliases.expand(meta, list, env, trace) do
atom when is_atom(atom) -> {:alias, atom}
[_ | _] -> :error
end
end
@doc """
Expands an import given by `name` and `arity`.
If the import points to a macro, it returns a tuple
with the module and a function that expands the macro.
The function expects the metadata to be attached to the
expansion and the arguments of the macro.
If the import points to a function, it returns a tuple
with the module and the function name.
If any import is found, the appropriate compiler tracing
event will be emitted.
Otherwise returns `{:error, reason}`.
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
def expand_import(env, meta, name, arity, opts \\ [])
when is_list(meta) and is_atom(name) and is_integer(arity) and is_list(opts) do
case :elixir_import.special_form(name, arity) do
true ->
{:error, :not_found}
false ->
allow_locals = Keyword.get(opts, :allow_locals, true)
trace = Keyword.get(opts, :trace, true)
module = env.module
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
{:function, receiver, name} ->
{:function, receiver, name}
error ->
{:error, error}
end
end
end
@doc """
Expands a require given by `module`, `name`, and `arity`.
If the require points to a macro and the module has been
required, it returns a tuple with the module and a function
that expands the macro. The function expects the metadata
to be attached to the expansion and the arguments of the macro.
The appropriate `:remote_macro` compiler tracing event will
be emitted if a macro is found (note a `:remote_function`
event is not emitted in `:error` cases).
Otherwise returns `:error`.
## Options
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
when is_list(meta) and is_atom(module) and is_atom(name) and is_integer(arity) and
is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_dispatch.expand_require(meta, module, name, arity, env, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
:error ->
:error
end
end
defp wrap_expansion(receiver, expander, meta, name, arity, env, opts) do
fn expansion_meta, args ->
if Keyword.get(opts, :check_deprecations, true) do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
end
@doc """
Returns an environment in the guard context.
"""
@doc since: "1.17.0"
@spec to_guard(t) :: t
def to_guard(%{__struct__: Macro.Env} = env) do
%{env | context: :guard}
end
@doc """
Returns an environment in the match context.
Returns a `Macro.Env` in the match context.
"""
@spec to_match(t) :: t
def to_match(%{__struct__: Macro.Env} = env) do
+16 -13
View File
@@ -96,11 +96,11 @@ defmodule Map do
iex> %{map | one: "one"}
%{one: "one", two: 2}
Or any other keys:
Or any other key:
iex> other_map = %{"three" => 3, "four" => 4, "five" => 5}
iex> %{other_map | "three" => "three", "four" => "four"}
%{"five" => 5, "four" => "four", "three" => "three"}
iex> other_map = %{"three" => 3, "four" => 4}
iex> %{other_map | "three" => "three"}
%{"four" => 4, "three" => "three"}
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
@@ -128,8 +128,6 @@ defmodule Map do
@doc """
Builds a map from the given `keys` and the fixed `value`.
Inlined by the compiler.
## Examples
iex> Map.from_keys([1, 2, 3], :number)
@@ -138,7 +136,9 @@ defmodule Map do
"""
@doc since: "1.14.0"
@spec from_keys([key], value) :: map
defdelegate from_keys(keys, value), to: :maps
def from_keys(keys, value) do
:maps.from_keys(keys, value)
end
@doc """
Returns all keys from `map`.
@@ -709,10 +709,10 @@ defmodule Map do
end
@doc """
Removes and returns the value associated with `key` in `map` alongside
the updated map, or raises if `key` is not present.
Removes the value associated with `key` in `map` and returns the value
and the updated map, or it raises if `key` is not present.
Behaves the same as `pop/3` but raises a `KeyError` exception if `key` is not present in `map`.
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
## Examples
@@ -1011,8 +1011,9 @@ defmodule Map do
@doc """
Converts a `struct` to map.
It accepts a struct and simply removes the `__struct__` field
from the given struct.
It accepts the struct module or a struct itself and
simply removes the `__struct__` field from the given struct
or from a new struct generated from the given module.
## Example
@@ -1020,13 +1021,15 @@ defmodule Map do
defstruct [:name]
end
Map.from_struct(User)
#=> %{name: nil}
Map.from_struct(%User{name: "john"})
#=> %{name: "john"}
"""
@spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
delete(struct.__struct__(), :__struct__)
end
+1 -1
View File
@@ -401,7 +401,7 @@ defmodule MapSet do
"""
@doc since: "1.15.0"
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
@spec split_with(MapSet.t(), (any() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
{%MapSet{map: while_true}, %MapSet{map: while_false}}
+73 -115
View File
@@ -270,9 +270,6 @@ defmodule Module do
a dependency on it, causing the module to be recompiled as soon
as the file is added.
For more control over when a module is recompiled, see
[`__mix_recompile__?/0`](`m:Mix.Tasks.Compile.Elixir#module-__mix_recompile__-0`).
### `@file`
Changes the filename used in stacktraces for the function or macro that
@@ -444,8 +441,7 @@ defmodule Module do
* `@typep` - defines a private type to be used in `@spec`
* `@opaque` - defines an opaque type to be used in `@spec`
* `@spec` - provides a specification for a function
* `@callback` - provides a specification for a behaviour callback (and generates
a `behaviour_info/1` function in the module, see below)
* `@callback` - provides a specification for a behaviour callback
* `@macrocallback` - provides a specification for a macro behaviour callback
* `@optional_callbacks` - specifies which behaviour callbacks and macro
behaviour callbacks are optional
@@ -542,9 +538,7 @@ defmodule Module do
undefined functions, deprecations, etc. A module is always verified after
it is compiled. In Mix projects, a module is also verified when any of its
runtime dependencies change. Therefore this is useful to perform verification
of the current module while avoiding compile-time dependencies. Given the
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
of the current module while avoiding compile-time dependencies.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
@@ -594,78 +588,6 @@ defmodule Module do
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
the given module or the given `Mod.fun/arity` are not defined
## Generated functions
Sometimes the compiler will generate public functions within modules. These
are documented below.
### `behaviour_info/1`
This function is generated for modules that define a behaviour, that is,
that have one or more `@callback` definitions. The signature for this function,
expressed as a spec, is:
@spec behaviour_info(:callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec behaviour_info(:optional_callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
`behaviour_info(:callbacks)` includes optional callbacks.
For example:
iex> Enum.sort(GenServer.behaviour_info(:callbacks))
[
code_change: 3,
format_status: 1,
format_status: 2,
handle_call: 3,
handle_cast: 2,
handle_continue: 2,
handle_info: 2,
init: 1,
terminate: 2
]
### `module_info/0`
This function is generated for all modules. It returns all the attributes
returned by `module_info/1` (see below), but as a single keyword list. See also the
[Erlang documentation](https://www.erlang.org/doc/system/modules.html#module_info-0).
### `module_info/1`
This function is generated for all modules and returns
information about the module. The signature for this function,
expressed as a spec, is:
@spec module_info(:module) :: module() # Returns the module itself
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: module()
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
For example:
iex> URI.module_info(:module)
URI
iex> {:decode_www_form, 1} in URI.module_info(:exports)
true
For more information about `module_info/1`, also check out the [Erlang
documentation](https://www.erlang.org/doc/system/modules.html#module_info-1).
### `__info__/1`
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@@ -704,8 +626,7 @@ defmodule Module do
@callback __info__(:macros) :: keyword()
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
@doc """
Returns information about module attributes used by Elixir.
@@ -746,10 +667,6 @@ defmodule Module do
doc:
"If set to `true` generates a default protocol implementation for all types (inside `defprotocol`)."
},
undefined_impl_description: %{
doc:
"A string with additional description to be used on `Protocol.UndefinedError` when looking up the implementation fails."
},
for: %{
doc:
"The current module/type a protocol implementation is being defined for (inside `defimpl`)."
@@ -834,7 +751,50 @@ defmodule Module do
:elixir_module.is_open(module)
end
@deprecated "Use Code.eval_quoted/3 instead"
@doc """
Evaluates the quoted contents in the given module's context.
A list of environment options can also be given as argument.
See `Code.eval_string/3` for more information.
Raises an error if the module was already compiled.
## Examples
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__MODULE__, contents)
end
Foo.sum(1, 2)
#=> 3
For convenience, you can pass any `Macro.Env` struct, such
as `__ENV__/0`, as the first argument or as options. Both
the module and all options will be automatically extracted
from the environment:
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__ENV__, contents)
end
Foo.sum(1, 2)
#=> 3
Note that if you pass a `Macro.Env` struct as first argument
while also passing `opts`, they will be merged with `opts`
having precedence.
"""
@spec eval_quoted(module | Macro.Env.t(), Macro.t(), list, keyword | Macro.Env.t()) :: term
def eval_quoted(module_or_env, quoted, binding \\ [], opts \\ [])
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts)
@@ -865,8 +825,7 @@ defmodule Module do
the given quoted expressions.
The line where the module is defined and its file **must**
be passed as options. See `Code.env_for_eval/1` for a complete
list of options.
be passed as options.
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
@@ -910,7 +869,7 @@ defmodule Module do
end
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
if not Keyword.has_key?(opts, :file) do
unless Keyword.has_key?(opts, :file) do
raise ArgumentError, "expected :file to be given as option"
end
@@ -921,7 +880,7 @@ defmodule Module do
defp create(meta, module, quoted, env_or_opts) do
next = :elixir_module.next_counter(nil)
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
:elixir_module.compile(meta, module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
end
@doc """
@@ -946,8 +905,7 @@ defmodule Module do
@doc """
Concatenates two aliases and returns a new alias.
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
It handles binaries and atoms.
## Examples
@@ -957,9 +915,6 @@ defmodule Module do
iex> Module.concat(Foo, "Bar")
Foo.Bar
iex> Module.concat(Foo, nil)
Foo
"""
@spec concat(binary | atom, binary | atom) :: atom
def concat(left, right)
@@ -1424,7 +1379,15 @@ defmodule Module do
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
assert_not_readonly!(__ENV__.function, module)
:elixir_def.take_definition(module, {name, arity}) != false
case :elixir_def.take_definition(module, {name, arity}) do
false ->
false
_ ->
:elixir_locals.yank({name, arity}, module)
true
end
end
@doc """
@@ -1453,7 +1416,8 @@ defmodule Module do
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
neighbours = :elixir_locals.yank(tuple, module)
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
end
other ->
@@ -1477,7 +1441,7 @@ defmodule Module do
"to defoverridable/1 because #{error_explanation}"
end
behaviour_callbacks = Module.Behaviour.callbacks(behaviour)
behaviour_callbacks = Module.Types.Behaviour.callbacks(behaviour)
tuples =
for definition <- definitions_in(module),
@@ -1677,11 +1641,11 @@ defmodule Module do
case :ets.lookup(set, key) do
[{_, _, :accumulate, traces}] ->
trace_attribute(true, module, traces, set, key, nil, [])
trace_attribute(true, module, traces, set, key, [])
reverse_values(:ets.take(bag, {:accumulate, key}), [])
[{_, value, _, traces}] ->
trace_attribute(module, key, nil, traces)
trace_attribute(module, traces)
:ets.delete(set, key)
value
@@ -1944,7 +1908,7 @@ defmodule Module do
_ ->
message =
case :ets.lookup(set, {:elixir, :struct}) do
case :ets.lookup(set, :__struct__) do
[] ->
"warning: module attribute @derive was set but never used (it must come before defstruct)"
@@ -1994,19 +1958,16 @@ defmodule Module do
{set, bag} = data_tables_for(module)
case :ets.lookup(set, key) do
[{_, _, :unset, _}] ->
default
[{_, _, :accumulate, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
trace_attribute(trace?, module, traces, set, key, caller_line, [{3, :used}])
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
value
[{_, value, _, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
value
[] when is_integer(caller_line) ->
@@ -2034,15 +1995,12 @@ defmodule Module do
end
end
defp trace_attribute(module, key, caller_line, traces) do
defp trace_attribute(module, traces) do
:lists.foreach(
fn {line, lexical_tracker, tracers, aliases} ->
line = caller_line || line
env = %{
Macro.Env.__struct__()
| line: line,
file: "@#{key}",
lexical_tracker: lexical_tracker,
module: module,
tracers: tracers
@@ -2059,10 +2017,10 @@ defmodule Module do
)
end
defp trace_attribute(trace?, module, traces, set, key, caller_line, updates) do
defp trace_attribute(trace?, module, traces, set, key, updates) do
updates =
if trace? and traces != [] do
trace_attribute(module, key, caller_line, traces)
trace_attribute(module, traces)
updates ++ [{4, []}]
else
updates
@@ -2266,7 +2224,7 @@ defmodule Module do
end
defp preprocess_attribute(:nifs, value) do
if not function_arity_list?(value) do
unless function_arity_list?(value) do
raise ArgumentError,
"@nifs is a built-in module attribute for specifying a list " <>
"of functions and their arities that are NIFs, got: #{inspect(value)}"
+258
View File
@@ -0,0 +1,258 @@
# This is an Elixir module responsible for tracking
# calls in order to extract Elixir modules' behaviour
# during compilation time.
#
# ## Implementation
#
# The implementation uses ETS to track all dependencies
# resembling a graph. The keys and what they point to are:
#
# * `:reattach` points to `{name, arity}`
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
# * `{:import, {name, arity}}` points to `Module`
#
# This is built on top of the internal module tables.
defmodule Module.LocalsTracker do
@moduledoc false
@defmacros [:defmacro, :defmacrop]
@doc """
Adds and tracks defaults for a definition into the tracker.
"""
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
for i <- :lists.seq(arity - defaults, arity - 1) do
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
end
:ok
end
@doc """
Adds a local dispatch from-to the given target.
"""
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
:ok
end
@doc """
Adds an import dispatch to the given target.
"""
def add_import({set, _bag}, function, module, imported)
when is_tuple(function) and is_atom(module) do
put_edge(set, {:import, imported}, module)
:ok
end
@doc """
Yanks a local node. Returns its in and out vertices in a tuple.
"""
def yank({_set, bag}, local) do
:lists.usort(take_out_neighbours(bag, {:local, local}))
end
@doc """
Reattach a previously yanked node.
"""
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
for out_neighbour <- out_neighbours do
put_edge(bag, {:local, function}, out_neighbour)
end
# Make a call from the old function to the new one
if function != tuple do
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
end
# Finally marked the new one as reattached
put_edge(bag, :reattach, tuple)
:ok
end
@doc """
Collect all conflicting imports with the given functions
"""
def collect_imports_conflicts({set, _bag}, all_defined) do
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
{meta, {n, pair}}
end
end
@doc """
Collect all unused definitions based on the private
given, also accounting the expected number of default
clauses a private function have.
"""
def collect_unused_locals({_set, bag}, all_defined, private) do
reachable =
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
if kind in [:def, :defmacro] do
reachable_from(bag, pair, acc)
else
acc
end
end)
reattached = :lists.usort(out_neighbours(bag, :reattach))
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
end
@doc """
Collect undefined functions based on local calls and existing definitions.
"""
def collect_undefined_locals({set, bag}, all_defined, file) do
undefined =
for {pair, _, meta, _} <- all_defined,
{{local_name, _} = local, position, macro_dispatch?} <-
out_neighbours(bag, {:local, pair}),
error = undefined_local_error(set, local, macro_dispatch?) do
file =
case Keyword.get(meta, :file) do
{keep_file, _keep_line} -> keep_file
nil -> file
end
meta = build_meta(position, local_name)
{pair, meta, file, local, error}
end
:lists.usort(undefined)
end
defp undefined_local_error(set, local, true) do
case :ets.member(set, {:def, local}) do
true -> false
false -> :undefined_function
end
end
defp undefined_local_error(set, local, false) do
try do
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
:incorrect_dispatch
else
false
end
catch
_, _ -> :undefined_function
end
end
defp unreachable(reachable, reattached, private) do
for {tuple, kind, _, _} <- private,
not reachable?(tuple, kind, reachable, reattached),
do: tuple
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private macros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
defp reachable?(tuple, :defp, reachable, _reattached) do
Map.has_key?(reachable, tuple)
end
defp collect_warnings(reachable, private) do
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({_, _, false, _}, acc, _reachable) do
acc
end
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
if Map.has_key?(reachable, tuple) do
acc
else
[{meta, {:unused_def, tuple, kind}} | acc]
end
end
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable) do
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
^min -> acc
^max -> [{meta, {:unused_args, tuple}} | acc]
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
end
end
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
case Map.has_key?(reachable, {name, max}) do
true -> min_reachable_default(max - 1, min, max, name, reachable)
false -> min_reachable_default(max - 1, min, last, name, reachable)
end
end
defp min_reachable_default(_max, _min, last, _name, _reachable) do
last
end
@doc """
Returns all local nodes reachable from `vertex`.
By default, all public functions are reachable.
A private function is only reachable if it has
a public function that it invokes directly.
"""
def reachable_from({_, bag}, local) do
bag
|> reachable_from(local, %{})
|> Map.keys()
end
defp reachable_from(bag, local, vertices) do
vertices = Map.put(vertices, local, true)
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
case acc do
%{^local => true} -> acc
_ -> reachable_from(bag, local, acc)
end
end)
end
defp get_line(meta), do: Keyword.get(meta, :line)
defp get_position(meta), do: {get_line(meta), meta[:column]}
defp build_meta(position, function_name) do
case position do
{line, nil} -> [line: line]
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
end
end
## Lightweight digraph implementation
defp put_edge(d, from, to) do
:ets.insert(d, {from, to})
end
defp out_neighbour(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> nil
end
end
defp out_neighbours(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> []
end
end
defp take_out_neighbours(d, from) do
Keyword.values(:ets.take(d, from))
end
end
+179 -172
View File
@@ -5,19 +5,20 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type kind() :: :def | :defmacro
@type mode() :: :elixir | :erlang
@doc """
Initializes the parallel checker process.
"""
def start_link(schedulers \\ nil) do
:proc_lib.start_link(__MODULE__, :init, [schedulers])
:gen_server.start_link(__MODULE__, schedulers, [])
end
@doc """
Stops the parallel checker process.
"""
def stop({checker, _table}) do
def stop(checker) do
send(checker, {__MODULE__, :stop})
:ok
end
@@ -28,30 +29,26 @@ defmodule Module.ParallelChecker do
def get do
case :erlang.get(:elixir_checker_info) do
{parent, nil} ->
{:ok, cache} = start_link()
put(parent, cache)
{parent, cache}
{:ok, checker} = start_link()
put(parent, checker)
{parent, checker}
{parent, cache} ->
{parent, cache}
{parent, checker} ->
{parent, checker}
end
end
@doc """
Stores the parallel checker information.
"""
def put(pid, {checker, table}) when is_pid(pid) do
:erlang.put(:elixir_checker_info, {pid, {checker, table}})
def put(pid, checker) when is_pid(pid) and is_pid(checker) do
:erlang.put(:elixir_checker_info, {pid, checker})
end
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, {checker, table}}, module, module_map, log?) do
inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?)
end
defp inner_spawn(pid, checker, table, module, info, log?) do
def spawn({pid, checker}, module, info, log?) do
ref = make_ref()
spawned =
@@ -59,25 +56,20 @@ defmodule Module.ParallelChecker do
mon_ref = Process.monitor(pid)
receive do
{^ref, :cache} ->
{^ref, :cache, ets} ->
Process.link(pid)
module_tuple =
cond do
is_tuple(info) ->
info
is_binary(info) ->
with {:ok, binary} <- File.read(info),
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
cache_from_module_map(table, module_map)
else
_ -> nil
end
module_map =
if is_map(info) do
info
else
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
end
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
@@ -86,8 +78,8 @@ defmodule Module.ParallelChecker do
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
if module_map do
check_module(module_map, {checker, ets}, log?)
else
[]
end
@@ -121,12 +113,12 @@ defmodule Module.ParallelChecker do
case :erlang.get(:elixir_checker_info) do
{_, nil} -> :ok
{_, cache} -> verify(cache, [])
{_, checker} -> verify(checker, [])
end
result
after
{_, cache} = :erlang.get(:elixir_checker_info)
{_, checker} = :erlang.get(:elixir_checker_info)
if previous != :undefined do
:erlang.put(:elixir_checker_info, previous)
@@ -134,7 +126,7 @@ defmodule Module.ParallelChecker do
:erlang.erase(:elixir_checker_info)
end
cache && stop(cache)
checker && stop(checker)
end
_ ->
@@ -149,13 +141,13 @@ defmodule Module.ParallelChecker do
the modules and adds the ExCk chunk to the binaries. Returns the updated
list of warnings from the verification.
"""
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn({self(), checker}, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -189,8 +181,20 @@ defmodule Module.ParallelChecker do
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
{:ok, checker} = start_link()
{checker, :gen_server.call(checker, :ets, :infinity)}
end
@doc """
Preloads a module into the cache. Call this function before any other
cache lookups for the module.
"""
@spec preload_module(cache(), module()) :: :ok
def preload_module({server, ets}, module) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, _}] -> :ok
[] -> cache_module({server, ets}, module)
end
end
@doc """
@@ -199,81 +203,74 @@ defmodule Module.ParallelChecker do
or if the function does not exist return `{:error, :function}`.
"""
@spec fetch_export(cache(), module(), atom(), arity()) ::
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
| {:error, :function | :module}
def fetch_export({checker, table}, module, fun, arity) do
case :ets.lookup(table, module) do
[] ->
cache_module({checker, table}, module)
fetch_export({checker, table}, module, fun, arity)
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
def fetch_export({_server, ets}, module, fun, arity) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, false}] ->
{:error, :module}
[{_key, mode}] ->
case :ets.lookup(table, {module, {fun, arity}}) do
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
[] -> {:error, :function}
end
end
end
## Module checking
defp check_module(module_tuple, cache, log?) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
module_tuple
behaviour_warnings =
Module.Behaviour.check_behaviours_and_impls(
module,
file,
line,
behaviours,
impls,
definitions
)
diagnostics =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
@doc """
Returns all exported functions and macros for the given module from
the cache.
"""
@spec all_exports(cache(), module()) :: [{atom(), arity()}]
def all_exports({_server, ets}, module) do
# This is only called after we get a deprecation notice
# so we can assume it's a cached module
ets
|> :ets.match({{:export, module, :"$1"}, :_, :_})
|> Enum.flat_map(& &1)
|> Enum.sort()
end
defp module_map_to_module_tuple(module_map) do
## Module checking
defp check_module(module_map, cache, log?) do
%{
module: module,
file: file,
line: line,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls,
after_verify: after_verify
uses_behaviours: uses_behaviours,
impls: impls
} = module_map
# TODO: Match on anno directly in Elixir v1.22+
line =
case module_map do
%{anno: anno} -> :erl_anno.line(anno)
%{line: line} -> line
end
behaviours = for {:behaviour, module} <- attributes, do: module
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
behaviour_warnings =
Module.Types.Behaviour.check_behaviours_and_impls(
module,
file,
line,
uses_behaviours,
impls,
definitions
)
warnings =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(log?)
module_map
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
warnings
end
defp extract_no_warn_undefined(compile_opts) do
@@ -294,60 +291,42 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
{ungrouped, grouped}
else
{[{[location], diagnostic} | ungrouped], grouped}
end
end)
grouped =
Enum.map(grouped, fn {_warning, {locations, diagnostic}} ->
{Enum.sort(locations), diagnostic}
end)
Enum.sort(ungrouped ++ grouped)
warnings
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
defp emit_warnings(warnings, log?) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
log? and print_warning(message, diagnostics)
diagnostics
end)
end
defp print_diagnostics([diagnostic]) do
:elixir_errors.print_diagnostic(diagnostic, true)
defp print_warning(message, [diagnostic]) do
:elixir_errors.print_warning(message, diagnostic)
end
defp print_diagnostics(diagnostics) do
:elixir_errors.print_diagnostics(diagnostics)
defp print_warning(message, grouped_warnings) do
:elixir_errors.print_warning_group(message, grouped_warnings)
end
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
file = Path.absname(file)
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
%{
severity: :warning,
source: source,
source: file,
file: file,
position: position_to_tuple(position),
message: IO.iodata_to_binary(message),
stacktrace: [to_stacktrace(file, position, mfa)],
span: nil
}
|> Map.merge(diagnostic)
end
defp position_to_tuple(position) do
@@ -372,15 +351,15 @@ defmodule Module.ParallelChecker do
## Cache
defp cache_module({checker, table}, module) do
if lock(checker, module) do
defp cache_module({server, ets}, module) do
if lock(server, module) do
object_code = :code.get_object_code(module)
# The chunk has more information, so that's our preference
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(table, module, contents.exports)
cache_chunk(ets, module, contents.exports)
else
_ ->
# Otherwise, if the module is loaded, use its info
@@ -388,28 +367,37 @@ defmodule Module.ParallelChecker do
true ->
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(table, module, exports, deprecated, %{}, mode)
cache_info(ets, module, exports, deprecated, mode)
false ->
# Or load exports from chunk
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
cache_info(table, module, exports, %{}, %{}, :erlang)
exports = Map.new(Enum.map(exports, &{&1, :def}))
cache_info(ets, module, exports, %{}, :erlang)
else
_ ->
:ets.insert(table, {module, false})
:ets.insert(ets, {{:cached, module}, false})
end
end
end
unlock(checker, module)
unlock(server, module)
end
end
defp info_exports(module) do
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
map =
Map.new(
[{{:__info__, 1}, :def}] ++
behaviour_exports(module) ++
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
Enum.map(module.__info__(:functions), &{&1, :def})
)
{:elixir, map}
rescue
_ -> {:erlang, module.module_info(:exports)}
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
end
defp info_deprecated(module) do
@@ -418,55 +406,70 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
defp cache_from_module_map(table, map) do
defp maybe_module_map(binary, module) when is_binary(binary) do
# If a module was compiled without debug_info,
# then there is no module_map for further verification.
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
{:debug_info_v1, backend, data} = chunk,
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
module_map
else
_ -> nil
end
end
defp cache_from_module_map(ets, map) do
exports =
[{:__info__, 1}] ++
[{{:__info__, 1}, :def}] ++
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
definitions_to_exports(map.definitions)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
module_map_to_module_tuple(map)
deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated, :elixir)
end
defp cache_info(table, module, exports, deprecated, sigs, mode) do
Enum.each(exports, fn fa ->
reason = Map.get(deprecated, fa)
:ets.insert(table, {{module, fa}, reason, Map.get(sigs, fa, :none)})
defp cache_info(ets, module, exports, deprecated, mode) do
Enum.each(exports, fn {{fun, arity}, kind} ->
reason = Map.get(deprecated, {fun, arity})
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
{{fun, arity}, kind}
end)
:ets.insert(table, {module, mode})
:ets.insert(ets, {{:cached, module}, mode})
end
defp cache_chunk(table, module, exports) do
Enum.each(exports, fn {{fun, arity}, info} ->
sig =
case info do
%{sig: {:strong, _, _} = sig} -> sig
%{sig: {:infer, _} = sig} -> sig
_ -> :none
end
defp cache_chunk(ets, module, exports) do
Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
:ets.insert(
table,
{{module, {fun, arity}}, Map.get(info, :deprecated), sig}
)
{{fun, arity}, kind}
end)
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(table, {module, :elixir})
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
:ets.insert(ets, {{:cached, module}, :elixir})
end
defp behaviour_exports(%{defines_behaviour: true}), do: [{:behaviour_info, 1}]
defp behaviour_exports(%{defines_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
defp behaviour_exports(%{defines_behaviour: false}), do: []
defp behaviour_exports(module) when is_atom(module) do
if function_exported?(module, :behaviour_info, 1) do
[{:behaviour_info, 1}]
[{{:behaviour_info, 1}, :def}]
else
[]
end
end
defp definitions_to_exports(definitions) do
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
if kind in [:def, :defmacro] do
[{function, kind}]
else
[]
end
end)
end
defp lock(server, module) do
:gen_server.call(server, {:lock, module}, :infinity)
end
@@ -482,22 +485,22 @@ defmodule Module.ParallelChecker do
## Server callbacks
def init(schedulers) do
table = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
:proc_lib.init_ack({:ok, {self(), table}})
ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
state = %{
ets: ets,
waiting: %{},
modules: [],
spawned: 0,
schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
}
:gen_server.enter_loop(__MODULE__, [], state)
{:ok, state}
end
def handle_call(:start, _from, %{modules: modules} = state) do
def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
for {pid, ref} <- modules do
send(pid, {ref, :cache})
send(pid, {ref, :cache, ets})
end
for {_pid, ref} <- modules do
@@ -509,10 +512,14 @@ defmodule Module.ParallelChecker do
{:reply, length(modules), run_checkers(state)}
end
def handle_call(:ets, _from, state) do
{:reply, state.ets, state}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
case waiting do
%{^module => from_list} ->
waiting = Map.put(state.waiting, module, [from | from_list])
%{^module => froms} ->
waiting = Map.put(state.waiting, module, [from | froms])
{:noreply, %{state | waiting: waiting}}
%{} ->
@@ -522,8 +529,8 @@ defmodule Module.ParallelChecker do
end
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
from_list = Map.fetch!(waiting, module)
Enum.each(from_list, &:gen_server.reply(&1, false))
froms = Map.fetch!(waiting, module)
Enum.each(froms, &:gen_server.reply(&1, false))
waiting = Map.delete(waiting, module)
{:reply, :ok, %{state | waiting: waiting}}
end
+463 -331
View File
@@ -1,304 +1,55 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
# perform subtyping and return its output (OUT).
#
# * :strict - Requires types signatures (not implemented).
# * Strong arrows with gradual performs subtyping and returns OUT
# * Weak arrows with gradual performs subtyping and returns OUT
#
# * :static - Type signatures have been given.
# * Strong arrows with gradual performs compatibility and returns OUT
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :dynamic - Type signatures have not been given.
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [__protocol__: 1, behaviour_info: 1]
@doc false
def infer(module, file, defs, private, used_private, env, {_, cache}) do
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
false -> false
end
end
handler = fn meta, fun_arity, stack, context ->
case local_handler(meta, fun_arity, stack, context, finder) do
false ->
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
false
{kind, _, _} = triplet ->
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
false
else
triplet
end
end
end
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
else
{types, context}
end
end
# Now traverse all used privates to find any other private that have been used by them.
context =
%{local_sigs: used_sigs} =
for fun_arity <- used_private, reduce: context do
context ->
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
context
end
{unreachable, _context} =
Enum.reduce(private, {[], context}, fn
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(info, used_sigs, env)
# Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
# defp is reachable if used, defmacrop only if directly invoked
private_sigs = if kind == :defp, do: used_sigs, else: reachable_sigs
if is_map_key(private_sigs, fun_arity) do
{unreachable, context}
else
{[fun_arity | unreachable], context}
end
end)
{Map.new(types), unreachable}
defmodule Error do
defexception [:message]
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
end
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
env = %{env | function: stack.function, file: stack.file}
tuple = {reason, {fun, arity}, stack.module}
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
:ok
end
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
end
:ok
end
defp min_reachable_default(max, min, last, name, used) when max >= min do
fun_arity = {name, max}
case is_map_key(used, fun_arity) do
true -> min_reachable_default(max - 1, min, max, name, used)
false -> min_reachable_default(max - 1, min, last, name, used)
end
end
defp min_reachable_default(_max, _min, last, _name, _used) do
last
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern, Unify}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause}
false -> false
end
end
stack = stack()
handler = &local_handler(&1, &2, &3, &4, finder)
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
finder = fn _ -> {:dynamic, def} end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
context = warn_unused_clauses(defs, stack, context)
context.warnings
end
try do
warnings_from_clause(args, guards, body, def_expr, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
defp warn_unused_clauses(defs, stack, context) do
for {fun_arity, pending} <- context.local_used,
pending != [],
{_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0),
not Keyword.get(meta, :from_super, false),
reduce: context do
context ->
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
error =
Error.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
clauses_indexes =
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
end
The exception happened while checking this code:
defp local_handler(_meta, fun_arity, stack, context, finder) do
case context.local_sigs do
%{^fun_arity => {kind, inferred, _mapping}} ->
{kind, inferred, context}
#{Macro.to_string(def_expr)}
%{^fun_arity => kind} when is_atom(kind) ->
{kind, :none, context}
In case it is a bug, please report it at: https://github.com/elixir-lang/elixir/issues
""")
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
{kind, inferred, context}
false ->
false
reraise error, __STACKTRACE__
end
end
end
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
expected = List.duplicate(Descr.dynamic(), arity)
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
try do
{args_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{return_type, context} =
Expr.of_expr(body, stack, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end)
inferred = {:infer, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(context, clauses_context)}
end)
end
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc])
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
defp with_file_meta(stack, meta) do
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
:error -> stack
{:ok, {meta_file, _}} -> meta_file
:error -> file
end
end
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
exception =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
#{Exception.format_banner(:error, e, trace)}\
The exception happened while checking this code:
#{Macro.to_string(def_expr)}
Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
reraise exception, trace
end
defp guards_to_expr([], left) do
left
end
@@ -307,94 +58,475 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
head_stack = Unify.push_expr_stack(def_expr, stack)
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
{:ok, _type, context} <- Expr.of_expr(body, :dynamic, stack, context) do
context.warnings
else
{:error, {type, error, context}} ->
[error_to_warning(type, error, context) | context.warnings]
end
end
@doc false
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
when mode in @modes do
def context(file, module, function, no_warn_undefined, cache) do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
# File of module
file: file,
# Module of definitions
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A tuple with cache information (may be nil)
# A list of cached modules received from the parallel compiler
cache: cache,
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
# Expression variable to type variable
vars: %{},
# Type variable to expression variable
types_to_vars: %{},
# Type variable to type
types: %{},
# Trace of all variables that have been refined to a type,
# including the type they were refined to, why, and where
traces: %{},
# Counter to give type variables unique names
counter: 0,
# Track if a variable was inferred from a type guard function such is_tuple/1
# or a guard function that fails such as elem/2, possible values are:
# `:guarded` when `is_tuple(x)`
# `:guarded` when `is_tuple and elem(x, 0)`
# `:fail` when `elem(x, 0)`
guard_sources: %{},
# A list with all warnings from the running the code
warnings: []
}
end
@doc false
def context() do
def stack() do
%{
# A list of all warnings found so far
warnings: [],
# All vars and their types
vars: %{},
# Variables and arguments from patterns
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{}
# Stack of variables we have refined during unification,
# used for creating relevant traces
unify_stack: [],
# Last expression we have recursed through during inference,
# used for tracing
last_expr: nil,
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
# There are two factors that control how we track guards.
#
# * consider_type_guards?: if type guards should be considered.
# This applies only at the root and root-based "and" and "or" nodes.
#
# * keep_guarded? - if a guarded clause should remain as guarded
# even on failure. Used on the right side of and.
#
type_guards: {_consider_type_guards? = true, _keep_guarded? = false},
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: nil
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
## ERROR TO WARNING
# Collect relevant information from context and traces to report error
def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do
{fun, arity} = context.function
location = {context.file, get_position(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[signature | args], traces} = lift_all_types([signature | args], traces, context)
error = {:unable_apply, mfa, args, expected, signature, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
location = {context.file, get_position(stack), {context.module, fun, arity}}
traces = type_traces(stack, context)
{[left, right], traces} = lift_all_types([left, right], traces, context)
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp restore_context(%{vars: vars, failed: failed}, later_context) do
%{later_context | vars: vars, failed: failed}
defp get_position(stack) do
get_meta(stack.last_expr)
end
## Diagnostics
# Collect relevant traces from context.traces using stack.unify_stack
defp type_traces(stack, context) do
# TODO: Do we need the unify_stack or is enough to only get the last variable
# in the stack since we get related variables anyway?
stack =
stack.unify_stack
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|> Enum.uniq()
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
}
Enum.flat_map(stack, fn var_index ->
with %{^var_index => traces} <- context.traces,
%{^var_index => expr_var} <- context.types_to_vars do
Enum.map(traces, &tag_trace(expr_var, &1, context))
else
_other -> []
end
end)
end
## Module errors
defp related_variables(var, types) do
Enum.flat_map(types, fn
{related_var, {:var, ^var}} ->
[related_var | related_variables(related_var, types)]
def format_error({:unused_args, {name, arity}}),
do: "default values for the optional arguments in #{name}/#{arity} are never used"
_ ->
[]
end)
end
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
# Tag if trace is for a concrete type or type variable
defp tag_trace(var, {type, expr, location}, context) do
with {:var, var_index} <- type,
%{^var_index => expr_var} <- context.types_to_vars do
{:var, var, expr_var, expr, location}
else
_ -> {:type, var, type, expr, location}
end
end
def format_error({:unused_args, {name, arity}, count}),
do:
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
defp lift_all_types(types, traces, context) do
trace_types = for({:type, _, type, _, _} <- traces, do: type)
{types, lift_context} = Unify.lift_types(types, context)
{trace_types, _lift_context} = Unify.lift_types(trace_types, lift_context)
def format_error({:unused_def, {name, arity}, :defp}),
do: "function #{name}/#{arity} is unused"
{traces, []} =
Enum.map_reduce(traces, trace_types, fn
{:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc}
other, acc -> {other, acc}
end)
def format_error({:unused_def, {name, arity}, :defmacrop}),
do: "macro #{name}/#{arity} is unused"
{types, traces}
end
def format_error({:undefined_function, {f, a}, _})
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
do:
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
## FORMAT WARNINGS
def format_error({:undefined_function, {f, a}, module}),
do:
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do
{original_module, original_function, arity} = mfa
{_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, [])
{module, function, ^arity} = call_to_mfa(mfa_or_fa)
format_mfa = Exception.format_mfa(module, function, arity)
{traces, [] = _hints} = format_traces(traces, [], false)
def format_error({:incorrect_dispatch, {f, a}, _module}),
do: "cannot invoke macro #{f}/#{a} before its definition"
clauses =
Enum.map(signature, fn {ins, out} ->
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
{:fun, [{ins, out}]}
|> Unify.format_type(false)
|> IO.iodata_to_binary()
|> binary_slice(1..-2//1)
end)
[
"expected #{format_mfa} to have signature:\n\n ",
Enum.map_join(args, ", ", &Unify.format_type(&1, false)),
" -> #{Unify.format_type(expected, false)}",
"\n\nbut it has signature:\n\n ",
indent(Enum.join(clauses, "\n")),
"\n\n",
format_expr(expr, location),
traces,
"Conflict found at"
]
end
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
if map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) do
{:ok, atom, known_atoms} = missing_field(left, right)
# Drop the last trace which is the expression map.foo
traces = Enum.drop(traces, 1)
{traces, hints} = format_traces(traces, [left, right], true)
[
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.join(Enum.sort(known_atoms), ", "),
"\n\n",
traces,
format_message_hints(hints),
"Conflict found at"
]
else
simplify_left? = simplify_type?(left, right)
simplify_right? = simplify_type?(right, left)
{traces, hints} = format_traces(traces, [left, right], simplify_left? or simplify_right?)
[
"incompatible types:\n\n ",
Unify.format_type(left, simplify_left?),
" !~ ",
Unify.format_type(right, simplify_right?),
"\n\n",
format_expr(expr, location),
traces,
format_message_hints(hints),
"Conflict found at"
]
end
end
defp missing_field(
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
{:map, fields}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(
{:map, fields},
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(_, _), do: :error
defp matched_missing_field(fields, type, atom) do
if List.keymember?(fields, type, 1) do
:error
else
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
{:ok, atom, known_atoms}
end
end
defp format_traces([], _types, _simplify?) do
{[], []}
end
defp format_traces(traces, types, simplify?) do
traces
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map_reduce([], fn
{:type, var, type, expr, location}, hints ->
{hint, hints} = format_type_hint(type, types, expr, hints)
trace = [
"where \"",
Macro.to_string(var),
"\" was given the type ",
Unify.format_type(type, simplify?),
hint,
" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
{:var, var1, var2, expr, location}, hints ->
trace = [
"where \"",
Macro.to_string(var1),
"\" was given the same type as \"",
Macro.to_string(var2),
"\" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
end)
end
defp format_location({file, position, _mfa}) do
format_location({file, position[:line]})
end
defp format_location({file, line}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line)], else: []
[file, ?:, line, ?\n]
end
defp simplify_type?(type, other) do
map_like_type?(type) and not map_like_type?(other)
end
## EXPRESSION FORMATTING
defp format_expr(nil, _location) do
[]
end
defp format_expr(expr, location) do
[
"in expression:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
end
@doc false
def expr_to_string(expr) do
expr
|> reverse_rewrite()
|> Macro.to_string()
end
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
## Hints
defp format_message_hints(hints) do
hints
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map(&[format_message_hint(&1), "\n"])
end
defp format_message_hint(:inferred_dot) do
"""
#{hint()} "var.field" (without parentheses) implies "var" is a map() while \
"var.fun()" (with parentheses) implies "var" is an atom()
"""
end
defp format_message_hint(:inferred_bitstring_spec) do
"""
#{hint()} all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behaviour.
"""
end
defp format_message_hint({:sized_and_unsize_tuples, {size, var}}) do
"""
#{hint()} use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \
tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple.
"""
end
defp hint, do: :elixir_errors.prefix(:hint)
defp format_type_hint(type, types, expr, hints) do
case format_type_hint(type, types, expr) do
{message, hint} -> {message, [hint | hints]}
:error -> {[], hints}
end
end
defp format_type_hint(type, types, expr) do
cond do
dynamic_map_dot?(type, expr) ->
{" (due to calling var.field)", :inferred_dot}
dynamic_remote_call?(type, expr) ->
{" (due to calling var.fun())", :inferred_dot}
inferred_bitstring_spec?(type, expr) ->
{[], :inferred_bitstring_spec}
message = sized_and_unsize_tuples(expr, types) ->
{[], {:sized_and_unsize_tuples, message}}
true ->
:error
end
end
defp dynamic_map_dot?(type, expr) do
with true <- map_type?(type),
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
true <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp dynamic_remote_call?(type, expr) do
with true <- atom_type?(type),
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
false <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp inferred_bitstring_spec?(type, expr) do
with true <- integer_type?(type),
{:<<>>, _, args} <- expr,
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
true
else
_ -> false
end
end
defp sized_and_unsize_tuples({{:., _, [:erlang, :is_tuple]}, _, [var]}, types) do
case Enum.find(types, &match?({:tuple, _, _}, &1)) do
{:tuple, size, _} ->
{size, var}
nil ->
nil
end
end
defp sized_and_unsize_tuples(_expr, _types) do
nil
end
## Formatting helpers
defp indent(string) do
String.replace(string, "\n", "\n ")
end
defp map_type?({:map, _}), do: true
defp map_type?(_other), do: false
defp map_like_type?({:map, _}), do: true
defp map_like_type?({:union, union}), do: Enum.any?(union, &map_like_type?/1)
defp map_like_type?(_other), do: false
defp atom_type?(:atom), do: true
defp atom_type?({:atom, _}), do: false
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
defp atom_type?(_other), do: false
defp integer_type?(:integer), do: true
defp integer_type?(_other), do: false
defp call_to_mfa({{:., _, [mod, fun]}, _, args}), do: {mod, fun, length(args)}
defp call_to_mfa({fun, _, args}) when is_atom(fun), do: {Kernel, fun, length(args)}
end
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,10 @@
defmodule Module.Behaviour do
defmodule Module.Types.Behaviour do
# Checking functionality for @behaviours and @impl
@moduledoc false
@doc false
def callbacks(behaviour) do
for callback <- behaviour.behaviour_info(:callbacks) do
for callback <- behaviour_info(behaviour, :callbacks) do
{pair, _kind} = normalize_macro_or_function_callback(callback)
pair
end
@@ -33,7 +33,9 @@ defmodule Module.Behaviour do
module: module,
file: file,
line: line,
# Map containing the callbacks to be implemented
callbacks: %{},
# list of warnings {message, env}
warnings: []
}
end
@@ -55,8 +57,8 @@ defmodule Module.Behaviour do
warn(context, {:module_does_not_define_behaviour, context.module, behaviour})
true ->
optional_callbacks = behaviour.behaviour_info(:optional_callbacks)
callbacks = behaviour.behaviour_info(:callbacks)
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
callbacks = behaviour_info(behaviour, :callbacks)
Enum.reduce(callbacks, context, &add_callback(&2, &1, behaviour, optional_callbacks))
end
end)
@@ -142,7 +144,7 @@ defmodule Module.Behaviour do
end
defp impl_behaviours({function, arity}, defaults, kind, value, behaviours, callbacks) do
impls = for n <- (arity - defaults)..arity, do: {function, n}
impls = for n <- arity..(arity - defaults), do: {function, n}
impl_behaviours(impls, kind, value, behaviours, callbacks)
end
@@ -264,6 +266,13 @@ defmodule Module.Behaviour do
". The known callbacks are:\n#{formatted_callbacks}\n"
end
defp behaviour_info(module, key) do
case module.behaviour_info(key) do
list when is_list(list) -> list
:undefined -> []
end
end
defp normalize_macro_or_function_callback({function_name, arity}) do
case :erlang.atom_to_list(function_name) do
# Macros are always provided one extra argument in behaviour_info/1
@@ -275,11 +284,7 @@ defmodule Module.Behaviour do
end
end
def format_diagnostic(warning) do
%{message: IO.iodata_to_binary(format_warning(warning))}
end
defp format_warning({:undefined_behaviour, module, behaviour}) do
def format_warning({:undefined_behaviour, module, behaviour}) do
[
"@behaviour ",
inspect(behaviour),
@@ -289,12 +294,12 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
[
"the behaviour ",
inspect(behaviour),
@@ -306,7 +311,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
[
"conflicting behaviours found. Callback ",
format_definition(kind, callback),
@@ -320,7 +325,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
def format_warning({:missing_callback, module, callback, kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" is not implemented (in module ",
@@ -329,7 +334,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" was implemented as \"",
@@ -342,14 +347,14 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:private_function, callback, kind}) do
def format_warning({:private_function, callback, kind}) do
[
format_definition(kind, callback),
" is private, @impl attribute is always discarded for private functions/macros"
]
end
defp format_warning({:no_behaviours, callback, kind, value}) do
def format_warning({:no_behaviours, callback, kind, value}) do
[
"got \"@impl ",
inspect(value),
@@ -359,7 +364,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
[
"got \"@impl false\" for ",
format_definition(kind, callback),
@@ -368,7 +373,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_defined, callback, kind, callbacks}) do
def format_warning({:impl_defined, callback, kind, callbacks}) do
[
"got \"@impl true\" for ",
format_definition(kind, callback),
@@ -377,7 +382,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -387,7 +392,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -398,7 +403,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_impl, callback, kind, behaviour}) do
def format_warning({:missing_impl, callback, kind, behaviour}) do
[
"module attribute @impl was not set for ",
format_definition(kind, callback),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+129 -212
View File
@@ -2,8 +2,6 @@ defmodule Module.Types.Helpers do
# AST and enumeration helpers.
@moduledoc false
## AST helpers
@doc """
Guard function to check if an AST node is a variable.
"""
@@ -17,20 +15,9 @@ defmodule Module.Types.Helpers do
end
@doc """
Unpacks a list into head elements and tail.
Returns unique identifier for the current assignment of the variable.
"""
def unpack_list([{:|, _, [head, tail]}], acc), do: {Enum.reverse([head | acc]), tail}
def unpack_list([head], acc), do: {Enum.reverse([head | acc]), []}
def unpack_list([head | tail], acc), do: unpack_list(tail, [head | acc])
@doc """
Unpacks a match into several matches.
"""
def unpack_match({:=, _, [left, right]}, acc),
do: unpack_match(left, unpack_match(right, acc))
def unpack_match(node, acc),
do: [node | acc]
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
@doc """
Returns the AST metadata.
@@ -39,241 +26,171 @@ defmodule Module.Types.Helpers do
def get_meta(_other), do: []
@doc """
Attaches span information.
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
"""
def with_span(meta, name) do
:elixir_env.calculate_span(meta, name)
def reduce_ok(list, acc, fun) do
do_reduce_ok(list, acc, fun)
end
## Warnings
defp do_reduce_ok([head | tail], acc, fun) do
case fun.(head, acc) do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
@doc """
Formatted hints in typing errors.
"""
def format_hints(hints) do
hints
|> Enum.uniq()
|> Enum.map(fn
:inferred_bitstring_spec ->
"""
#{hint()} all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behavior.
"""
:dot ->
"""
#{hint()} "var.field" (without parentheses) means "var" is a map() while \
"var.fun()" (with parentheses) means "var" is an atom()
"""
:anonymous_rescue ->
"""
#{hint()} when you rescue without specifying exception names, \
the variable is assigned a type of a struct but all of its fields are unknown. \
If you are trying to access an exception's :message key, either specify the \
exception names or use `Exception.message/1`.
"""
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
This information is exposed to language servers and
therefore must remain backwards compatible.
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
version = meta[:version]
case vars do
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
end
defp collect_var_traces(traces) do
traces
|> Enum.reject(fn {_expr, _file, type, _formatter} ->
# As an otimization do not care about dynamic terms
type == %{dynamic: :term}
end)
|> case do
[] -> traces
filtered -> filtered
{:error, reason} ->
{:error, reason}
end
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
# This information is exposed to language servers and
# therefore must remain backwards compatible.
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
}
end)
|> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|> Enum.dedup()
end
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
@doc """
Format previously collected traces.
Like `Enum.unzip/1` but only continues while `fun` returns `{:ok, elem1, elem2}`
and stops on `{:error, reason}`.
"""
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
def unzip_ok(list) do
do_unzip_ok(list, [], [])
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces =
for trace <- traces do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.meta[:line], trace.meta[:column])
|> String.replace_suffix(":", "")
[
"""
# type: #{indent(trace.formatted_type, 4)}
# from: #{location}
\
""",
indent(trace.formatted_expr, 4),
?\n,
trace.formatted_hints
]
end
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
defp do_unzip_ok([{:ok, head1, head2} | tail], acc1, acc2) do
do_unzip_ok(tail, [head1 | acc1], [head2 | acc2])
end
defp do_unzip_ok([{:error, reason} | _tail], _acc1, _acc2), do: {:error, reason}
defp do_unzip_ok([], acc1, acc2), do: {:ok, Enum.reverse(acc1), Enum.reverse(acc2)}
@doc """
Formats a var for pretty printing.
Like `Enum.map/2` but only continues while `fun` returns `{:ok, elem}`
and stops on `{:error, reason}`.
"""
def format_var({var, _, context}), do: format_var(var, context)
def format_var(var, nil), do: "\"#{var}\""
def format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
def map_ok(list, fun) do
do_map_ok(list, [], fun)
end
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp do_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_map_ok(tail, [elem | acc], fun)
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
{:error, reason} ->
{:error, reason}
end
end
defp expr_hints(_), do: []
defp do_map_ok([], acc, _fun), do: {:ok, Enum.reverse(acc)}
@doc """
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
and stops on `{:error, reason}`.
"""
def expr_to_string(expr) do
expr
|> Macro.prewalk(fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
|> Macro.to_string()
def map_reduce_ok(list, acc, fun) do
do_map_reduce_ok(list, {[], acc}, fun)
end
defp erl_to_ex(
:erlang,
:error,
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
meta
) do
{:raise, meta, [expr]}
defp do_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elem, acc} ->
do_map_reduce_ok(tail, {[elem | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
@doc """
Like `Enum.flat_map/2` but only continues while `fun` returns `{:ok, list}`
and stops on `{:error, reason}`.
"""
def flat_map_ok(list, fun) do
do_flat_map_ok(list, [], fun)
end
defp do_flat_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_flat_map_ok(tail, [elem | acc], fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_ok([], acc, _fun), do: {:ok, Enum.reverse(Enum.concat(acc))}
@doc """
Like `Enum.flat_map_reduce/3` but only continues while `fun` returns `{:ok, list, acc}`
and stops on `{:error, reason}`.
"""
def flat_map_reduce_ok(list, acc, fun) do
do_flat_map_reduce_ok(list, {[], acc}, fun)
end
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elems, acc} ->
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
@doc """
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
if there are no errors.
"""
def oks_or_errors(list) do
case Enum.split_with(list, &match?({:ok, _}, &1)) do
{oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)}
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
end
end
@doc """
Indents new lines.
Combines a list of guard expressions `when x when y when z` to an expression
combined with `or`, `x or y or z`.
"""
def indent(content, count) do
String.replace(content, "\n", "\n" <> String.duplicate(" ", count))
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
def guards_to_or([]) do
[]
end
def guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
@doc """
Emits a warning.
Like `Enum.zip/1` but will zip multiple lists together instead of only two.
"""
def warn(module, warning, meta, stack, context) do
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, context)
end
def zip_many(lists) do
zip_many(lists, [], [[]])
end
defp effectively_warn(module, warning, meta, stack, context) do
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
defp zip_many([], [], [[] | acc]) do
map_reverse(acc, [], &Enum.reverse/1)
end
@doc """
Emits an error.
In practice an error is a warning that halts other errors from being collected.
"""
def error(module, warning, meta, stack, context) do
case context do
%{failed: true} ->
context
%{failed: false} ->
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, %{context | failed: true})
end
end
defp zip_many([], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [[] | [last | acc]])
end
@doc """
The type to return when there is an error.
"""
def error_type, do: Module.Types.Descr.dynamic()
defp zip_many([[] | _], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [last | acc])
end
defp zip_many([[elem | list1] | list2], remain, [last | acc]) do
zip_many(list2, [list1 | remain], [[elem | last] | acc])
end
defp map_reverse([], acc, _fun), do: acc
defp map_reverse([head | tail], acc, fun), do: map_reverse(tail, [fun.(head) | acc], fun)
end
+290 -440
View File
@@ -2,248 +2,137 @@ defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
alias Module.ParallelChecker
## Variables
import Module.Types.Helpers
import Module.Types.Unify
# There are important assumptions on how we work with maps.
#
# First, the keys in the map must be ordered by subtyping.
#
# Second, optional keys must be a superset of the required
# keys, i.e. %{required(atom) => integer, optional(:foo) => :bar}
# is forbidden.
#
# Third, in order to preserve co/contra-variance, a supertype
# must satisfy its subtypes. I.e. %{foo: :bar, atom() => :baz}
# is forbidden, it must be %{foo: :bar, atom() => :baz | :bar}.
#
# Once we support user declared maps, we need to validate these
# assumptions.
@doc """
Fetches the type of a defined variable.
Handles open maps (with dynamic => dynamic).
"""
def var({_name, meta, _context}, context) do
version = Keyword.fetch!(meta, :version)
%{vars: %{^version => %{type: type}}} = context
type
end
@doc """
Marks a variable with error.
"""
def error_var(var, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
put_in(context.vars[version], data)
end
@doc """
Refines the type of a variable.
"""
def refine_var(var, type, expr, formatter \\ :default, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
case context.vars do
%{^version => %{type: old_type, off_traces: off_traces} = data} ->
new_type = intersection(type, old_type)
data = %{
data
| type: new_type,
off_traces: new_trace(expr, type, formatter, stack, off_traces)
}
context = put_in(context.vars[version], data)
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, error_type(),
error({:refine_var, old_type, type, var, context}, meta, stack, context)}
else
{:ok, new_type, context}
def open_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
# If we match on a map such as %{"foo" => "bar"}, we cannot
# assert that %{binary() => binary()}, since we are matching
# only a single binary of infinite possible values. Therefore,
# the correct would be to match it to %{binary() => binary() | var}.
#
# We can skip this in two cases:
#
# 1. If the key is a singleton, then we know that it has no
# other value than the current one
#
# 2. If the value is a variable, then there is no benefit in
# creating another variable, so we can skip it
#
# For now, we skip generating the var itself and introduce
# :dynamic instead.
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) or match?({:var, _}, value) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end
%{} ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, formatter, stack, [])
}
context = put_in(context.vars[version], data)
{:ok, type, context}
end
end
defp new_trace(nil, _type, _formatter, _stack, traces),
do: traces
defp new_trace(expr, type, formatter, stack, traces),
do: [{expr, stack.file, type, formatter} | traces]
## Map/structs
@doc """
Handles fetching a map key.
"""
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
{_optional?, value_type} ->
{value_type, context}
reason ->
{error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@doc """
Builds a closed map.
Handles closed maps (without dynamic => dynamic).
"""
def closed_map(pairs, stack, context, of_fun) do
permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
def closed_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
defp map_pairs(pairs, stack, context, of_fun) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context),
{:ok, value_type, context} <- of_fun.(value, :dynamic, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
@doc """
Builds permutation of maps according to the given keys.
"""
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
context =
Enum.reduce(pairs, context, fn {key, value}, context ->
{_, context} = of_fun.(key, stack, context)
{_, context} = of_fun.(value, stack, context)
context
end)
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
{dynamic(), context}
defp closed_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
{[], pairs} -> pairs_to_unions(pairs, [], context)
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
end
end
def permutate_map(pairs, stack, context, of_fun, of_map) do
{dynamic?, fallback, single, multiple, assert, context} =
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
{key, value}, {dynamic?, fallback, single, multiple, assert, context} ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
{value_type, context} = of_fun.(value, stack, context)
dynamic? = dynamic? or dynamic_key? or gradual?(value_type)
defp pairs_to_unions([{key, value} | ahead], behind, context) do
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
case keys do
:none ->
fallback = union(fallback, value_type)
# In case nothing matches, use the original ahead
ahead = matched_ahead || ahead
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
all_values =
[value | values] ++
find_subtype_values(ahead, key, context) ++
find_subtype_values(behind, key, context)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
end
{dynamic?, fallback, [], [], assert, context}
defp pairs_to_unions([], acc, context) do
acc
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.map(fn {key, value} -> {:required, key, value} end)
end
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
defp find_subtype_values(pairs, key, context) do
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
end
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
end
end)
defp find_matching_values([{key, value} | ahead], key, acc, values) do
find_matching_values(ahead, key, acc, [value | values])
end
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
find_matching_values(ahead, key, [pair | acc], values)
end
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
end
|> Enum.reduce(&union/2)
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
defp find_matching_values([], _key, _acc, []), do: {nil, []}
@doc """
Handles structs.
"""
def struct(struct, meta, context) do
context = remote(struct, :__struct__, 0, meta, context)
entries =
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
{:required, {:atom, key}, :dynamic}
end
if dynamic?, do: {dynamic(map), context}, else: {map, context}
end
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
end
defp finite_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, stack, context)
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
end
defp cartesian_map(lists) do
case lists do
[] ->
[[]]
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
end
end
@doc """
Handles instantiation of a new struct.
"""
def struct_instance(struct, args, meta, stack, context, of_fun)
when is_atom(struct) do
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
{type, context} = of_fun.(value, stack, context)
{{key, type}, context}
end)
{info, context} = struct_info(struct, meta, stack, context)
{struct_type(struct, info, args_types), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
{:ok, info} -> {info, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{info, context}
end
end
@doc """
Builds a type from the struct info.
"""
def struct_type(struct, info, args_types \\ []) do
term = term()
pairs = for %{field: field} <- info, do: {field, term}
pairs = [{:__struct__, atom([struct])} | pairs]
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
closed_map(pairs)
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
end
## Binary
@@ -254,262 +143,223 @@ defmodule Module.Types.Of do
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context) do
context
def binary([], _stack, context, _of_fun) do
{:ok, context}
end
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
def binary([head], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
binary_segment(head, head_stack, context, of_fun)
end
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
end
def binary([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
end
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
end
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
when is_binary(left) or is_number(left) do
context
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{_type, context} =
case kind do
:match ->
Module.Types.Pattern.of_match_var(left, type, expr, stack, context)
:guard ->
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
:expr ->
{actual, context} = Module.Types.Expr.of_expr(left, stack, context)
intersect(actual, type, expr, stack, context)
end
specifier_size(kind, right, stack, context)
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Expr.of_expr(arg, stack, context)
{_, context} = intersect(actual, integer(), expr, stack, context)
context
end
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
context
end
defp specifier_size(_kind, _specifier, _stack, context) do
context
end
## Modules
@doc """
Returns modules in a type.
The call information is used on report reporting.
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
{mods, context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
## Warning helpers
defp binary_many([last], stack, context, of_fun) do
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
binary_segment(last, last_stack, context, of_fun)
end
defp binary_many([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do
expected_type =
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
float? = collect_binary_specifier(specifiers, &float_type?/1)
# Special case utf and float specifiers because they can be two types as literals
# but only a specific type as a variable in a pattern
cond do
stack.context == :pattern and utf? and is_binary(expr) ->
{:ok, context}
stack.context == :pattern and float? and is_integer(expr) ->
{:ok, context}
true ->
with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
end
defp collect_binary_specifier(other, fun) do
fun.(other)
end
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
defp binary_type(:pattern, {:float, _, _}), do: :float
defp binary_type(_context, {:integer, _, _}), do: :integer
defp binary_type(_context, {:bits, _, _}), do: :binary
defp binary_type(_context, {:bitstring, _, _}), do: :binary
defp binary_type(_context, {:bytes, _, _}), do: :binary
defp binary_type(_context, {:binary, _, _}), do: :binary
defp binary_type(_context, _specifier), do: nil
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
defp utf_type?(_), do: false
defp float_type?({:float, _, _}), do: true
defp float_type?(_), do: false
## Remote
@doc """
Intersects two types and emit an incompatible error if empty.
Handles remote calls.
"""
def intersect(actual, expected, expr, stack, context) do
type = intersection(actual, expected)
if empty?(type) do
{error_type(), incompatible_error(expr, expected, actual, stack, context)}
def remote(module, fun, arity, meta, context) when is_atom(module) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) do
context
else
{type, context}
ParallelChecker.preload_module(context.cache, module)
check_export(module, fun, arity, meta, context)
end
end
@doc """
Emits incompatible types warning for the given expression.
def remote(_module, _fun, _arity, _meta, context), do: context
This is a generic warning for when the expected/actual types
themselves may come from several different circumstances.
"""
def incompatible_error(expr, expected_type, actual_type, stack, context) do
meta = get_meta(expr) || stack.meta
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
warning = {:incompatible, expr, expected_type, actual_type, hints, context}
error(warning, meta, stack, context)
defp check_export(module, fun, arity, meta, context) do
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
{:ok, mode, :def, reason} ->
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:ok, mode, :defmacro, reason} ->
context = warn(meta, context, {:unrequired_module, module, fun, arity})
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, context) do
warn(meta, context, {:undefined_module, module, fun, arity})
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, context) do
exports = ParallelChecker.all_exports(context.cache, module)
warn(meta, context, {:undefined_function, module, fun, arity, exports})
else
context
end
end
end
defp error(warning, meta, stack, context) do
error(__MODULE__, warning, meta, stack, context)
defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do
if reason do
warn(meta, context, {:deprecated, module, fun, arity, reason})
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn(meta, context, {:deprecated, module, fun, arity, reason})
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> :string.titlecase()
reason = "It will be removed in #{removal}. #{reason}"
warn(meta, context, {:deprecated, module, fun, arity, reason})
_ ->
context
end
end
# The protocol code dispatches to unknown modules, so we ignore them here.
#
# try do
# SomeProtocol.Atom.__impl__
# rescue
# ...
# end
#
# But for protocols we don't want to traverse the protocol code anyway.
# TODO: remove this clause once we no longer traverse the protocol code.
defp warn_undefined?(_module, :__impl__, 1, _context), do: false
defp warn_undefined?(_module, :module_info, 0, _context), do: false
defp warn_undefined?(_module, :module_info, 1, _context), do: false
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, context) do
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, context, warning) do
{fun, arity} = context.function
location = {context.file, meta, {context.module, fun, arity}}
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
end
## Warning formatting
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in expression:
#{expr_to_string(expr) |> indent(4)}
got type:
#{to_quoted_string(actual_type) |> indent(4)}
but expected type:
#{to_quoted_string(expected_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
}
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_diagnostic({:badmap, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a map or struct when accessing .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints([:dot])
])
}
end
def format_diagnostic({:badkey, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
span: expr |> get_meta() |> :elixir_env.calculate_span(key) |> Keyword.get(:span),
message:
IO.iodata_to_binary([
"""
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
defp empty_if(condition, content) do
if condition, do: "", else: content
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
end
File diff suppressed because it is too large Load Diff
+992
View File
@@ -0,0 +1,992 @@
defmodule Module.Types.Unify do
@moduledoc false
import Module.Types.Helpers
# Those are the simple types known to the system:
#
# :dynamic
# {:var, var}
# {:atom, atom} < :atom
# :integer
# :float
# :binary
# :pid
# :port
# :reference
#
# Those are the composite types:
#
# {:list, type}
# {:tuple, size, [type]} < :tuple
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
# {:fun, [{params, return}]}
#
# Once new types are added, they should be considered in:
#
# * unify (all)
# * format_type (all)
# * subtype? (subtypes only)
# * recursive_type? (composite only)
# * collect_var_indexes (composite only)
# * lift_types (composite only)
# * flatten_union (composite only)
# * walk (composite only)
#
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(same, same, _stack, context) do
{:ok, same, context}
end
def unify({:var, var}, type, stack, context) do
unify_var(var, type, stack, context, _var_source = true)
end
def unify(type, {:var, var}, stack, context) do
unify_var(var, type, stack, context, _var_source = false)
end
def unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
{:error, reason} -> {:error, reason}
end
end
def unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
def unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
unify_maps(source_pairs, target_pairs, stack, context)
end
def unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
def unify(:dynamic, target, _stack, context) do
{:ok, target, context}
end
def unify({:union, types}, target, stack, context) do
unify_result =
map_reduce_ok(types, context, fn type, context ->
unify(type, target, stack, context)
end)
case unify_result do
{:ok, types, context} -> {:ok, to_union(types, context), context}
{:error, context} -> {:error, context}
end
end
def unify(source, target, stack, context) do
cond do
# TODO: This condition exists to handle unions with unbound vars.
match?({:union, _}, target) and has_unbound_var?(target, context) ->
{:ok, source, context}
subtype?(source, target, context) ->
{:ok, source, context}
true ->
error(:unable_unify, {source, target, stack}, context)
end
end
def unify_var(var, :dynamic, _stack, context, _var_source?) do
{:ok, {:var, var}, context}
end
def unify_var(var, type, stack, context, var_source?) do
case context.types do
%{^var => :unbound} ->
context = refine_var!(var, type, stack, context)
stack = push_unify_stack(var, stack)
if recursive_type?(type, [], context) do
if var_source? do
error(:unable_unify, {{:var, var}, type, stack}, context)
else
error(:unable_unify, {type, {:var, var}, stack}, context)
end
else
{:ok, {:var, var}, context}
end
%{^var => {:var, _} = var_type} ->
# Do not recursively traverse type vars for now
# to avoid pathological cases related to performance.
{:ok, var_type, context}
%{^var => var_type} ->
# Only add trace if the variable wasn't already "expanded"
context =
if variable_expanded?(var, stack, context) do
context
else
trace_var(var, type, stack, context)
end
stack = push_unify_stack(var, stack)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, {:var, ^var}, context} ->
{:ok, {:var, var}, context}
{:ok, res_type, context} ->
context = refine_var!(var, res_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
# * All required keys on each side need to match to the other side.
# * All optional keys on each side that do not match must be discarded.
def unify_maps(source_pairs, target_pairs, stack, context) do
{source_required, source_optional} = split_pairs(source_pairs)
{target_required, target_optional} = split_pairs(target_pairs)
with {:ok, source_required_pairs, context} <-
unify_source_required(source_required, target_pairs, stack, context),
{:ok, target_required_pairs, context} <-
unify_target_required(target_required, source_pairs, stack, context),
{:ok, source_optional_pairs, context} <-
unify_source_optional(source_optional, target_optional, stack, context),
{:ok, target_optional_pairs, context} <-
unify_target_optional(target_optional, source_optional, stack, context) do
# Remove duplicate pairs from matching in both left and right directions
pairs =
Enum.uniq(
source_required_pairs ++
target_required_pairs ++
source_optional_pairs ++
target_optional_pairs
)
{:ok, {:map, pairs}, context}
else
{:error, :unify} ->
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
{:error, context} ->
{:error, context}
end
end
def unify_source_required(source_required, target_pairs, stack, context) do
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{:required, source_key, source_value}]}
target_map = {:map, [{target_kind, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
def unify_target_required(target_required, source_pairs, stack, context) do
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{source_kind, source_key, source_value}]}
target_map = {:map, [{:required, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
def unify_source_optional(source_optional, target_optional, stack, context) do
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
Enum.find_value(target_optional, fn {target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
def unify_target_optional(target_optional, source_optional, stack, context) do
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
Enum.find_value(source_optional, fn {source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp split_pairs(pairs) do
{required, optional} =
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
{required, optional}
end
def error(type, reason, context), do: {:error, {type, reason, context}}
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Gets a variable.
"""
def get_var!(var, context) do
Map.fetch!(context.vars, var_name(var))
end
@doc """
Adds a variable to the typing context and returns its type variable.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
var_name = var_name(var)
case context.vars do
%{^var_name => type} ->
{type, context}
%{} ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name, type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Adds an internal variable to the typing context and returns its type variable.
An internal variable is used to help unify complex expressions,
it does not belong to a specific AST expression.
"""
def add_var(context) do
type = {:var, context.counter}
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
@doc """
Maybe resolves a variable.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> {:var, var}
%{^var => type} -> resolve_var(type, context)
end
end
def resolve_var(other, _context), do: other
# Check unify stack to see if variable was already expanded
defp variable_expanded?(var, stack, context) do
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
end
defp variable_same?(left, right, context) do
case context.types do
%{^left => {:var, new_left}} ->
variable_same?(new_left, right, context)
%{^right => {:var, new_right}} ->
variable_same?(left, new_right, context)
%{} ->
false
end
end
defp push_unify_stack(var, stack) do
%{stack | unify_stack: [var | stack.unify_stack]}
end
@doc """
Restores the variable information from the old context into new context.
"""
def restore_var!(var, new_context, old_context) do
%{^var => type} = old_context.types
%{^var => trace} = old_context.traces
types = Map.put(new_context.types, var, type)
traces = Map.put(new_context.traces, var, trace)
%{new_context | types: types, traces: traces}
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var!(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
line = get_meta(last_expr)[:line]
trace = {type, last_expr, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case context.types do
%{^var => :unbound} ->
false
%{^var => type} ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:union, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {_kind, key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?({:fun, clauses}, parents, context) do
Enum.any?(clauses, fn {args, return} ->
Enum.any?([return | args], &recursive_type?(&1, [clauses | parents], context))
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Collects all type vars recursively.
"""
def collect_var_indexes(type, context, acc \\ %{}) do
{_type, indexes} =
walk(type, acc, fn
{:var, var}, acc ->
case acc do
%{^var => _} ->
{{:var, var}, acc}
%{} ->
case context.types do
%{^var => :unbound} ->
{{:var, var}, Map.put(acc, var, true)}
%{^var => type} ->
{{:var, var}, collect_var_indexes(type, context, Map.put(acc, var, true))}
end
end
other, acc ->
{other, acc}
end)
indexes
end
@doc """
Checks if the type has a type var.
"""
def has_unbound_var?(type, context) do
walk(type, :ok, fn
{:var, var}, acc ->
case context.types do
%{^var => :unbound} ->
throw(:has_unbound_var?)
%{^var => type} ->
has_unbound_var?(type, context)
{{:var, var}, acc}
end
other, acc ->
{other, acc}
end)
false
catch
:throw, :has_unbound_var? -> true
end
@doc """
Returns `true` if it is a singleton type.
Only atoms are singleton types. Unbound vars are not
considered singleton types.
"""
def singleton?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> singleton?(type, context)
end
end
def singleton?({:atom, _}, _context), do: true
def singleton?(_type, _context), do: false
@doc """
Checks if the first argument is a subtype of the second argument.
This function assumes that:
* unbound variables are not subtype of anything
* dynamic is not considered a subtype of all other types but the top type.
This allows this function can be used for ordering, in other cases, you
may need to check for both sides
"""
def subtype?(type, type, _context), do: true
def subtype?({:var, var}, other, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(type, other, context)
end
end
def subtype?(other, {:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(other, type, context)
end
end
def subtype?(_, :dynamic, _context), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
# Composite
def subtype?({:tuple, _, _}, :tuple, _context), do: true
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
left_types
|> Enum.zip(right_types)
|> Enum.all?(fn {left, right} -> subtype?(left, right, context) end)
end
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
Enum.all?(left_pairs, fn
{:required, left_key, left_value} ->
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
end)
{:optional, _, _} ->
true
end)
end
def subtype?({:list, left}, {:list, right}, context) do
subtype?(left, right, context)
end
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?({:union, left_types}, right, context) do
Enum.all?(left_types, &subtype?(&1, right, context))
end
def subtype?(_left, _right, _context), do: false
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
def to_union([type], _context), do: type
def to_union(types, context) when types != [] do
case unique_super_types(unnest_unions(types), context) do
[type] -> type
types -> {:union, types}
end
end
defp unnest_unions(types) do
Enum.flat_map(types, fn
{:union, types} -> unnest_unions(types)
type -> [type]
end)
end
# Filter subtypes
#
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
#
# Does not merge `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
## Type lifting
@doc """
Lifts type variables to their inferred types from the context.
"""
def lift_types(types, %{lifted_types: _} = context) do
Enum.map_reduce(types, context, &lift_type/2)
end
def lift_types(types, context) do
context = %{
types: context.types,
lifted_types: %{},
lifted_counter: 0
}
Enum.map_reduce(types, context, &lift_type/2)
end
# Lift type variable to its inferred (hopefully concrete) types from the context
defp lift_type({:var, var}, context) do
case context.lifted_types do
%{^var => lifted_var} ->
{{:var, lifted_var}, context}
%{} ->
case context.types do
%{^var => :unbound} ->
new_lifted_var(var, context)
%{^var => type} ->
if recursive_type?(type, [], context) do
new_lifted_var(var, context)
else
# Remove visited types to avoid infinite loops
# then restore after we are done recursing on vars
types = context.types
context = put_in(context.types[var], :unbound)
{type, context} = lift_type(type, context)
{type, %{context | types: types}}
end
%{} ->
new_lifted_var(var, context)
end
end
end
defp lift_type({:union, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:union, types}, context}
end
defp lift_type({:tuple, n, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:tuple, n, types}, context}
end
defp lift_type({:map, pairs}, context) do
{pairs, context} =
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
{key, context} = lift_type(key, context)
{value, context} = lift_type(value, context)
{{kind, key, value}, context}
end)
{{:map, pairs}, context}
end
defp lift_type({:list, type}, context) do
{type, context} = lift_type(type, context)
{{:list, type}, context}
end
defp lift_type({:fun, clauses}, context) do
clauses =
Enum.map_reduce(clauses, context, fn {args, return}, context ->
{[return | args], context} = Enum.map_reduce([return | args], context, &lift_type/2)
{{args, return}, context}
end)
{{:fun, clauses}, context}
end
defp lift_type(other, context) do
{other, context}
end
defp new_lifted_var(original_var, context) do
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
counter = context.lifted_counter + 1
type = {:var, context.lifted_counter}
context = %{context | lifted_types: types, lifted_counter: counter}
{type, context}
end
# TODO: Figure out function expansion
@doc """
Expand unions so that all unions are at the top level.
{integer() | float()} => {integer()} | {float()}
"""
def flatten_union({:union, types}, context) do
Enum.flat_map(types, &flatten_union(&1, context))
end
def flatten_union(type, context) do
List.wrap(do_flatten_union(type, context))
end
def do_flatten_union({:tuple, num, types}, context) do
flatten_union_tuple(types, num, context, [])
end
def do_flatten_union({:list, type}, context) do
case do_flatten_union(type, context) do
{:union, union_types} -> Enum.map(union_types, &{:list, &1})
_type -> [{:list, type}]
end
end
def do_flatten_union({:map, pairs}, context) do
flatten_union_map(pairs, context, [])
end
def do_flatten_union({:var, var}, context) do
if looping_var?(var, context, []) do
{:var, var}
else
case context.types do
%{^var => :unbound} -> {:var, var}
%{^var => {:union, types}} -> Enum.map(types, &do_flatten_union(&1, context))
%{^var => type} -> do_flatten_union(type, context)
end
end
end
def do_flatten_union(type, _context) do
type
end
defp flatten_union_tuple([type | types], num, context, acc) do
case do_flatten_union(type, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_tuple(types, num, context, [&1 | acc]))
type ->
flatten_union_tuple(types, num, context, [type | acc])
end
end
defp flatten_union_tuple([], num, _context, acc) do
[{:tuple, num, Enum.reverse(acc)}]
end
defp flatten_union_map([{kind, key, value} | pairs], context, acc) do
case do_flatten_union(key, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map_value(kind, &1, value, pairs, context, acc))
type ->
flatten_union_map_value(kind, type, value, pairs, context, acc)
end
end
defp flatten_union_map([], _context, acc) do
[{:map, Enum.reverse(acc)}]
end
defp flatten_union_map_value(kind, key, value, pairs, context, acc) do
case do_flatten_union(value, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map(pairs, context, [{kind, key, &1} | acc]))
value ->
flatten_union_map(pairs, context, [{kind, key, value} | acc])
end
end
defp looping_var?(var, context, parents) do
case context.types do
%{^var => :unbound} ->
false
%{^var => {:var, type}} ->
if var in parents do
true
else
looping_var?(type, context, [var | parents])
end
%{^var => _type} ->
false
end
end
@doc """
Formats types.
The second argument says when complex types such as maps and
structs should be simplified and not shown.
"""
def format_type({:map, pairs}, true) do
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
{:required, {:atom, :__struct__}, {:atom, struct}} ->
["%", inspect(struct), "{}"]
_ ->
"map()"
end
end
def format_type({:union, types}, simplify?) do
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(" | ")
end
def format_type({:tuple, _, types}, simplify?) do
format =
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(", ")
["{", format, "}"]
end
def format_type({:list, type}, simplify?) do
["[", format_type(type, simplify?), "]"]
end
def format_type({:map, pairs}, false) do
case List.keytake(pairs, {:atom, :__struct__}, 1) do
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
["%", inspect(struct), "{", format_map_pairs(pairs), "}"]
_ ->
["%{", format_map_pairs(pairs), "}"]
end
end
def format_type({:atom, literal}, _simplify?) do
inspect(literal)
end
def format_type({:var, index}, _simplify?) do
["var", Integer.to_string(index + 1)]
end
def format_type({:fun, clauses}, simplify?) do
format =
Enum.map(clauses, fn {params, return} ->
params = Enum.intersperse(Enum.map(params, &format_type(&1, simplify?)), ", ")
params = if params == [], do: params, else: [params, " "]
return = format_type(return, simplify?)
[params, "-> ", return]
end)
["(", Enum.intersperse(format, "; "), ")"]
end
def format_type(atom, _simplify?) when is_atom(atom) do
[Atom.to_string(atom), "()"]
end
defp format_map_pairs(pairs) do
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
(atoms ++ required ++ optional)
|> Enum.map(fn
{:required, {:atom, atom}, right} ->
[Atom.to_string(atom), ": ", format_type(right, false)]
{:required, left, right} ->
[format_type(left, false), " => ", format_type(right, false)]
{:optional, left, right} ->
["optional(", format_type(left, false), ") => ", format_type(right, false)]
end)
|> Enum.intersperse(", ")
end
@doc """
Performs a depth-first, pre-order traversal of the type tree using an accumulator.
"""
def walk({:map, pairs}, acc, fun) do
{pairs, acc} =
Enum.map_reduce(pairs, acc, fn {kind, key, value}, acc ->
{key, acc} = walk(key, acc, fun)
{value, acc} = walk(value, acc, fun)
{{kind, key, value}, acc}
end)
fun.({:map, pairs}, acc)
end
def walk({:union, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:union, types}, acc)
end
def walk({:tuple, num, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:tuple, num, types}, acc)
end
def walk({:list, type}, acc, fun) do
{type, acc} = walk(type, acc, fun)
fun.({:list, type}, acc)
end
def walk({:fun, clauses}, acc, fun) do
{clauses, acc} =
Enum.map_reduce(clauses, acc, fn {params, return}, acc ->
{params, acc} = Enum.map_reduce(params, acc, &walk(&1, &2, fun))
{return, acc} = walk(return, acc, fun)
{{params, return}, acc}
end)
fun.({:fun, clauses}, acc)
end
def walk(type, acc, fun) do
fun.(type, acc)
end
end
+1 -1
View File
@@ -219,7 +219,7 @@ defmodule Node do
If `node` does not exist, a useless PID is returned.
For the list of available options, see `:erlang.spawn_opt/5`.
For the list of available options, see `:erlang.spawn/4`.
Inlined by the compiler.
"""
+25 -34
View File
@@ -152,47 +152,44 @@ defmodule OptionParser do
### Parsing unknown switches
When the `:switches` option is given, `OptionParser` will attempt to parse
unknown switches.
Switches without an argument will be set to `true`:
unknown switches:
iex> OptionParser.parse(["--debug"], switches: [key: :string])
{[debug: true], [], []}
Even though we haven't specified `--debug` in the list of switches, it is part
of the returned options. The same happens for switches followed by another switch:
iex> OptionParser.parse(["--debug", "--ok"], switches: [])
{[debug: true, ok: true], [], []}
Switches followed by a value will be assigned the value, as a string:
of the returned options. This would also work:
iex> OptionParser.parse(["--debug", "value"], switches: [key: :string])
{[debug: "value"], [], []}
Since we cannot assert the type of the switch value, it is preferred to use the
`:strict` option that accepts only known switches and always verify their types.
Switches followed by a value will be assigned the value, as a string. Switches
without an argument will be set automatically to `true`. Since we cannot assert
the type of the switch value, it is preferred to use the `:strict` option that
accepts only known switches and always verify their types.
If you do want to parse unknown switches, remember that Elixir converts switches
to atoms. Since atoms are not garbage-collected, to avoid creating new ones,
OptionParser by default only parses switches that translate to existing atoms.
The code below discards the `--option-parser-example` switch because the
`:option_parser_example` atom is never used anywhere:
to atoms. Since atoms are not garbage-collected, OptionParser will only parse
switches that translate to atoms used by the runtime to avoid leaking atoms.
For instance, the code below will discard the `--option-parser-example` switch
because the `:option_parser_example` atom is never used anywhere:
iex> OptionParser.parse(["--option-parser-example"], switches: [])
{[], [], []}
OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# The :option_parser_example atom is not used anywhere below
If a switch corresponds to an existing Elixir atom, whether from your
code, a dependency or from Elixir itself, it will be accepted. However,
it is best to not rely on external code, and always define the atoms
you want to parse in the same module that calls `OptionParser` itself,
as direct arguments to the `:switches` or `:strict` options.
However, the code below would work as long as `:option_parser_example` atom is
used at some point later (or earlier) **in the same module**. For example:
If you would like to parse all switches, regardless if they exist or not,
you can force creation of atoms by passing `allow_nonexistent_atoms: true`
as option. Use this option with care. It is only useful when you are building
command-line applications that receive dynamically-named arguments and must
be avoided in long-running systems.
{opts, _, _} = OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# ... then somewhere in the same module you access it ...
opts[:option_parser_example]
In other words, Elixir will only parse options that are used by the runtime,
ignoring all others. If you would like to parse all switches, regardless if
they exist or not, you can force creation of atoms by passing
`allow_nonexistent_atoms: true` as option. Use this option with care. It is
only useful when you are building command-line applications that receive
dynamically-named arguments and must be avoided in long-running systems.
## Aliases
@@ -865,13 +862,7 @@ defmodule OptionParser do
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
end
"#{option} : Missing argument of type #{type}"
else
msg = "#{option} : Unknown option"
+49 -111
View File
@@ -96,7 +96,7 @@ defmodule PartitionSupervisor do
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
You can start the dynamic supervisors under a `PartitionSupervisor`:
You can do start the dynamic supervisors under a `PartitionSupervisor`:
children = [
{PartitionSupervisor,
@@ -151,7 +151,7 @@ defmodule PartitionSupervisor do
@doc false
def child_spec(opts) when is_list(opts) do
id =
case Keyword.get(opts, :name, PartitionSupervisor) do
case Keyword.get(opts, :name, DynamicSupervisor) do
name when is_atom(name) -> name
{:via, _module, name} -> name
end
@@ -228,26 +228,26 @@ defmodule PartitionSupervisor do
def start_link(opts) when is_list(opts) do
name = opts[:name]
if !name do
unless name do
raise ArgumentError, "the :name option must be given to PartitionSupervisor"
end
{child_spec, opts} = Keyword.pop(opts, :child_spec)
if !child_spec do
unless child_spec do
raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor"
end
{partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online())
if not (is_integer(partitions) and partitions >= 1) do
unless is_integer(partitions) and partitions >= 1 do
raise ArgumentError,
"the :partitions option must be a positive integer, got: #{inspect(partitions)}"
end
{with_arguments, opts} = Keyword.pop(opts, :with_arguments, fn args, _partition -> args end)
if not is_function(with_arguments, 2) do
unless is_function(with_arguments, 2) do
raise ArgumentError,
"the :with_arguments option must be a function that receives two arguments, " <>
"the current call arguments and the partition, got: #{inspect(with_arguments)}"
@@ -260,17 +260,17 @@ defmodule PartitionSupervisor do
for partition <- 0..(partitions - 1) do
args = with_arguments.(args, partition)
if not is_list(args) do
unless is_list(args) do
raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}"
end
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
start = {__MODULE__, :start_child, [mod, fun, args, name, partition]}
Map.merge(map, %{id: partition, start: start, modules: modules})
end
auto_shutdown = Keyword.get(opts, :auto_shutdown, :never)
if auto_shutdown != :never do
unless auto_shutdown == :never do
raise ArgumentError,
"the :auto_shutdown option must be :never, got: #{inspect(auto_shutdown)}"
end
@@ -282,14 +282,14 @@ defmodule PartitionSupervisor do
end
@doc false
def start_child(mod, fun, args, partition) do
def start_child(mod, fun, args, name, partition) do
case apply(mod, fun, args) do
{:ok, pid} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid}
{:ok, pid, info} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid, info}
other ->
@@ -297,97 +297,33 @@ defmodule PartitionSupervisor do
end
end
defp register_child(partition, pid) do
:ets.insert(Process.get(:ets_table), {partition, pid})
defp register_child(name, partition, pid) when is_atom(name) do
:ets.insert(name, {partition, pid})
end
defp register_child({:via, _, _}, partition, pid) do
Registry.register(@registry, {self(), partition}, pid)
end
@impl true
def init({name, partitions, children, init_opts}) do
table = init_table(name)
:ets.insert(table, {:partitions, partitions, partitions})
Process.put(:ets_table, table)
init_partitions(name, partitions)
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
end
defp init_table(name) when is_atom(name) do
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
defp init_partitions(name, partitions) when is_atom(name) do
:ets.new(name, [:set, :named_table, :protected, read_concurrency: true])
:ets.insert(name, {:partitions, partitions})
end
defp init_table({:via, _, _}) do
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
ensure_registry()
Registry.register(@registry, self(), table)
table
end
defp init_partitions({:via, _, _}, partitions) do
child_spec = {Registry, keys: :unique, name: @registry}
defp ensure_registry do
if Process.whereis(@registry) == nil do
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
unless Process.whereis(@registry) do
Supervisor.start_child(:elixir_sup, child_spec)
end
end
@doc """
Resizes the number of partitions in the PartitionSupervisor.
This is done by starting or stopping a given number of
partitions in the supervisor. All of the child specifications
are kept in the `PartitionSupervisor` itself.
The final number of partitions cannot be less than zero and
cannot be more than the number of partitions the supervisor
started with.
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
table = table(name)
ensure_registry()
Registry.lock(@registry, supervisor, fn ->
case :ets.lookup(table, :partitions) do
[{:partitions, _current, max}] when partitions not in 0..max//1 ->
raise ArgumentError,
"the number of partitions to resize to must be a number between 0 and #{max}, got: #{partitions}"
[{:partitions, current, max}] when partitions > current ->
for id <- current..(partitions - 1) do
case Supervisor.restart_child(supervisor, id) do
{:ok, _} ->
:ok
{:ok, _, _} ->
:ok
{:error, reason} ->
raise "cannot restart partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
:ets.insert(table, {:partitions, partitions, max})
current
[{:partitions, current, max}] when partitions < current ->
:ets.insert(table, {:partitions, partitions, max})
for id <- partitions..(current - 1) do
case Supervisor.terminate_child(supervisor, id) do
:ok ->
:ok
{:error, reason} ->
raise "cannot terminate partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
current
[{:partitions, current, _max}] ->
current
end
end)
Registry.register(@registry, self(), partitions)
end
@doc """
@@ -396,27 +332,24 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
name |> table() |> partitions(name)
{_name, partitions} = name_partitions(name)
partitions
end
defp partitions(table, name) do
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp name_partitions(name) when is_atom(name) do
try do
:ets.lookup_element(table, :partitions, 2)
{name, :ets.lookup_element(name, :partitions, 2)}
rescue
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
end
defp table(name) when is_atom(name) do
name
end
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp table(name) when is_tuple(name) do
defp name_partitions(name) when is_tuple(name) do
with pid when is_pid(pid) <- GenServer.whereis(name),
[{_, table}] <- Registry.lookup(@registry, pid) do
table
[name_partitions] <- Registry.lookup(@registry, pid) do
name_partitions
else
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
@@ -441,7 +374,7 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec which_children(name()) :: [
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
Supervisor.which_children(name)
@@ -495,17 +428,22 @@ defmodule PartitionSupervisor do
@doc false
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
table = table(name)
partitions = partitions(table, name)
if partitions == 0 do
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
end
{name, partitions} = name_partitions(name)
partition =
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
:ets.lookup_element(table, partition, 2)
whereis_name(name, partition)
end
defp whereis_name(name, partition) when is_atom(name) do
:ets.lookup_element(name, partition, 2)
end
defp whereis_name(name, partition) when is_pid(name) do
@registry
|> Registry.values({name, partition}, name)
|> List.first(:undefined)
end
@doc false
+4 -13
View File
@@ -64,9 +64,6 @@ defmodule Path do
iex> Path.absname("../x", "bar")
"bar/../x"
iex> Path.absname("foo", fn -> "lazy" end)
"lazy/foo"
"""
@spec absname(t, t | (-> t)) :: binary
def absname(path, relative_to) do
@@ -172,17 +169,11 @@ defmodule Path do
Converts the path to an absolute one, expanding
any `.` and `..` components and a leading `~`.
If a relative path is provided it is expanded relatively to
the current working directory.
## Examples
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
Path.expand("foo/bar/../baz")
#=> "$PWD/foo/baz"
"""
@spec expand(t) :: binary
def expand(path) do
@@ -267,7 +258,7 @@ defmodule Path do
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
"""
# Note this function does not expand paths because the behavior
# Note this function does not expand paths because the behaviour
# is ambiguous. If we expand it before converting to relative, then
# "/usr/../../foo" means "/foo". If we expand it after, it means "../foo".
# We could expand only relative paths but it is best to say it never
@@ -884,7 +875,7 @@ defmodule Path do
* A `..` component would make it so that the path would traverse up above
the root of `relative_to`.
* A symbolic link in the path points to something above the root of `relative_to`.
* A symbolic link in the path points to something above the root of `cwd`.
## Examples
@@ -906,10 +897,10 @@ defmodule Path do
"""
@doc since: "1.14.0"
@spec safe_relative(t, t) :: {:ok, binary} | :error
def safe_relative(path, relative_to \\ File.cwd!()) do
def safe_relative(path, cwd \\ File.cwd!()) do
path = IO.chardata_to_string(path)
case :filelib.safe_relative_path(path, relative_to) do
case :filelib.safe_relative_path(path, cwd) do
:unsafe -> :error
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
end
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Port do
iex> send(port, {self(), :close})
:ok
iex> flush()
{#Port<0.1444>, :closed}
{#Port<0.1464>, :closed}
:ok
In the example above, we have created a new port that executes the
+6 -67
View File
@@ -88,9 +88,6 @@ defmodule Process do
`false`.
`pid` must refer to a process running on the local node or `ArgumentError` is raised.
To check whether a process on any node is alive you can use the [`:erpc`](`:erpc`) module.
:erpc.call(node(pid), Process, :alive?, [pid])
Inlined by the compiler.
"""
@@ -198,7 +195,7 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
The following behavior applies if `reason` is any term except `:normal`
The following behaviour applies if `reason` is any term except `:normal`
or `:kill`:
1. If `pid` is not trapping exits, `pid` will exit with the given
@@ -219,16 +216,6 @@ defmodule Process do
Inlined by the compiler.
> #### Differences to `Kernel.exit/1` {: .info }
>
> The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The
> `Kernel:exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
> when the signal is different than `:kill`.
## Examples
Process.exit(pid, :kill)
@@ -246,12 +233,6 @@ defmodule Process do
the current process will sleep forever, and not
consume or reply to messages.
> #### Sleeping limit {: .info }
>
> Before Elixir v1.18, `sleep/1` did not accept integer timeout values greater
> than `16#ffffffff`, that is, `2^32-1`. Since Elixir v1.18, arbitrarily-high integer
> values are accepted.
**Use this function with extreme care**. For almost all situations
where you would use `sleep/1` in Elixir, there is likely a
more correct, faster and precise way of achieving the same with
@@ -318,15 +299,7 @@ defmodule Process do
end
"""
# Max value for a receive's after clause
@max_receive_after 0xFFFFFFFF
@spec sleep(timeout) :: :ok
def sleep(timeout) when is_integer(timeout) and timeout > @max_receive_after do
receive after: (@max_receive_after -> sleep(timeout - @max_receive_after))
end
def sleep(timeout)
when is_integer(timeout) and timeout >= 0
when timeout == :infinity do
@@ -579,15 +552,10 @@ defmodule Process do
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> :pong
ref_and_alias = Process.monitor(pid, alias: :reply_demonitor)
#=> #Reference<0.906660723.3006791681.40191>
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc}
"""
@@ -656,7 +624,7 @@ defmodule Process do
exits with a reason other than `:normal` (which is also the exit reason used
when a process finishes its job) and `pid1` is not trapping exits (see
`flag/2`), then `pid1` will exit with the same reason as `pid2` and in turn
emit an exit signal to all its other linked processes. The behavior when
emit an exit signal to all its other linked processes. The behaviour when
`pid1` is trapping exits is described in `exit/2`.
See `:erlang.link/1` for more information.
@@ -871,17 +839,13 @@ defmodule Process do
nilify(:erlang.process_info(pid))
end
@type process_info_item :: atom | {:dictionary, term}
@type process_info_result_item :: {process_info_item, term}
@doc """
Returns information about the process identified by `pid`,
or returns `nil` if the process is not alive.
See `:erlang.process_info/2` for more information.
"""
@spec info(pid, process_info_item) :: process_info_result_item | nil
@spec info(pid, [process_info_item]) :: [process_info_result_item] | nil
@spec info(pid, atom | [atom]) :: {atom, term} | [{atom, term}] | nil
def info(pid, spec)
def info(pid, :registered_name) do
@@ -892,7 +856,7 @@ defmodule Process do
end
end
def info(pid, spec) do
def info(pid, spec) when is_atom(spec) or is_list(spec) do
nilify(:erlang.process_info(pid, spec))
end
@@ -976,31 +940,6 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
## Examples
Process.set_label(:worker)
#=> :ok
Process.set_label({:any, "term"})
#=> :ok
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
defp nilify(other), do: other
+132 -139
View File
@@ -6,7 +6,7 @@ defmodule Protocol do
implementations. A protocol is defined with `Kernel.defprotocol/2`
and its implementations with `Kernel.defimpl/3`.
## Example
## A real case
In Elixir, we have two nouns for checking how many items there
are in a data structure: `length` and `size`. `length` means the
@@ -102,7 +102,8 @@ defmodule Protocol do
invoking the protocol will raise unless it is configured to
fall back to `Any`. Conveniences for building implementations
on top of existing ones are also available, look at `defstruct/1`
for more information about deriving protocols.
for more information about deriving
protocols.
## Fallback to `Any`
@@ -125,8 +126,8 @@ defmodule Protocol do
Although the implementation above is arguably not a reasonable
one. For example, it makes no sense to say a PID or an integer
have a size of `0`. That's one of the reasons why `@fallback_to_any`
is an opt-in behavior. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behavior.
is an opt-in behaviour. For the majority of protocols, raising
an error when a protocol is not implemented is the proper behaviour.
## Multiple implementations
@@ -164,16 +165,56 @@ defmodule Protocol do
The `@spec` above expresses that all types allowed to implement the
given protocol are valid argument types for the given function.
## Configuration
## Reflection
The following module attributes are available to configure a protocol:
Any protocol module contains three extra functions:
* `@fallback_to_any` - when true, enables protocol dispatch to
fallback to any
* `__protocol__/1` - returns the protocol information. The function takes
one of the following atoms:
* `@undefined_impl_description` - a string with additional description
to be used on `Protocol.UndefinedError` when looking up the implementation
fails. This option is only applied if `@fallback_to_any` is not set to true
* `:consolidated?` - returns whether the protocol is consolidated
* `:functions` - returns a keyword list of protocol functions and their arities
* `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
implementing the protocol, otherwise `:not_consolidated`
* `:module` - the protocol module atom name
* `impl_for/1` - returns the module that implements the protocol for the given argument,
`nil` otherwise
* `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
not found
For example, for the `Enumerable` protocol we have:
iex> Enumerable.__protocol__(:functions)
[count: 1, member?: 2, reduce: 3, slice: 1]
iex> Enumerable.impl_for([])
Enumerable.List
iex> Enumerable.impl_for(42)
nil
In addition, every protocol implementation module contains the `__impl__/1`
function. The function takes one of the following atoms:
* `:for` - returns the module responsible for the data structure of the
protocol implementation
* `:protocol` - returns the protocol module for which this implementation
is provided
For example, the module implementing the `Enumerable` protocol for lists is
`Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:
iex(1)> Enumerable.List.__impl__(:for)
List
iex(2)> Enumerable.List.__impl__(:protocol)
Enumerable
## Consolidation
@@ -181,8 +222,7 @@ defmodule Protocol do
are known up-front, typically after all Elixir code in a project is compiled,
Elixir provides a feature called *protocol consolidation*. Consolidation directly
links protocols to their implementations in a way that invoking a function from a
consolidated protocol is equivalent to invoking two remote functions - one to
identify the correct implementation, and another to call the implementation.
consolidated protocol is equivalent to invoking two remote functions.
Protocol consolidation is applied by default to all Mix projects during compilation.
This may be an issue during test. For instance, if you want to implement a protocol
@@ -228,41 +268,6 @@ defmodule Protocol do
globally set.
"""
@doc """
A function available in all protocol definitions that returns protocol metadata.
"""
@callback __protocol__(:consolidated?) :: boolean()
@callback __protocol__(:functions) :: [{atom(), arity()}]
@callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated
@callback __protocol__(:module) :: module()
@doc """
A function available in all protocol definitions that returns the implementation
for the given `term` or nil.
If `@fallback_to_any` is true, `nil` is never returned.
"""
@callback impl_for(term) :: module() | nil
@doc """
A function available in all protocol definitions that returns the implementation
for the given `term` or raises.
If `@fallback_to_any` is true, it never raises.
"""
@callback impl_for!(term) :: module()
@doc """
An optional callback to be implemented by protocol authors for custom deriving.
It must return a quoted expression that implements the protocol for the given module.
See `Protocol.derive/3` for an example.
"""
@macrocallback __deriving__(module(), term()) :: Macro.t()
@optional_callbacks __deriving__: 2
@doc false
defmacro def(signature)
@@ -347,7 +352,7 @@ defmodule Protocol do
end
defp assert_impl!(protocol, base, extra) do
impl = Protocol.__concat__(protocol, base)
impl = Module.concat(protocol, base)
try do
Code.ensure_compiled!(impl)
@@ -375,50 +380,52 @@ defmodule Protocol do
@doc """
Derives the `protocol` for `module` with the given options.
Every time you derive a protocol, Elixir will verify if the protocol
has implemented the `c:Protocol.__deriving__/2` callback. If so,
the callback will be invoked and it should define the implementation
module. Otherwise an implementation that simply points to the `Any`
implementation is automatically derived.
If your implementation passes options or if you are generating
custom code based on the struct, you will also need to implement
a macro defined as `__deriving__(module, struct, options)`
to get the options that were passed.
## Examples
defprotocol Derivable do
@impl true
defmacro __deriving__(module, options) do
# If you need to load struct metadata, you may call:
# struct_info = Macro.struct_info!(module, __CALLER__)
def ok(arg)
end
defimpl Derivable, for: Any do
defmacro __deriving__(module, struct, options) do
quote do
defimpl Derivable, for: unquote(module) do
def ok(arg) do
{:ok, arg, unquote(options)}
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
end
end
end
end
def ok(arg)
def ok(arg) do
{:ok, arg}
end
end
Once the protocol is defined, there are two ways it can be
derived. The first is by using the `@derive` module attribute
by the time you define the struct:
defmodule ImplStruct do
@derive [Derivable]
defstruct a: 0, b: 0
end
Derivable.ok(%ImplStruct{})
#=> {:ok, %ImplStruct{a: 0, b: 0}, []}
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
If the struct has already been defined, you can call this macro:
Explicit derivations can now be called via `__deriving__/3`:
# Explicitly derived via `__deriving__/3`
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
# Explicitly derived by API via `__deriving__/3`
require Protocol
Protocol.derive(Derivable, ImplStruct, :oops)
Derivable.ok(%ImplStruct{a: 1, b: 1})
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
"""
defmacro derive(protocol, module, options \\ []) do
@@ -490,8 +497,7 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
path = to_charlist(path),
file <- list_dir(path),
mod = extract_from_file(path, file, prefix, callback),
do: mod
@@ -678,7 +684,7 @@ defmodule Protocol do
end
defp load_impl(protocol, for) do
Protocol.__concat__(protocol, for)
Module.concat(protocol, for)
end
# Finally compile the module and emit its bytecode.
@@ -693,14 +699,25 @@ defmodule Protocol do
def __protocol__(name, do: block) do
quote do
defmodule unquote(name) do
@behaviour Protocol
@before_compile Protocol
# We don't allow function definition inside protocols
import Kernel,
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
except: [
def: 1,
def: 2,
defp: 1,
defp: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defmacro: 1,
defmacro: 2,
defmacrop: 1,
defmacrop: 2
]
# Import the new `def` that is used by protocols
# Import the new dsl that holds the new def
import Protocol, only: [def: 1]
# Compile with debug info for consolidation
@@ -719,17 +736,19 @@ defmodule Protocol do
end
end
defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do
defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos})
when kind in [:callback, :macrocallback] do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa(
{_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
) do
{kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
)
when kind in [:callback, :macrocallback] do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa({_kind, _clause, _pos}) do
defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do
[]
end
@@ -747,10 +766,13 @@ defmodule Protocol do
end
defp warn(message, env, line) when is_integer(line) do
IO.warn(message, %{env | line: line})
stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace()
IO.warn(message, stacktrace)
end
def __before_compile__(env) do
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
functions = Module.get_attribute(env.module, :__functions__)
if functions == [] do
@@ -761,11 +783,8 @@ defmodule Protocol do
)
end
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
# TODO: Convert the following warnings into errors in future Elixir versions
:lists.foreach(
:lists.map(
fn {name, arity} = fa ->
warn(
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -780,7 +799,7 @@ defmodule Protocol do
macrocallback_metas = callback_metas(env.module, :macrocallback)
macrocallbacks = :maps.keys(macrocallback_metas)
:lists.foreach(
:lists.map(
fn {name, arity} = fa ->
warn(
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -807,7 +826,7 @@ defmodule Protocol do
quote bind_quoted: [built_in: __built_in__()] do
any_impl_for =
if @fallback_to_any do
__MODULE__.Any
quote do: unquote(__MODULE__.Any)
else
nil
end
@@ -831,7 +850,7 @@ defmodule Protocol do
# Define the implementation for built-ins
:lists.foreach(
fn {guard, mod} ->
target = Protocol.__concat__(__MODULE__, mod)
target = Module.concat(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case Code.ensure_compiled(unquote(target)) do
@@ -853,9 +872,6 @@ defmodule Protocol do
unquote(any_impl_for)
end
undefined_impl_description =
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
@doc false
@spec impl_for!(term) :: atom
if any_impl_for do
@@ -864,27 +880,23 @@ defmodule Protocol do
end
else
Kernel.def impl_for!(data) do
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
target =
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
end
# Inline struct implementation for performance
@compile {:inline, struct_impl_for: 1}
if not Module.defines_type?(__MODULE__, {:t, 0}) do
unless Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@@ -898,9 +910,9 @@ defmodule Protocol do
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: [{atom(), arity()}]
@spec __protocol__(:consolidated?) :: boolean()
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))
@spec __protocol__(:consolidated?) :: boolean
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
Kernel.def(__protocol__(:module), do: __MODULE__)
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
Kernel.def(__protocol__(:consolidated?), do: false)
@@ -908,6 +920,12 @@ defmodule Protocol do
end
end
@doc false
def __functions_spec__([]), do: []
def __functions_spec__([head | tail]),
do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]
@doc false
def __impl__(protocol, opts, do_block, env) do
opts = Keyword.merge(opts, do_block)
@@ -948,13 +966,12 @@ defmodule Protocol do
quote do
protocol = unquote(protocol)
for = unquote(for)
name = Protocol.__concat__(protocol, for)
name = Module.concat(protocol, for)
Protocol.assert_protocol!(protocol)
Protocol.__ensure_defimpl__(protocol, for, __ENV__)
defmodule name do
@moduledoc false
@behaviour protocol
@protocol protocol
@for for
@@ -971,12 +988,14 @@ defmodule Protocol do
@doc false
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
struct = Macro.struct!(for, env)
foreach = fn
proto when is_atom(proto) ->
derive(proto, for, [], env)
derive(proto, for, struct, [], env)
{proto, opts} when is_atom(proto) ->
derive(proto, for, opts, env)
derive(proto, for, struct, opts, env)
end
:lists.foreach(foreach, :lists.flatten(derives))
@@ -984,30 +1003,21 @@ defmodule Protocol do
:ok
end
defp derive(protocol, for, opts, env) do
defp derive(protocol, for, struct, opts, env) do
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
assert_protocol!(protocol, extra)
{mod, args} =
if macro_exported?(protocol, :__deriving__, 2) do
{protocol, [for, opts]}
else
# TODO: Deprecate this on Elixir v1.22+
assert_impl!(protocol, Any, extra)
{Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
end
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, extra)
# Clean up variables from eval context
env = :elixir_env.reset_vars(env)
args = [for, struct, opts]
impl = Module.concat(protocol, Any)
:elixir_module.expand_callback(env.line, mod, :__deriving__, args, env, fn mod, fun, args ->
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
if function_exported?(mod, fun, length(args)) do
apply(mod, fun, args)
else
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, extra)
impl = Protocol.__concat__(protocol, Any)
funs =
for {fun, arity} <- protocol.__protocol__(:functions) do
args = Macro.generate_arguments(arity, nil)
@@ -1031,11 +1041,7 @@ defmodule Protocol do
def __impl__(:for), do: unquote(for)
end
Module.create(
Protocol.__concat__(protocol, for),
[quoted | funs],
Macro.Env.location(env)
)
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
end
end)
end
@@ -1074,17 +1080,4 @@ defmodule Protocol do
is_reference: Reference
]
end
@doc false
def __concat__(left, right) do
String.to_atom(
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
)
end
defp ensure_prefix("Elixir." <> _ = left), do: left
defp ensure_prefix(left), do: "Elixir." <> left
defp remove_prefix("Elixir." <> right), do: right
defp remove_prefix(right), do: right
end

Some files were not shown because too many files have changed in this diff Show More