Compare commits

...
582 Commits
Author SHA1 Message Date
José Valim 175c8243b2 Release v1.18.2 2025-01-22 17:38:30 +01:00
José Valim 2c1a836db3 Make --color/--no-color consistent with mix test 2025-01-22 17:33:00 +01:00
José Valim f29e18bca1 Do not precompile regexes on Erlang/OTP 28 2025-01-21 18:51:54 +01:00
José Valim 329442c481 Provide more AST around cursor fragments, closes #14118 2025-01-21 10:36:19 +01:00
José Valim 01a88e7137 Simplify token pruning, closes #14139 2025-01-20 18:57:09 +01:00
Jean Klingler 4def31f8ab Mention the new type system in typespecs doc (#14188) 2025-01-15 20:42:33 +09:00
José Valim 88c75bcbf5 Remove powershell scripts
It current leaves the shell broken after quitting Erlang.

Issues have been reported upstream, so we may be able
to bring it back in future Erlang/OTP versions.
2025-01-13 13:51:05 +01:00
José Valim 786f3ce797 Add --color for easy enabling and disabling of coloring (#14183) 2025-01-13 08:55:12 +01:00
José Valim 98aeee34b6 Simplify bat and ps1 files (#14180) 2025-01-13 08:55:08 +01:00
José Valim 6f7eaf1122 Keep line information in defstruct 2025-01-12 20:09:44 +01:00
Jonatan Kłosko 9593cef2f5 Remove duplicate call to mkdir in concurrency lock (#14175) 2025-01-11 10:53:36 +01:00
Claudio Ortolina c78f426bf5 Improve clarity of Path.safe_relative/2 argument names (#14167)
Switches from `cwd` to `relative_to`, which is more generic and more consistent with possible use cases of the function.
2025-01-10 20:03:32 +01:00
Steffen Deusch ff29628602 Track mix_pubsub and mix_lock folders per user (#14171) 2025-01-10 20:03:31 +01:00
Zach Allaun f5e42ba11a Update task-and-gen-tcp.md (#14166) 2025-01-09 18:10:35 +01:00
José Valim b0dae83353 Clarify the need for better tools in the TCP guide 2025-01-09 16:37:06 +01:00
Jean Klingler 40da7e8857 Fix ExUnit crash when diffing bitstring specifiers (#14161) 2025-01-09 18:27:58 +09:00
Jean Klingler ac910c1cc2 Prevent infinite loop in compiler for some invalid type specs (#14155) 2025-01-09 18:27:55 +09:00
José Valim 5c340e488e Migrate the git repo against the origin branch, closes #14163 2025-01-09 10:03:18 +01:00
Daniel Drexler defef37856 Improve warning for charlists (#14160)
The current warning for single-quoted character lists is confusing
because it appears to be a stylistic note instead of a deprecation
warning. This commit updates the language to make the reasoning clearer
and to more clearly describe what `mix format --migrate` will change
about the users' code.
2025-01-09 09:22:20 +01:00
Guillaume Milan 432d6fa1c2 Fix crashing on autocompleting structs with runtime values (#14150) 2025-01-07 11:40:01 +01:00
José Valim 24714c68a7 Improve error message for invalid default arguments 2025-01-02 18:36:27 +01:00
Jonatan Kłosko f088fc9cdb Print compilation lock waiting message to stderr (#14138) 2025-01-02 09:41:01 +01:00
José Valim dddb8f7b19 Bring kv-observer image back as it is used in guides 2024-12-31 08:53:56 +01:00
José Valim 2da3300bfc Do not wrap literals in variable when expanding in/2 2024-12-31 08:45:54 +01:00
Jonatan Kłosko c7a841baae Add an environment variable to optionally disable compilation locking (#14129) 2024-12-30 17:00:35 +01:00
José Valim f3dfb72c69 Properly handle optional keys in map intersection 2024-12-30 11:39:57 +01:00
José Valim 7e149619f5 Fix expression-tag pairing inside with type checking 2024-12-30 11:07:35 +01:00
José Valim 6dbb932bf3 Properly raise for invalid async value 2024-12-28 09:33:04 +01:00
José Valim 0cb79b0ba1 Add OTP 25 to sign entry 2024-12-28 09:18:59 +01:00
José Valim 265b9aaeb3 Update AST metadata documentation (#14120) 2024-12-27 11:37:54 +01:00
José Valim b9bd0e3dd9 Bring conditional violation reports back 2024-12-26 19:37:25 +01:00
José Valim 6a3301f237 Release v1.18.1 2024-12-24 10:17:42 +01:00
José Valim 4911916f62 Do not warn when comparing literals 2024-12-24 10:02:29 +01:00
José Valim 546a0db392 Use division by zero to show exception
Closes #14111.
2024-12-24 09:42:21 +01:00
José Valim 463f1aa593 Do not recompile if compilation fails due to --warnings-as-errors 2024-12-23 18:27:11 +01:00
José Valim a3a632efd1 Improve warning as errors deprecation notice 2024-12-23 18:26:37 +01:00
José Valim b7a5fd7b56 Improve container_cursor_to_quoted with trailing fragment, closes #14087 2024-12-23 12:24:03 +01:00
Jonatan Kłosko 04378bd9a6 Add build lock to deps.loadpaths (#14108) 2024-12-23 10:56:17 +01:00
José Valim fae36c5e49 Queue async groups until state is loaded 2024-12-23 10:56:15 +01:00
José Valim 712f24af0a Do not run async groups on load and support --repeat-until-failure (#14107) 2024-12-23 10:32:12 +01:00
José Valim 8c9f303e37 Avoid crash when typing violation is detected on dynamic dispatch
Closes #14105.
2024-12-22 17:56:59 +01:00
José Valim 1a2be16109 Track source for warnings, closes #14093 2024-12-22 11:08:13 +01:00
José Valim 4adaac702e Do not validate clauses of overridable functions, closes #14094 2024-12-22 09:55:23 +01:00
José Valim a1be4fbc86 Verify no warnings are emitted on generated clauses, see #14094 2024-12-22 09:55:21 +01:00
Jonatan Männchen d4e6a558cb Fix Release Signing Again (#14099) (#14102)
This time, fix the Docs publishing.
2024-12-21 18:31:26 +01:00
Jonatan Männchen 4e3203f19a Fix Release Signing (#14099) (#14100) 2024-12-21 18:31:21 +01:00
José Valim 4a5fb1eb66 Use binwrite only on Windows, closes #14101 2024-12-21 18:25:45 +01:00
Jonatan Kłosko 0fe6f68c34 Fix mix escript.build when protocol consolidation is disabled (#14098) 2024-12-21 12:22:54 +01:00
Jean Klingler 5aa049ccb3 Remove no_parens when using capture with arity (#14090) 2024-12-20 22:28:19 +09:00
Jonatan Männchen e4c86d4b5f Hash release files after signing (#14085) 2024-12-20 11:29:29 +01:00
José Valim e5033c94ce Keep traces backwards compatible 2024-12-20 10:43:48 +01:00
José Valim 5d65d60721 Comment out canonical and update release notes 2024-12-19 20:34:17 +01:00
José Valim 3564c68cdc Release v1.18.0 2024-12-19 18:30:20 +01:00
José Valim 9df42f5a0a Update CHANGELOG 2024-12-19 17:26:59 +01:00
José Valim c6597bcb20 Collapse structs for better pretty printing in different scenarios 2024-12-19 15:13:18 +01:00
José Valim ca6edfd389 Include types in comparison warning 2024-12-19 15:13:14 +01:00
José Valim ed83c407a5 Simplify and optimize NaiveDateTime.utc_now 2024-12-19 15:13:12 +01:00
José Valim a426520b66 Improvements to descr pretty printing 2024-12-18 09:32:08 +01:00
Guillaume Duboc 1c0f585f40 Fuse maps and tuples for printing (#14079) 2024-12-18 09:32:06 +01:00
José Valim 7c86c4bdb3 Update CHANGELOG 2024-12-17 10:11:15 +01:00
José Valim 4931ab40a5 Do not warn for CaseTemplate options, closes #14077 2024-12-17 10:03:53 +01:00
José Valim c7f78e8015 Emit warnings for duplicate patterns instead of errors 2024-12-16 19:30:54 +01:00
José Valim 6a24bafbda Tag exceptions as dynamic, closes #14074 2024-12-16 16:13:04 +01:00
José Valim 8ddef06e81 Also encode Duration to JSON 2024-12-16 12:23:47 +01:00
José Valim 472c4e1a24 Avoid crashes on diagnostics with tabs, closes #14073 2024-12-16 12:09:18 +01:00
José Valim 305d8e603c Do not convert fun() into (... -> any()) and vice-versa in typespecs 2024-12-15 15:18:52 +01:00
Jonatan Kłosko 1c7fc86e9c Make :source_path in docs chunk a charlist (#14071) 2024-12-13 16:57:54 +01:00
José Valim 617dc72759 Fix Range.new deprecation assertion 2024-12-13 10:14:47 +01:00
José Valim 4329a3c2f2 Address bootstrap 2024-12-13 10:01:18 +01:00
José Valim 8e8c9a7334 Wrap Range.new in Function.identity to avoid tail call optimization 2024-12-13 09:55:39 +01:00
Michał Łępicki 36c06c3a56 Fix JSON.decode/3 spec (#14068)
The error clause was missing the :error tuple wrapping
2024-12-13 09:52:42 +01:00
Mathias Polligkeit 048cc2ddb1 Further improve warning message for range with negative step (#14070) 2024-12-13 09:52:42 +01:00
Wojtek Mach 048ae58d6d Do not set docs source_ref to -latest tags (#14065) 2024-12-12 11:51:41 +01:00
José Valim 85e00cddd5 Clarify warnings happen on compiled code 2024-12-12 09:52:30 +01:00
Jean Klingler 6c866e8ab2 Fix type warnings in pop_in/1 (#14064) 2024-12-12 17:47:23 +09:00
José Valim 55eca5dff5 Implement JSON.Encoder for Calendar types (#14061) 2024-12-12 00:38:44 +01:00
Panagiotis Nezis 070a6d51bf Improve warning in Range.new/2 with negative step (#14062) 2024-12-12 00:38:40 +01:00
José Valim 9c28fb7a39 Improve JSON and Float docs 2024-12-11 21:08:20 +01:00
Andrea Leopardi cda1a09c58 Improve error message on empty "mix do" (#14055) 2024-12-11 12:44:47 +01:00
José Valim 3ca0bddd9d Fix encoding specs 2024-12-11 12:16:33 +01:00
José Valim e07a91594b Update deprecations 2024-12-11 12:02:01 +01:00
José Valim dfc659104c Touch the compile path when writing .app file for the first time (#14053)
Closes #14049.
2024-12-11 10:01:00 +01:00
Shenghang Tsai a4fa71ab23 Update CHANGELOG.md (#14052) 2024-12-11 09:43:42 +01:00
EksperimentalandWojtek Mach 2273bafb47 Improve grammar in CHANGELOG.md (#14050)
Co-authored-by: Wojtek Mach <wojtekmach@users.noreply.github.com>
2024-12-11 08:41:11 +01:00
José Valim a78f1715dc Fix error message around single quote keywords 2024-12-10 19:30:39 +01:00
José Valim b709865976 Improvements to markdown rendering in CHANGELOG 2024-12-10 19:18:32 +01:00
José Valim da7e04a540 Improve docs around auto_reloading 2024-12-10 19:16:09 +01:00
José Valim 66c5908619 Release v1.18.0-rc.0 2024-12-10 18:32:26 +01:00
José Valim 31d8da6c77 Avoid mtime races on compile.app_test.exs 2024-12-10 15:41:16 +01:00
Daniel Gomez de Souza 3bdf5a912c Fix typo (#14046) 2024-12-10 15:34:32 +01:00
José Valim 0e9b5ec070 Branch out v1.18 2024-12-10 15:01:43 +01:00
José Valim 71441623cc Update CHANGELOG and TODOs 2024-12-10 14:58:44 +01:00
Wojtek Mach c22a222176 Update CHANGELOG.md (#14044) 2024-12-10 14:42:05 +01:00
José Valim aff4fdbb8e Update CHANGELOG 2024-12-10 12:49:30 +01:00
José Valim 793c114721 Add JSON encoding and decoding (#14021) 2024-12-10 12:14:30 +01:00
Alex Naser 480945a8db Load modules for mix recompile all-at-once (#14006) 2024-12-10 12:12:24 +01:00
dependabot[bot] 051bffa0ec Bump actions/attest-build-provenance from 1 to 2 (#14042)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 11:51:50 +01:00
José Valim db2a39afd2 Do not emit warnings on code generation for escript builds 2024-12-10 11:12:58 +01:00
Leif Metcalf 3f01b31ce1 Mention syntax for updating multiple map keys (#14041) 2024-12-09 12:51:52 +01:00
Łukasz Samson 9753a10158 Remove not needed macrocallback code (#14038)
This code added in 6951f0cd2a was made obsolete in aee7fbc0dc (v1.1). Correct macrocallback handling was added in 04f151b68b (v1.10) and since then macro caller arg is passed to typespecs as `term()` instead of `Macro.Env.t()`
2024-12-09 00:26:07 +01:00
José Valim 64e4b89053 Fix map ordering in earlier Elixir versions 2024-12-06 09:23:10 +01:00
José Valim beaca3b5ad Improve diff printing for when there are nested three-element tuples 2024-12-06 09:13:10 +01:00
peter madsen-mygdal 8994297ec6 Fix typo (#14033)
peformant -> performant
2024-12-05 19:02:43 +01:00
José Valim b75cccc8df Describe why Logger.flush is not advised in production 2024-12-05 13:10:58 +01:00
José Valim 4328b090ae Add Duration.to_string (#14028) 2024-12-04 16:11:10 +01:00
Ray Chang 41832ba397 Use forward slash delimiters in Regex.split doc (#14031) 2024-12-04 00:43:10 +01:00
Theodor Fiedler d49937c7bd Simplify docs for Duration.to_iso8601/1 (#14030) 2024-12-03 20:04:57 +01:00
José Valim 67573f1a6b Use binwrite on Windows, closes #14026 (#14027) 2024-12-03 18:36:23 +01:00
José Valim 768fa4963e Optimize inspection of binaries
We have used this code:

    data = Base.encode64(:crypto.strong_rand_bytes(2_000_000))

    Mix.Tasks.Profile.Tprof.profile(
      fn -> inspect(data, printable_limit: :infinity) end,
      type: :memory
    )

The first result reported this:

    Profile results of #PID<0.106.0>
    #                                    CALLS      %      WORDS PER CALL
    Total                             10666685 100.00 9268308211   868.90
    :code.ensure_loaded/1                    1   0.00          3     3.00
    Inspect.BitString.inspect/2              1   0.00          4     4.00
    Inspect.Algebra.group/2                  1   0.00          4     4.00
    Inspect.Algebra.format/2                 1   0.00          6     6.00
    Inspect.Algebra.format/3                 3   0.00          8     2.67
    :erlang.iolist_to_binary/1               2   0.00         11     5.50
    anonymous fn/2 in Kernel.struct/2        1   0.00         17    17.00
    Inspect.Opts.new/1                       1   0.00         17    17.00
    Code.Identifier.escape/5           2666669   0.06    5333344     2.00
    String.recur_printable?/2          2666669   0.29   26666778    10.00
    Code.Identifier.escape_map/1       2666668  28.72 2662035405   998.26
    Code.Identifier.escape_char/1      2666668  70.93 6574272614  2465.35

As you can see, most of the allocations were in escape_char,
which makes sense: it will be invoked a lot for raw binaries
and it allocated lists, instead of relying on binary optimizations.

We reimplemented Code.Identifier.escape/5 to rely on binary
optimizations which then yielded this:

    Profile results of #PID<0.112.0>
    #                                   CALLS      %    WORDS PER CALL
    Total                             5333350 100.00 26666864     5.00
    :code.ensure_loaded/1                   1   0.00        3     3.00
    Inspect.BitString.inspect/2             1   0.00        4     4.00
    Inspect.Algebra.group/2                 1   0.00        4     4.00
    Inspect.Algebra.format/2                1   0.00        6     6.00
    Code.Identifier.escape/4                1   0.00        8     8.00
    Code.Identifier.escape/5          2666669   0.00        8     0.00
    Inspect.Algebra.format/3                3   0.00        8     2.67
    :erlang.iolist_to_binary/1              2   0.00       11     5.50
    anonymous fn/2 in Kernel.struct/2       1   0.00       17    17.00
    Inspect.Opts.new/1                      1   0.00       17    17.00
    String.recur_printable?/2         2666669 100.00 26666778    10.00

Now the allocations are all on `recur_printable?/2`. By running this
command, we can find out why it is allocating so much:

    ERL_COMPILER_OPTIONS=bin_opt_info elixir lib/elixir/lib/string.ex

It happens that, due to a catch all, new binary contexts had to be
allocated. By addressing that, we now get:

    Profile results of #PID<0.119.0>
    #                                   CALLS      % WORDS PER CALL
    Total                             5333350 100.00    91     0.00
    :code.ensure_loaded/1                   1   3.30     3     3.00
    Inspect.BitString.inspect/2             1   4.40     4     4.00
    Inspect.Algebra.group/2                 1   4.40     4     4.00
    String.recur_printable?/2         2666669   5.49     5     0.00
    Inspect.Algebra.format/2                1   6.59     6     6.00
    Code.Identifier.escape/4                1   8.79     8     8.00
    Code.Identifier.escape/5          2666669   8.79     8     0.00
    Inspect.Algebra.format/3                3   8.79     8     2.67
    :erlang.iolist_to_binary/1              2  12.09    11     5.50
    anonymous fn/2 in Kernel.struct/2       1  18.68    17    17.00
    Inspect.Opts.new/1                      1  18.68    17    17.00

Which is several orders of magnitude fewer words. We basically
allocate the new escaped binary and a few extra bytes.

Closes #14029.
2024-12-03 18:34:59 +01:00
Eksperimental eadbd8bdf3 Fix typos (#14023)
The typos were detected by this app: https://pypi.org/project/typos/
running the following command

    typos --exclude "lib/elixir/unicode/" --exclude "man/" --exclude "typos"  --format brief
2024-12-01 16:59:24 +01:00
José Valim d97b29fa64 Improvements to comparison docs 2024-11-30 15:49:02 +01:00
José Valim b087a7501b Keep struct fields in calendar sigil for better pretty printing 2024-11-30 15:44:05 +01:00
José Valim 44107f1010 Precreate the compilation path to avoid mtime races 2024-11-29 13:42:13 +01:00
maintenance-beam-app 13d3cc0a3f Update Unicode to version 16.0.0 (#14024)
This is an automated commit created by the Maintenance project
https://github.com/eksperimental/maintenance

Before merging, please read the release notes by visiting
<http://www.unicode.org/versions/Unicode16.0.0/>
and assess if additional changes are necessary in the code base.
2024-11-29 10:01:27 +01:00
José Valim 521216f427 Do not assert time on CI to avoid failures 2024-11-29 10:01:10 +01:00
José Valim afc99e2ca4 Fix local installer warnings 2024-11-27 21:16:23 +01:00
José Valim ede345f3e9 Clean up app start logs 2024-11-27 21:12:42 +01:00
José Valim 80993a465a Update docs generation config 2024-11-27 20:46:52 +01:00
José Valim 13f527aaa7 Use arity() consistently 2024-11-27 18:56:22 +01:00
José Valim e3bad83233 Move __deriving__ callback to the protocol 2024-11-27 18:38:48 +01:00
José Valim 1f98708649 Add @undefined_impl_description 2024-11-27 18:38:48 +01:00
12780b15dd Add project/0 warning (#14016)
Co-authored-by: Alex Naser <alex.naser@remote.com>
Co-authored-by: José Valim <jose.valim@gmail.com>
2024-11-25 17:04:02 +01:00
José Valim 8eba863f8f Properly track stab inside container_cursor_to_quoted, closes #13826 2024-11-25 14:46:23 +01:00
José Valim a7264aa816 Update docs 2024-11-25 14:46:23 +01:00
José Valim a6d7232db6 Rely on mtime to recompile app (#14012) 2024-11-25 11:34:40 +01:00
Christopher Keele 0c0f53cd66 Correct small typo in PartitionSupervisor (#14014) 2024-11-23 23:35:40 +01:00
José Valim f0ab9d6507 Document use of +JMsingle true
Closes #14013.
2024-11-23 10:57:19 +01:00
Jonatan Kłosko 67c891a5f3 Fix typo in formatter docs (#14011) 2024-11-22 11:17:33 +09:00
José Valim 6cfb4e833f Attach :root to returned formatter options 2024-11-21 20:59:00 +01:00
José Valim 32e0b9c54f Remove deprecated conditional in Date 2024-11-21 19:59:15 +01:00
José Valim 0ae9ac33d2 Add new options to control deps and plugin loading in formatter 2024-11-21 19:58:52 +01:00
Gary Rennie aa108ebb64 Fix docs for Process.monitor/2 (#14009)
Previously, the example contained 2 issues.

1. A syntax error on `receive` as there were no parens.
2. The example was incomplete, and would hang on the 2nd receive

This commit updates the example to reflect the original intent, by
sending 2 messages and getting a `:noproc` on the 2nd `receive`.
2024-11-21 14:50:49 +01:00
José Valim fb4b1ffe22 Also take Mix.start() into account on MIX_DEBUG=1 2024-11-21 13:22:17 +01:00
José Valim 6e07eb619f Measure the time to boot the CLI 2024-11-21 13:17:26 +01:00
José Valim 2db97960d0 Add a note about mix help <TASK> at the end of mix help 2024-11-21 11:02:21 +01:00
Dave Lucia 2f844fd77b Move value in protocol errors to its own line (#14003) 2024-11-21 10:00:11 +01:00
Tobias Pfeiffer 50619b1dbf Clarify the interplay of Stream and Enum (#14002) 2024-11-19 19:39:42 +01:00
José Valim bb028b951f Always consider Elixir modules on inference (#14000) 2024-11-19 13:40:16 +01:00
dependabot[bot] 84b5db7380 Bump DavidAnson/markdownlint-cli2-action from 17.0.0 to 18.0.0 (#14001)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 17.0.0 to 18.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v17.0.0...v18.0.0)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-18 23:56:33 +01:00
José Valim 4162d230d2 Emit a warning if path dependency does not exist, closes #13915 2024-11-18 14:51:17 +01:00
Jonatan Kłosko 8e1d0411ba Add token metadata to distinguish between :nil and nil (#13999) 2024-11-18 09:32:15 +01:00
José Valim 0174d50d92 Solve map ordering in earlier Erlang/OTP versions 2024-11-17 19:45:52 +01:00
Daniel Kukula a8c92060ea Format inserted/deleted maps in list assertions (#13954) 2024-11-17 14:09:10 +01:00
Guillaume Duboc d7f8578742 Add map normalization cases (#13997) 2024-11-15 19:22:24 +01:00
José Valim 5c88118868 Fix typing violation reported by Dialyzer 2024-11-15 19:20:48 +01:00
Jonatan Kłosko 3b01b2a632 Add separate metadata entry for every block parens (#13996) 2024-11-15 13:55:22 +01:00
Theodor Fiedler 9c7604ccf2 Ignore microsecond precision when validating shift date duration (#13993) 2024-11-14 15:14:47 +01:00
José Valim a6ad2b8c03 Fix test which accidentally removed :throw 2024-11-14 12:28:13 +01:00
José Valim a7c8be77e6 Ensure one arity catch is always expanded to :throw 2024-11-14 11:16:38 +01:00
José Valim 52d6020787 Add functions to list of valid AST nodes
In theory we need to check if the function does not have
an environment but the goal of the check is not to be precise,
but rather to catch obvious mistakes.
2024-11-13 23:13:41 +01:00
José Valim bb9ddb3317 Mark negation inside refute as generated, closes #13992 2024-11-13 23:11:01 +01:00
Ian Harris 7c6efcf960 No need to mention key enforcement on struct update (#13990) 2024-11-13 09:26:22 +01:00
Michael Berkowitz 8b87b4dc9b Fix tyoes -> types typo (#13991) 2024-11-13 09:20:48 +01:00
Guillaume Duboc f869345e27 Add list normalization for printing (#13989) 2024-11-13 00:53:19 +01:00
José Valim bfebc1e30b Fix Dialyzer failures 2024-11-13 00:37:37 +01:00
José Valim 60bd0ae7e8 Simplify test by using automatic generation 2024-11-12 23:39:55 +01:00
José Valim 0182f7d146 Raise for undefined function within unused function 2024-11-12 23:19:41 +01:00
Guillaume Duboc 26318ebb0a Improve perf of tuple_difference (#13988) 2024-11-12 23:19:28 +01:00
Egor Mihnevich 2c3a9067a9 Improve NaiveDateTime.from_erl and Time.from_erl specs (#13987) 2024-11-12 17:35:49 +01:00
José Valim cb2e03688e Perform return type inference and application across local calls (#13984) 2024-11-11 23:06:31 +01:00
Sergei Maximov 6689eb9462 Fix unclosed quotes in c:IEx.Info.info/1 docs (#13983) 2024-11-09 22:55:36 +01:00
José Valim 485ca72ae2 Infer types and use them across remote calls (#13981)
This also unifies handling of deterministic builds with Erlang/OTP
as well as warnings_as_errors.
2024-11-09 18:00:27 +01:00
Dave Lucia b35b0ef435 Simplify the explanation of a basic test case (#13982)
Simplifies the language used to describe a "basic setup for ExUnit"

In particular, I removed the term "test case" which was an alias for a
"test module". "test case" can easily be confused with a `test` block.
2024-11-09 11:01:48 +01:00
José Valim 9d1933b679 Use attributes field to compute behaviours 2024-11-08 12:33:08 +01:00
José Valim 57de6ea33a Type behaviour_info, module_info, and __info__ 2024-11-08 12:33:08 +01:00
Waseem Medhat f18343d8c6 Fix typo in Access.get_and_update/3 error message (#13979) 2024-11-07 22:25:46 +01:00
José Valim 8ed104d175 Add test_pid tag to ExUnit.Case
Closes #13880.
2024-11-07 21:05:04 +01:00
José Valim f44aa8d710 Allow aliases and imports to be enabled conditionally in module body, closes #13975 2024-11-07 17:09:44 +01:00
José Valim 731e714b6f Define fewer modules in aliases tests 2024-11-07 13:45:11 +01:00
José Valim 3146c4a71a Simplify metadata stored in ExCk chunk
The checker only runs on functions, so there is no need
to store macros. This reduces the amount of data serialized
and stored. In Livebook, it makes the group pass run about
30% faster.
2024-11-07 13:32:43 +01:00
José Valim baf77f1cbf Update to set-theoretic docs 2024-11-07 11:59:04 +01:00
Jonatan Kłosko 5b221a5545 Add AST metadata about assoc operator location (#13978) 2024-11-07 10:14:38 +01:00
José Valim 9197ce51fc Handle precedence in = 2024-11-07 08:48:21 +01:00
Jean Klingler fbeccf9681 Clarify that charlists cannot be used as logger metadata (#13977) 2024-11-07 16:26:48 +09:00
José Valim 0e052fd18e Remove unused :match_var tuple 2024-11-06 16:30:55 +01:00
José Valim 578fd6e2f2 Update CHANGELOG 2024-11-06 15:43:58 +01:00
Tobias Pfeiffer 57841a09f4 Bump Windows Runner & add more OTP versions (#13972) 2024-11-06 14:08:45 +01:00
Jonatan Kłosko 7d421b197c Add parens meta to stab operator and empty block (#13973) 2024-11-06 13:55:04 +01:00
José Valim c3dc6c86dd Detect never matching clauses and patterns (#13968) 2024-11-06 13:51:04 +01:00
Tobias Pfeiffer e28a84ce2a Fix bug with schemaless URI in URI.merge/2 (#13970)
Only occurs when mixing strings and URIs, but the docs seem to
encourage that.

Without the fix the test results in the following error:

```
  1) test merge/2 (URITest)
     test/elixir/uri_test.exs:376
     ** (KeyError) key :scheme not found in: "http://google.com/foo"

     If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
     code: assert URI.merge("http://google.com/foo", URI.new!("//example.com/baz"))
     stacktrace:
       (elixir 1.18.0-dev) lib/uri.ex:911: URI.merge/2
       test/elixir/uri_test.exs:390: (test)
```

That's because that (one) particular clause didn't check for the
base to be a `%URI{}`.
2024-11-06 12:35:32 +01:00
Tobias Pfeiffer 75fd563691 Bump CI (OTP 27.1 and Ubuntu 24.04) (#13969) 2024-11-06 11:42:16 +01:00
José Valim 01474e0cd4 Type checking of try 2024-11-05 19:08:18 +01:00
Jonatan Kłosko d0f7c03744 Fix delimiter metadata for single quote atoms and remote calls (#13966) 2024-11-05 17:11:39 +01:00
Jonatan Kłosko ee90126527 Add delimiter meta to demote calls with quoted identifier (#13963) 2024-11-05 13:14:20 +01:00
José Valim 1ca06361b6 Type checking of stacktrace and error/raise functions 2024-11-05 13:13:59 +01:00
José Valim 3e9fb4d5ad Type checking of case and receive 2024-11-05 13:13:59 +01:00
felipe stival 4c7e723bec Fix Logger.put_process_level/2 note (#13959)
The documentation for this function stated that it would override
primary logger level, which is not true.
2024-11-05 11:51:45 +01:00
José Valim 0a694164dc Clarify Logger formatter docs, closes #13962 2024-11-05 11:51:16 +01:00
José Valim 2b687fe733 Type map and struct updates (#13957) 2024-11-05 11:46:55 +01:00
Guillaume Duboc 7abcb484f8 Add tuple_values to descr (#13964) 2024-11-05 11:38:48 +01:00
José Valim 7e1aaacf53 Add more tests around list hd/tl 2024-11-04 16:58:31 +01:00
Jonatan Kłosko e54b87c181 Fix formatter adding extra escapes to remote call functions (#13960) 2024-11-04 14:37:46 +01:00
José Valim ef56e81d34 Improve coverage over union calls 2024-11-04 14:36:53 +01:00
José Valim 1557d1db5f Typecheck cond 2024-11-04 14:36:53 +01:00
Jean Klingler 5d626cb873 Improve validation of unquote_splicing AST (#13961) 2024-11-04 22:08:48 +09:00
José Valim 9bf8233f68 Improve error messages for bad funs 2024-11-04 09:31:09 +01:00
José Valim ae396c109b Type timeouts in receive 2024-11-04 09:25:15 +01:00
José Valim 9f182f9abd No longer automatically tag structs as dynamic 2024-11-03 16:05:23 +01:00
José Valim 0c75027e1f Optimize bare_log invocation (#13956) 2024-11-03 15:44:35 +01:00
Jonatan Kłosko bd7d428ca0 Add metadata about single-expression block closing/opening (#13940) 2024-11-03 10:20:44 +01:00
Jean Klingler 322159cc7f Add test for call with thai atom (#13952) 2024-11-03 17:26:35 +09:00
Jean Klingler cc68ad91cb Add shallow validation when unquoting AST (#13950) 2024-11-03 17:20:33 +09:00
Dave Lucia 9162de7c52 typo (#13953) 2024-11-03 10:20:07 +09:00
José Valim e3f017d0b2 Deal with lists inside tuples inside lists 2024-11-01 21:46:43 +01:00
José Valim 69a95a8e8c Merge empty_list and non_empty_list together on pretty printing 2024-11-01 21:03:27 +01:00
José Valim 4dd0d5d86e Spread pattern info namespace over positive and negative keys 2024-11-01 20:53:12 +01:00
José Valim dfa733314d Properly track patterns nested in lists 2024-11-01 20:49:41 +01:00
José Valim aa3d2357f3 Add guards to typespec translation, closes #13948 2024-11-01 17:27:46 +01:00
José Valim c90bd0fca5 Ensure all variables are propagated in a bad match, closes #13946 2024-11-01 17:21:32 +01:00
José Valim f2d8064121 Do not halt streams twice in Stream.transform/5, closes #13944 2024-10-31 16:42:55 +01:00
José Valim 4a97256839 Remove uneeded blank line 2024-10-31 15:20:45 +01:00
José Valim 8ad110aba0 Clarify bang convention 2024-10-31 15:16:10 +01:00
André Albuquerque 8f594a3b0a Fix typo in anti-pattern docs (#13943) 2024-10-31 11:15:57 +01:00
Wojtek Mach f9eda1c382 Update "Structs with 32 fields or more" anti pattern (#13942) 2024-10-31 11:05:58 +01:00
José Valim af953c15b2 Add anti-pattern on struct with 32 fields or more 2024-10-31 10:32:11 +01:00
José Valim 705407b61a Share profiling code in ParallelCompiler 2024-10-30 20:05:34 +01:00
André Albuquerque 63aa5703de Measure each_cycle callback + deadlock detection (#13941) 2024-10-30 19:45:32 +01:00
José Valim 91e76d311c Keep compatibility with modules from archives 2024-10-30 19:38:39 +01:00
José Valim 166c03dbd7 Remove --warnings-as-errors flag from Erlang build on CI 2024-10-30 10:33:42 +01:00
José Valim 8c78a0ea9c Mark defguard expansion outside of guards as generated 2024-10-30 10:32:30 +01:00
José Valim e6857d5af3 Store record metadata in the documentation chunk (#13939) 2024-10-30 09:24:58 +01:00
Jonatan Kłosko 1c19c60abb Add source and behaviour information to docs chunk metadata (#13914) 2024-10-30 09:18:00 +01:00
José Valim f2565849b1 Enable warnings_as_errors for Erlang only on latest 2024-10-30 09:16:46 +01:00
Enzo c2a0f53951 Add more links across compile documentation (#13766) 2024-10-30 08:53:43 +01:00
José Valim 418ab7618a Avoid warnings during parser_test.exs suite 2024-10-30 08:31:28 +01:00
José Valim 5c29b7e73e Do not crash on preemptive errors with static atoms encoder, closes #13927 2024-10-30 08:31:06 +01:00
José Valim b5eb309e07 Fix clause rewriting in typing violations on apply 2024-10-30 07:20:25 +01:00
José Valim 6886f7c6c6 Address bootstrap 2024-10-29 19:42:45 +01:00
José Valim 4a3f61f703 Implement type checking across multiple clauses 2024-10-29 18:52:12 +01:00
José Valim 809971a690 Remove redundant warnings 2024-10-29 18:52:12 +01:00
Albin Kocheril Chacko 2539f85355 Fix a typo in the docs (#13937) 2024-10-29 15:24:41 +01:00
José Valim c85bfdfabf Registry fixes 2024-10-29 09:50:15 +01:00
José Valim 4b72da2e1b Add PartitionSupervisor.resize!/2 2024-10-29 09:39:58 +01:00
José Valim 1572aacb9e Add Registry.lock/3 2024-10-29 09:39:58 +01:00
Ellyse 5bbe055037 Updates to keywords_and_maps documentation (#13935) 2024-10-29 00:34:53 +01:00
José Valim fce88ba5ca Use syntax allowed in Erlang/OTP 25 2024-10-28 22:48:06 +01:00
José Valim d133c704ac Improvements to documentation chapter 2024-10-28 22:46:54 +01:00
newton 139f96c84e Fix typo on new updated docs (#13934) 2024-10-28 22:29:23 +01:00
dependabot[bot] 385b046370 Bump azure/trusted-signing-action from 0.4.0 to 0.5.0 (#13936)
Bumps [azure/trusted-signing-action](https://github.com/azure/trusted-signing-action) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/azure/trusted-signing-action/releases)
- [Commits](https://github.com/azure/trusted-signing-action/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: azure/trusted-signing-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-28 22:28:11 +01:00
José Valim 16f1917c7a Fix bug in recursive variable detection 2024-10-28 22:21:41 +01:00
José Valim 909f4ce265 Do not mention incompatible in comparison warning 2024-10-28 22:21:41 +01:00
José Valim 42cc3ec06f Simplify syntax reference docs 2024-10-28 17:33:50 +01:00
José Valim 268bb2df93 Provide more hints on missing module message (#13933) 2024-10-28 17:26:22 +01:00
José Valim c5cae45963 More updates to keywords and maps 2024-10-28 17:25:48 +01:00
José Valim 31e5f5ba1e Simplifying keywords and guards 2024-10-28 17:21:45 +01:00
José Valim bd254aa815 Simplify IO and file system chapter 2024-10-28 17:02:44 +01:00
José Valim 6740246636 Simplify streams examples 2024-10-28 16:50:03 +01:00
José Valim db0e336eae Improvements to recursion guide 2024-10-28 16:38:52 +01:00
José Valim 5f5961284e Improvements to modules and functions guides 2024-10-28 16:31:32 +01:00
José Valim 5d6648b10a Improve introduction to capture syntax shorthand 2024-10-28 16:05:04 +01:00
José Valim 7f64c51fa3 Improvements to cond introduction 2024-10-28 14:54:35 +01:00
José Valim 63ed546755 Deal with maps and tuples of none 2024-10-28 12:49:34 +01:00
José Valim 50d628cc57 Fixes to descr 2024-10-28 12:33:13 +01:00
José Valim aec6d6840c Temporarily comment out typing of not 2024-10-27 20:43:38 +01:00
José Valim 29a385c2ea Type checking of all Kernel inlined functions 2024-10-27 20:37:21 +01:00
José Valim 771cfe6b9b Typing of basic arithmetic operators 2024-10-27 20:07:36 +01:00
José Valim 77f1a858f8 Properly implement static and dynamic modes 2024-10-27 19:39:45 +01:00
José Valim 1b29932cae Optimize optional handling 2024-10-27 16:34:11 +01:00
José Valim c2dc6d18de More docs 2024-10-27 16:33:57 +01:00
José Valim 90f6cede5b Typing of comparison operator 2024-10-27 11:57:18 +01:00
José Valim a0e061f94a Fix warnings during suite 2024-10-27 11:34:40 +01:00
José Valim 17448e7429 Type erlang process functions in Kernel 2024-10-27 11:14:34 +01:00
José Valim 76faed3cb9 Add ansi escapes to typing violations 2024-10-27 11:02:39 +01:00
José Valim af7c8ef11c Simplify format pruning and keep columns 2024-10-27 08:57:35 +01:00
José Valim bbbf3c9bc7 Convert optional to a separate descr field
The previous implementation was expensive to do upfront
checks.
2024-10-27 07:48:06 +01:00
José Valim 503c28ddce Optimize disjoint to abort as soon as possible 2024-10-26 20:12:58 +02:00
José Valim b068710ee7 Properly tag between warnings and errors in types tests 2024-10-26 15:27:49 +02:00
José Valim 54be23261d Avoid re-traversing arguments if they did not change 2024-10-26 15:19:48 +02:00
José Valim 61157aabfd Avoid showing duplicate traces 2024-10-26 15:09:04 +02:00
José Valim 8e455b7661 More type checking 2024-10-26 14:55:16 +02:00
José Valim f43b47622d Use standardized return type 2024-10-26 14:28:36 +02:00
José Valim 74525303fb Type check Integer.to_string and String.to_integer 2024-10-26 13:48:32 +02:00
José Valim 8d60093bff Remove unneeded qualified calls 2024-10-26 11:40:27 +02:00
José Valim 1db2d29e16 Update CHANGELOG 2024-10-26 09:47:21 +02:00
José Valim c7fa5f95fc Add Macro.struct_info! 2024-10-26 09:21:04 +02:00
José Valim 90f504e9c3 Deprecate usage around __struct__/0 2024-10-25 19:20:45 +02:00
José Valim 51289220af Silence warnings 2024-10-25 16:59:38 +02:00
José Valim 47b712eb61 Unify apply and remote handling 2024-10-25 16:40:13 +02:00
José Valim 04741c80f6 Improve coverage on lists 2024-10-25 16:03:20 +02:00
José Valim 6acc6a065b Remove the need to preload exports 2024-10-25 16:03:10 +02:00
José Valim ad1ffd2547 Remove unused clause 2024-10-25 12:05:21 +02:00
José Valim b39205bc69 Optimize sized tuple check 2024-10-25 12:01:55 +02:00
José Valim 50547c1115 Include function name in type warnings 2024-10-25 11:50:31 +02:00
José Valim 94759fb32f Type hd/1 and tl/1 2024-10-25 11:41:25 +02:00
José Valim 8f31a4b73b Warn when equality is always disjoint 2024-10-24 20:19:19 +02:00
José Valim 23982cb78c Type Tuple.duplicate/2 2024-10-24 17:33:52 +02:00
José Valim 9598d555f1 Type Tuple.delete_at/2 2024-10-24 17:25:58 +02:00
José Valim c1ef879746 Type Tuple.insert_at/3 2024-10-24 17:21:34 +02:00
José Valim f0f42eb08d Refactor tuple insert/delete 2024-10-24 15:12:25 +02:00
Guillaume Duboc d59b298dc0 Add tuple type operators for insertion and deletion (#13912) 2024-10-24 14:44:48 +02:00
Guillaume Duboc 799caf3cf8 Support for list types 2024-10-24 13:33:42 +02:00
José Valim 9429e1e742 Add more docs around certificate configuration 2024-10-24 10:54:16 +02:00
José Valim fbc618ea35 Refactor the current type checker for return type inference (#13928)
The main goal is to allow checking to continue even
if it finds typing violations, so we can infer return types.
The violations are replaced by dynamic(). Only the first
violation is reported to avoid cascading warnings.
2024-10-23 12:31:09 +02:00
Zach Daniel e8a670959e Provide suggestions when OptionParser flag has underscores (#13925) 2024-10-23 07:29:46 +02:00
José Valim 09d43562e7 Fix edge case in normalizer for keyword args (#13924)
Closes #13895.
Closes #13916.
2024-10-22 16:56:01 +02:00
jamauro f39d85d93b Change variable from bk to bucket (#13921)
Simple change from `bk` to `bucket` for consistency with the rest of the doc
2024-10-21 18:00:40 +02:00
José Valim ccc5753d4d Update CHANGELOG 2024-10-21 18:00:33 +02:00
Jonatan Kłosko 26899a9d3a Make Mix.Project.with_build_lock/2 public (#13920) 2024-10-21 09:25:24 +02:00
Jonatan Kłosko 74beb7ffa9 Add iex option to automatically purge recompiled modules (#13919) 2024-10-21 09:23:09 +02:00
Jonatan Männchen 90fae5d42b Upload Release Attestations (#13917) 2024-10-21 09:22:23 +02:00
Jonatan Kłosko f17fc8b4c3 Add note on mix listener messages catch-all (#13918) 2024-10-21 08:03:29 +02:00
José Valim 860f485bdf Inference of patterns (#13909)
This pull request adds inference of patterns, being able to
refine types as new information is added. The next step is
to use this inference in the compiler itself.

Future work will add module-local inference of return types
(most likely still in v1.18) and inference of guards (most
likely in v1.19).

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), there is a much cleaner version of writing
the same code, that does not require solving cycles in our head:

    def foo(x, x, x)
2024-10-18 11:01:23 +02:00
Jean Klingler 673fe4a893 Fix crash in Macro.to_string/1 on invalid sigils (#13905) 2024-10-17 15:38:49 +09:00
Jean Klingler e7ec9a0d08 prune_metadata keeps delimiters (#13911) 2024-10-17 15:38:22 +09:00
Jean Klingler f99c856927 Code.Fragment.surround_context/2 handles &123 as :capture_arg (#13847) 2024-10-17 07:18:56 +09:00
Jonatan Kłosko 9c42348f64 Add API for fetching all persisted compiler diagnostics (#13907) 2024-10-16 12:28:25 +02:00
Zeke Douandc4710n fa2a49f66e Improve docs for URI.encode/2 (#13908)
Co-authored-by: c4710n <c4710n@users.noreply.github.com>
2024-10-16 11:12:05 +02:00
Jonatan Kłosko 9860f96f7f Notify each dep compilation from deps.compile (#13906) 2024-10-15 17:20:16 +02:00
Jonatan Kłosko 5ec9ca5ff1 Notify modules diff from compile.erlang (#13904) 2024-10-15 08:31:49 +02:00
Andrea Leopardi 2ce6cfdb7c Add "since" docs to ExUnit's :group 2024-10-14 19:16:46 +02:00
Jonatan Kłosko 7e909060a0 Add API for listening to concurrent mix compilations (#13896) 2024-10-14 16:26:48 +02:00
Jean Klingler 251badd7c6 Fix warning when dbg with/1 with catch-all clause (#13903)
* Fix warning when dbg with/1 with catch-all clause

* Mention with/1 in dbg changelog
2024-10-14 17:55:59 +09:00
DeedleFake 656944b30c Fix a small typo in a warning in Access (#13901) 2024-10-14 00:43:00 +02:00
Art Kay 072c56e05b Fix two typos (#13900) 2024-10-14 07:36:48 +09:00
José Valim 8ef2684e3a Avoid timing issues in CI 2024-10-13 20:37:24 +02:00
José Valim 3320a11456 Ensure sync tests are executed 2024-10-13 18:23:29 +02:00
José Valim a992ee9c66 Only spawn setup_all process if there is such callbacks 2024-10-13 17:25:12 +02:00
Dino Kovač 7190f41cc9 Support :group option in ExUnit (#13897)
Tests in the same group cannot run concurrently,
but it can run concurrently with other groups.
2024-10-13 16:31:24 +02:00
Mikael Fangel 0c7459e6fd Simplify Integer.digits/2 (#13899) 2024-10-13 23:27:35 +09:00
Mikael Fangel e8821d145e Use Float.{to_string/1, to_charlist/1} when possible (#13898) 2024-10-13 11:45:00 +02:00
Jean Klingler ed75e6066c Add more info for :with expressions in dbg/1 (#13891) 2024-10-12 08:15:12 +09:00
José Valim 08c72e35b8 Bring back path loading optimization and remove stale test 2024-10-11 09:05:44 +02:00
Ahmed Kamal bd899cc399 Reword some docs in the getting started guide (#13888) 2024-10-10 20:40:04 +02:00
José Valim 4b4d2cb3ff Ensure async property is preserved when not given 2024-10-10 12:10:15 +02:00
Jean Klingler 8321e905df Add @doc false to Inspect.Algebra.no_limit/1 (#13893) 2024-10-10 18:13:46 +09:00
Thanabodee Charoenpiriyakij 8786bcca7f Fetch tags before building docs (#13894)
This is regression by https://github.com/elixir-lang/elixir/pull/13845. Docs artifact needs
to fetch the tags before start building it to make version selection works.

Fixes #13892

Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
2024-10-10 09:26:59 +02:00
Jonatan Kłosko be84111523 Wrap deps tasks in a lock (#13889) 2024-10-08 14:58:02 +02:00
Jean Klingler 2551fbd1a7 Move fun/arity explanation later in getting started (#13890) 2024-10-08 21:24:16 +09:00
Jean Klingler 39d1ecb499 Various small improvements in guides and docs (#13887) 2024-10-08 18:05:38 +09:00
Jonatan Kłosko 63e9caf507 Wrap compilation in a cross-os-process lock (#13860) 2024-10-08 08:19:07 +02:00
Jean Klingler fb31253a8f Consistently use links without arities in getting started first pages (#13885) 2024-10-08 07:02:55 +09:00
Jean Klingler a4adaa871f Fix moduledoc for Code: close backtick (#13884) 2024-10-07 19:24:22 +09:00
Ahmed Kamal 290ad65ddf docs: add arity to macro reference of def/2 and defmodule/2 (#13882) 2024-10-07 11:57:38 +02:00
José Valim c60b4774bf Bring old implementation of List.zip back
The previous implementation supported lists of tuples.
Although its @spec only accepted lists of lists, we bring
it back to avoid unnecessary breaking changes, especially
since the function has been deprecated.
2024-10-07 10:47:51 +02:00
Jean Klingler 49979d241b Fix signature of trunc/1 in guide (#13883) 2024-10-07 15:58:41 +09:00
José Valim 8f98f5019a Fix default child spec id of partition supervisor 2024-10-06 22:32:42 +02:00
José Valim 35d7046f31 Move imported_quoted event to elixir_quote module 2024-10-06 21:10:32 +02:00
José Valim 3ca7aaed24 Document imported_quoted trace event, closes #13878 2024-10-06 12:30:50 +02:00
Ahmed Kamal 6c5502b589 Fix a typo in the docs (#13879) 2024-10-05 16:53:10 +02:00
José Valim 0edf4d29cc Fail fast if StringIO receives a file operation 2024-10-04 19:31:55 +02:00
Thanabodee Charoenpiriyakij 9d5af3fe32 Improve upload artifact to builds.hex.pm workflow (#13845) 2024-10-02 19:25:00 +02:00
Omar Odeh 6152ecb82f Replace "dict" with "map" in Agents documentation page (#13874)
Dictionary is deprecated as per the documentation, and the example is using Map module.
2024-10-02 08:49:27 +02:00
José Valim e2556fd478 Clarify behaviour of Macro.to_string on invalid ASTs
Closes #13875.
2024-10-02 08:48:49 +02:00
Jean Klingler 8dcc969517 Replace any -> term in modules or functions mixing them (#13872) 2024-10-01 20:05:11 +09:00
Vasily Suvorov 7fbb7a89ea Fix typedoc for init_option (#13870)
Change `c:init/1` to`init/2` in the `typedoc`'s text
2024-09-30 20:11:46 +02:00
Frerich Raabe a2012c26e2 Update .gitignore generated by 'mix new' (#13871)
The .gitignore file would reference '/.fetch' files, which caused some
confusion (cf.
https://elixir-lang.slack.com/archives/C03EPRA3B/p1727706411018609).

Some research suggests that these files are no longer generated as of
commit 59b6e2ee8b so let's stop mentioning
them in the .gitignore, too.
2024-09-30 16:51:37 +02:00
Jean Klingler f685a76c87 Fix another dialyzer Kernel.with edge case (#13869)
Close https://github.com/elixir-lang/elixir/issues/13868
2024-09-30 17:11:17 +09:00
José Valim 1f9233213e Improve error messages when matching on size 2024-09-27 08:48:30 +02:00
Tyler Pachal d1271966e7 Update docs for mix test --slowest and --slowest-modules (#13862) 2024-09-27 08:30:46 +02:00
Christopher Keele 44bef71c51 Document new unused ... operator in correct place in operators.md (#13861) 2024-09-26 21:02:30 +02:00
Ariel Otilibili ae49e4e19f Refactored pre-requisites of build_man (#13853) 2024-09-26 14:00:42 +02:00
Ian Harris 0fb34f922f Use direct link to referenced section in docs (#13859) 2024-09-25 20:30:57 +02:00
Mihkel Alavere d690c48737 Fix lexical errors in GenServer docs (#13858) 2024-09-25 08:58:37 +02:00
Matt Enlow 2a25d4e1f4 migrate_unless: don't use |> Kernel.!() in conditions in the head (#13857) 2024-09-25 06:18:28 +09:00
José Valim dcf31bed0e Do not use protocols during consolidation to avoid races (#13856) 2024-09-24 17:22:08 +02:00
Tobias PfeifferandAndrea Leopardi 6cd46d76ee Improve docs for Logger overload protection (#13855)
The previous docs didn't mention that logger may drop messages
entirely. This should give a fair overview of the built-in overload
protections, similar to erlang/otp#8855.

---
Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com>
2024-09-24 17:15:29 +02:00
Nathan Long b8b79474c0 Document checking whether a process on another node is alive (#13854) 2024-09-23 23:33:00 +02:00
José Valim 03cc62933d Type check size in binaries 2024-09-23 12:56:27 +02:00
José Valim cc0cb4f1d0 Update CHANGELOG 2024-09-23 10:19:04 +02:00
José Valim 261a3ed759 Verify the right side of binary generators 2024-09-23 09:57:04 +02:00
Jean Klingler 13cab5f801 Remove mentions to unless/2 in guides (#13852) 2024-09-22 23:05:23 +09:00
Jean Klingler 619b1eebba Soft-deprecate Kernel.unless/2 (#13851) 2024-09-22 21:41:59 +09:00
Jean Klingler b5282802a7 Disable migrate_unless within defmacro and remove dbg for unless (#13850)
* Disable migrate_unless within defmacro

* Delete test for unless expression dbg

* Remove dbg support for unless
2024-09-22 20:04:23 +09:00
Jean Klingler 4126cdd8ab Add --migrate flag to mix format (#13846) 2024-09-21 21:47:16 +09:00
Jean Klingler 5902c29ad5 Add :migrate_unless option to formatter (#13844) 2024-09-20 23:00:01 +09:00
Jean Klingler fb065b7709 Fix Code.Fragment.surround_context for keyword keys (#13843) 2024-09-20 17:53:32 +09:00
Jean Klingler 5eddb4bbfe Rework migrate options for the formatter (#13841) 2024-09-20 17:34:59 +09:00
Jean Klingler 8e31621327 Format charlists as ~c sigils in doc (#13842) 2024-09-20 16:14:30 +09:00
Gonzalo 357793a6f0 Fix Logger docs layout (#13840) 2024-09-19 20:24:21 +02:00
Igor Goryachev 0d4d1e3db1 Fix logger translator tests under Erlang 27.1 (#13839) 2024-09-19 12:57:48 +02:00
Jean Klingler ad4db2e8ec Explicit String.at/2 access cost and alternatives (#13838) 2024-09-19 18:48:49 +09:00
José Valim e0967c24fc Ensure args excludes overrides user ones 2024-09-18 16:44:20 +02:00
Jean Klingler 8cb6abf6d2 Formatter fix: next_break_fits respects line_length (#13792)
* Refactor fits to return :fit | :no_fit

* Formatter fix: next_break_fits respects line_length
2024-09-18 23:13:11 +09:00
José Valim 7bfe5caf08 Update RELEASE.md steps 2024-09-18 14:42:22 +02:00
José Valim 6b15b093c7 Remove line information from struct definitions, closes #13761 2024-09-18 11:35:38 +02:00
José Valim 4104bfc193 Include module attribute line and name on tracing, closes #13762 2024-09-18 10:19:39 +02:00
Urmas Talimaa 7a634a2fe6 Fix parsing of fractional durations (#13832)
The parsing of fractional durations checked for non-negativity by
testing second > 0, which reports false for not only negative integers
but also for 0.

Note that changing `if second > 0` to `if second >= 0` would fix
behaviour for "PT0,6S", but would break "PT-0,6S".
2024-09-18 09:03:36 +02:00
bigbluejay9 a0b5cc8ad5 Use ~c to represent charlist arg for connect/3 (#13837)
Suppresses compiler warning about single quoted strings.

```
warning: single-quoted strings represent charlists. Use ~c"" if you indeed want a charlist or use "" instead
```
2024-09-18 08:26:24 +02:00
Thanabodee Charoenpiriyakij f6856cf801 Remove GITHUB_TOKEN out of attest-build-provenance (#13836)
Signed-off-by: Thanabodee Charoenpiriyakij <wingyminus@gmail.com>
2024-09-17 19:48:53 +02:00
Przemysław Rychter 12dc4b5459 Improve Map.pop!/2 docs 2024-09-17 19:12:09 +02:00
Andreas Lappe 6d44df6573 Fix typo in docs of logger.ex (#13835) 2024-09-17 19:09:42 +02:00
José Valim 6342bb2803 Exclude latest tags 2024-09-17 18:01:25 +02:00
José Valim 22bcecd049 Do not verify modules twice 2024-09-17 16:22:30 +02:00
José Valim ad3646e4fe Fix docs node generation 2024-09-17 09:36:39 +02:00
Wojtek Mach c6477803ed Fix .github/workflows/release.yml (#13831) 2024-09-17 09:20:52 +02:00
dependabot[bot] 38e9de44f7 Bump DavidAnson/markdownlint-cli2-action from 16.0.0 to 17.0.0 (#13830)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 16.0.0 to 17.0.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v16.0.0...v17.0.0)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-17 00:13:35 +02:00
José Valim 79f00192f7 Initial support for tuples and in elem/2 2024-09-16 22:32:48 +02:00
Wojtek Mach 69ad427ff2 Add releases for main & maintenance branches (#13829)
* Add releases for main & maintenance branches

Before this patch, release.yml workflow was only compiling and
publishing builds on tag pushes.

This patch additionally handles branch pushes (main, v1.17, etc) and
publishes them to corresponding releases (main-latest, v1.17-latest,
etc).

When we first looked at using GitHub releases for main builds, we
discarded the idea because we didn't want to keep re-creating (and thus
sending out notifications) releases. There was no API to update the SHA
the release was pointing too either. It's obvious in hindsight but all
we need to do is to update the _tag_ with the latest SHA and force-push
the tag that the release is pointing to.

We now have two places where we build main and maintenance branches,
release.yml and builds.hex.pm.yml, so in future commits we should look
into unifying the two.

* Update .github/workflows/release.yml
2024-09-16 22:31:24 +02:00
José Valim cf3132e89e Add guards and introduce error_type() for clarity 2024-09-16 15:08:35 +02:00
José Valim 44d94b09aa Track tuples in the type system 2024-09-16 14:49:38 +02:00
José Valim fb6dd0f903 Remove map_update as we can implement as a has_key?+put 2024-09-16 14:44:53 +02:00
Guillaume Duboc 66d44bee60 Add map delete, put, fetch type operations (#13795) 2024-09-16 14:39:46 +02:00
Łukasz Jan Niemier 05d9735121 Docs: add link to capture syntax (#13828)
This should provide a link for a reader to investigate more about that
new syntax shown in the example.
2024-09-15 20:42:53 +02:00
asdfzxcx 400ed5d415 Fix broken/outdated erlang doc links (#13827) 2024-09-14 19:33:28 +02:00
Christophe 4bbddcc053 Resolve drive on windows releases (#13825) 2024-09-13 11:39:44 +02:00
Jonatan Kłosko aef7e4eab5 Fix compilation path caching (#13823) 2024-09-12 14:38:57 +02:00
Jean Klingler 5137c33098 Do not allow guards in assert/1 (#13817) 2024-09-09 17:42:11 +09:00
Nathan Long 5cb8633a54 Show charlist sigil here (#13804) 2024-09-06 22:06:19 +02:00
Nathan Long 7a2ca783a3 Link to docs (#13812) 2024-09-06 22:01:20 +02:00
Rudolf Manusadzhian ad97f93108 Add a rule of thumb (#13813)
A rule of thumb when to prefer using `and`, `or` and `not` over `&&`, `||` and `!`
2024-09-06 21:58:19 +02:00
James Shi 37e5539239 Improve clarity of consolidated protocol's optimisation (#13811) 2024-09-06 11:36:39 +02:00
Mathew Shen defad93a11 Fix code example in GenServer docs (#13810) 2024-09-06 11:00:30 +02:00
Jean Klingler 9b9ab9948f Fix: Macro.expand/2 should not expand __ENV__ on :match context (#13807) 2024-09-06 16:23:38 +09:00
Jean Klingler 1286a6257f Replace unless/2 usages in the codebase (#13808) 2024-09-06 16:22:51 +09:00
Wojtek Mach 99beb29837 ex_unit: Add since to :parameterize documentation (#13803) 2024-09-03 07:48:36 +09:00
Jean Klingler 1ccb7a1135 Explicitly mention trimming in String.split/1 doc (#13802) 2024-09-01 14:15:13 +09:00
sarayourfriend 00f26629da Fix a few more minor typos (#13801) 2024-08-31 20:18:46 +02:00
José Valim 2f84c1e945 Store warnings emitted from external resources 2024-08-31 20:11:29 +02:00
sarayourfriend 31f3a74a2d Fix missing sentence subject (#13800)
Thanks for the excellent docs, I am having a great time learning Elixir. Just noticed a small typo 🙂 

Cheers.
2024-08-31 12:01:55 +09:00
Art Kay 281a9df6f7 Fix typos in a test and in a doc (#13799) 2024-08-30 22:53:08 +02:00
João Duarte e1b265ab5f Fix relative path resolution in "mix release" for symlinked scripts (#13796) 2024-08-27 18:40:57 -04:00
José Valim 14595bb63f Add Config.read_config/1 (#13754) 2024-08-27 04:08:17 +02:00
Jean Klingler 1889ee9882 Add visibility for flat_map filtering in docs (#13794) 2024-08-26 20:12:57 +09:00
Jean Klingler a564c2eeb4 Optimize Enum.flat_map/2 for filtering (#13793) 2024-08-26 20:10:45 +09:00
Dave Lucia 27b894c05f Fix grammar in Exception.message/1 callback (#13791) 2024-08-23 16:37:07 +02:00
Nathan Long f408063d69 Define time zone databases in the docs (#13790) 2024-08-21 18:43:49 +02:00
José Valim 79667c6f04 Pass proper path to consolidation in IEx recompile 2024-08-20 11:14:51 +02:00
Wojtek Mach 0678be68d2 Add trusted signing to Windows installers (#13786) 2024-08-19 13:43:26 +02:00
Łukasz Samson 0e4aaf00c5 Correctly validate number of args for clauses with when in for and catch (#13785) 2024-08-19 12:22:33 +02:00
erikosmond f2f4f3a621 Add doc to explain return value with after clauses (#13783) 2024-08-18 23:49:09 +02:00
José Valim 07137a1edd Improve docs around only and optional, closes #13782 2024-08-17 22:02:11 +02:00
Steve Cohen b8cec3037c Ensure :native is a valid parameter type (#13781)
Several of the functions in System that deal with time had a typespec
that only allowed the time_unit type even though they accepted the
:native time unit. This confuses dialyzer if :native is passed to
these functions
2024-08-17 10:52:20 +09:00
David Sulc cf49f8beba Clarify example comment in Enum.slide/3 (#13779) 2024-08-14 09:40:53 +02:00
Douglas Vought 5b742531b2 Load mermaid asynchronously (#13778)
If mermaid is synchronously loaded, a slow load can prevent the sidebar navigation from displaying. Instead, we load mermaid asynchronously and then call `mermaidLoaded()` when it loads.
2024-08-13 15:42:55 +02:00
José Valim 66f0bd0996 Add anti-pattern on compile-time dependency 2024-08-12 15:15:58 +02:00
José Valim 17a01309e0 More improvements to Logger metadata 2024-08-12 14:31:55 +02:00
Krapaince db72cdd5a8 Add required conditions for Logger's metadata to be print (#13777) 2024-08-12 14:24:56 +02:00
José Valim 02a1f07367 Avoid reusing modules across tests, see #13774 2024-08-10 18:53:32 +02:00
CharlesIrvineandChuck Irvine 1b4ec98478 Fix typo (#13776)
Co-authored-by: Chuck Irvine <chuck.irvine@icloud.com>
2024-08-10 18:31:53 +02:00
Steven C. Buttgereit 19057692cf Clarify map key ordering functionality in docs (#13764) 2024-08-09 15:07:19 +02:00
José Valim 00f8b1e0e5 Raise clearer error message if struct is missing on type checker 2024-08-09 13:05:26 +02:00
Jean Klingler e770710379 Add List.ends_with?/2 (#13768) 2024-08-09 18:09:56 +09:00
Jean Klingler fe2425ab05 Soft-deprecate List.zip/1 in favor of Enum.zip/1 (#13767) 2024-08-09 16:57:30 +09:00
Rodolfo Carvalho cc707370bb Fix typo in Task.async_stream/5 docs (#13765) 2024-08-08 10:16:26 +02:00
Frank Hunleth b09914b2d9 Make .app files deterministic in releases (#13763)
When copying .app files to a release, remove references to
`:compile_mtime` so that their contents don't change between builds.
2024-08-07 21:49:25 +02:00
José Valim 699ba28b83 More docs 2024-08-07 01:06:53 +02:00
José Valim 4aace69e01 Keep labels with at least one occurrence on xref graph cycles and stats.
Closes #13762.
2024-08-07 00:57:16 +02:00
Christopher Keele bd92cba860 Improve defguard/1 docs with links to guides (#13757) 2024-08-03 22:14:06 +02:00
Artur Plysiuk 1b73c4997f Fix spec for to_timeout/1 (#13755) 2024-08-02 12:15:01 +02:00
José Valim 8f24d452b4 Improve Logger docs, closes #13749 2024-08-02 09:13:38 +02:00
José Valim 960a568cf9 Optimize and streamline more code in descr 2024-08-01 20:57:58 +02:00
Taine Fan f6d8eb4df5 Add report for "normal" exits in IEx (#13753) 2024-08-01 18:41:30 +02:00
Guillaume Duboc 4629fb6c0e Set-theoretic tuple types (using lists) (#13752) 2024-08-01 16:58:31 +02:00
José Valim d57658a45d Add Mix.Rebar.available?/1 for explicit availability checks 2024-08-01 09:11:32 +02:00
José ValimandMichael Goodyear cb5e812bef Use System.cmd rather than System.shell for Rebar3 to avoid escaping (#13751)
Closes #13750.

Co-authored-by: Michael Goodyear <m@gdyr.co>
2024-07-31 21:45:08 +02:00
José Valim 99be673c3a Taint failure manifest if requiring or compiling tests fail, closes #13728 2024-07-30 13:12:44 +02:00
José Valim b799e9eda4 Revert formatter changes to ||| and &&&, closes #13711 2024-07-29 02:23:57 +02:00
José Valim 220c228f75 Do not emit type warning on generated nodes
Closes #13727.
2024-07-28 20:18:15 +02:00
José Valim 438e13dc8c Do not attempt to group module warnings with context
Otherwise, the larger the context, the more expensive
grouping the warnings would get.

Closes #13742
2024-07-28 20:11:42 +02:00
José Valim 3f0813eee3 Add notes about test helpers, closes #13740 2024-07-27 21:02:36 +02:00
Andrea LeopardiandJosé Valim 25b589851e Document behaviour_info/1 (#13738)
Co-authored-by: José Valim <jose.valim@dashbit.co>
2024-07-26 14:58:48 +02:00
José Valim eb6231d2be Improve spec and docs for Macro.underscore/1, closes #13739 2024-07-26 08:33:05 +02:00
Davion Yang 1e914b04b4 fix document typo for quote (#13737) 2024-07-25 11:17:22 +09:00
José Valim 205ec0330e Document limitations in comments on the formatter
See #13736.
2024-07-24 22:01:25 +02:00
Takumi Hara a3a3210772 Fix typo in docs (#13735) 2024-07-24 16:20:01 +02:00
José Valim a077d50249 Improve docs for Stream 2024-07-24 00:17:32 +02:00
Jonatan Kłosko fd9de33122 Refactor escript tests to use less modules (#13732) 2024-07-23 14:48:08 +02:00
Jonatan Kłosko 998e66de8c Fix escript archive layout and support priv (#13730) 2024-07-23 14:00:50 +02:00
Mayel de Borniol c887a8ab2a Update doc link to anti-pattern (#13729) 2024-07-22 07:58:47 -03:00
ponychicken 9e82da40c6 Add some info about Unix style convention in IO (#13724) 2024-07-17 13:24:23 +02:00
José Valim d3b8dcd99d Fallback to executable implementation when year is out of ISO range
Closes #13712
Closes #13720
2024-07-13 10:21:17 -03:00
José Valim 763543ce26 Document \r\n normalization, closes #13717 2024-07-11 18:05:09 +02:00
Alisina Bahadori 7a90b59b1f Enable use of unquote in defguard(p) (#13716) 2024-07-10 18:17:07 -03:00
José Valim 797e503e40 Move test case up 2024-07-10 15:04:08 +02:00
Rodolfo Carvalho 58e76cc8d2 Bring back support for Git :ref and :depth used together (#13713)
An optimization to fetch only the about-to-be-used refs introduced when
the :depth option was added broke support for specifying a SHA1 prefix
as :ref. Such possibility was neither documented nor tested, but
certainly used in the wild. This use case is now explicitly supported
with a new test case.

We maintain this fetch optimization whenever :branch, :tag or :depth is
specified. We fetch all refs when :ref is used without :depth to be able
to resolve short SHA1 refs, and require full commit hashes to use :ref
and :depth together.

When using :ref with :depth, a full SHA1 is required by Git and surfaced
in our docs. We enforce it when validating the options, leaving room for
longer hashes.

This commit brings back two tests to cover the :ref and :depth
functionality, and adds a new one to cover the validation mentioned
above.
2024-07-10 14:53:03 +02:00
Dimitar Panayotov bc436c8ac8 chore(docs): fix link to official GIT docs (#13714) 2024-07-09 13:06:32 -03:00
José Valim d9cf285d71 Enforce parens when formatting on more operators (#13711)
* On bitwise operators

* On right precedence operators which are semantically
  non-associative (such as -- and potentially ---)
2024-07-08 18:23:14 +02:00
José Valim 18bbfed45e Recompile when version changes, closes #13709 2024-07-06 12:50:12 +02:00
Art Kay 35b22cf642 Fix a typo in the docs (#13707) 2024-07-05 04:52:42 +09:00
JoeZ99 8706d02fa2 Update Task.Supervisor.option() (#13704) 2024-07-03 12:56:14 +02:00
jyc b8886741bd Clarify what specifying the module name does with & (#13668) 2024-07-03 09:07:32 +02:00
Jean Klingler 8539b5b4fc Minor change in Enum.with/2 docs (#13703) 2024-07-03 14:17:06 +09:00
José Valim 786f9592b7 Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:49 +02:00
Travis Vander Hoop 2d1f280034 Improve Enum.with_index/2 docs (#13671) 2024-07-02 15:25:48 +02:00
JoeZ99 bb902bc395 Change DynamicSupervisor doc to avoid confusions (#13678) 2024-07-02 15:24:57 +02:00
José Valim c08a9b371c Update CHANGELOG 2024-07-02 15:21:25 +02:00
José Valim 692b13dcfe Remove unused import 2024-07-02 14:56:21 +02:00
José Valim f01dc17d85 Optimize reversing logic
Rule #1:

    Enum.reverse([1, 2, 3]) ++ [4, 5, 6]

is equivalent but slower than

    Enum.reverse([1, 2, 3], [4, 5, 6])

Rule #2:

    Enum.reverse([1, 2, 3] ++ [4, 5, 6])

is equivalent but slower than:

    Enum.reverse([4, 5, 6], Enum.reverse([1, 2, 3]))

Rule #3

    Enum.reverse(Enum.reverse([1, 2, 3]))

is the same as:

    [1, 2, 3]
2024-07-02 14:51:49 +02:00
José Valim 9924afff5d Remove highly restrictive scriptset support 2024-07-02 14:41:57 +02:00
José Valim fcfa87ff36 Simplify tests 2024-07-02 14:22:51 +02:00
Luc Fueston c83334e5f2 Directional confusability protection & allowing script mixing in identifiers when separated by underscores (#13693)
* update support for uts39 from unicode 15

* follow uts39's recco that it's not necessary to require
  idents to be single-script (they call out proglang idents,
  reference the new uts55-5). We use a heuristic derived from
  the concept of identifier chunks from uts55-5, to allow
  idents like foo_bar_baz where each chunk around the _ can be
  single-or-highly-restrictive

* provide directional confusability detection, by reversing
  spans of direction-changed chars in idents for bidi_skeleton,
  see issue #12929
2024-07-02 14:07:20 +02:00
José Valim bd99dcc6f8 Add warning around runtime modification of compile env 2024-07-01 11:59:29 +02:00
Wojtek Mach 5deee85546 Update URI.parse/1 note about deprecated authority field (#13697) 2024-06-27 10:51:44 +02:00
Andrea Leopardi f082b2dc87 Add MissingDependenciesError exception (#13689) 2024-06-27 09:16:46 +02:00
José Valim 31f5f126a6 Clarify cookie generation, closes #13694 2024-06-26 13:28:52 +02:00
Devin Alexander Torres f0fcd64f93 Inline more functions (#13692) 2024-06-25 15:13:17 +02:00
Jean Klingler 8ed692d4c9 Inline Version parse* functions when constant (#13690) 2024-06-25 22:11:35 +09:00
Alexey Nikitin 1b604f57dc Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:46:48 +02:00
Jean Klingler 8745b9ea71 Fix some issues in syntax reference (#13687) 2024-06-22 22:08:25 +09:00
José Valim f1f54df22a Document tuples inside containers, closes #13681 2024-06-22 13:39:52 +02:00
Jean Klingler 2eb85eaf4b Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:09 +09:00
Ahmed ab881db292 fix mix description typo (#13686) 2024-06-22 19:53:55 +09:00
Jean Klingler a2fe01d56a Use Enum.sum_by/2 when appropriate (#13685) 2024-06-22 16:20:08 +09:00
Jean Klingler ef81dedfd3 Add Enum.product_by/2 (#13683) 2024-06-22 07:50:34 +09:00
Jean Klingler 0df128c33a Add Enum.sum_by/2 (#12885) 2024-06-22 06:27:42 +09:00
Colin Caine e2e38f3440 Fix typos in with docstring (#13680) 2024-06-21 18:08:50 +02:00
Wojtek Mach 134941e2b5 Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

  * Exit with `ERRORLEVEL` set to `1` on errors.

    Previously we did not and so apps that use releases and rely on exit
    codes to report errors were not doing so (e.g. Livebook).

  * Rename rpc node name from `rem-(...)` to `rpc-(...)`

  * Update generated `rel/env.bat.exs`
2024-06-21 16:06:06 +02:00
Jean Klingler 0793fc91b9 Inspect special purpose chars using their unicode representation (#13676)
This reduces confusion when working with zero-width characters or alternative spaces.

Relates to #13673
2024-06-19 16:05:17 +09:00
José Valim e8ea6a5259 Print type violation slogan 2024-06-18 12:46:53 +02:00
José Valim 61acf336c1 Clarify syntax reference headings 2024-06-17 09:30:33 +02:00
José Valim ace841ff85 Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:33 +02:00
Jean Klingler 1c17f1eed3 Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:17 +09:00
José Valim a39f469a76 Do not warn for assignment with blocks in EEx 2024-06-15 11:04:53 +02:00
José Valim b56b49fa2c Clarify docs, closes #13666 2024-06-15 10:20:34 +02:00
José Valim 1e0559e9ed Clarify charlists intro, closes #13663 2024-06-14 17:34:16 +02:00
José Valim c911466bbb Provide references on structs for more information, closes #13664 2024-06-14 17:31:22 +02:00
José Valim 7890e56c3c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

1. If we introduce inference of return types, then we
   need to infer and check the types for A again. This
   may be an argument for not introducing inference of
   return types (only of patterns and guards)

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:03:19 +02:00
José Valim 8c5c421b58 Avoid compiling some values twice when building .beam 2024-06-14 09:06:56 +02:00
José Valim 420fefcae5 Raise on timeout waiting for port 2024-06-13 16:29:25 +02:00
Jean Klingler d25845c9d5 Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 21:33:36 +09:00
Ben Murden 5945fb6fb4 Minor grammatical fixes on warning message (#13654) 2024-06-13 08:36:16 +02:00
Jonatan Kłosko 1e6f2d48ae Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-12 22:23:44 +02:00
Philip Sampaio 69bc8a756f Use GitHub Actions' "artifact attestation" feature (#13653)
The idea is to have a way to ensure that the artifacts were not tempered
after the build.

You can find details in the projects page:
https://github.com/actions/attest-build-provenance

There is also the docs page:
https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
2024-06-12 12:48:37 +02:00
José Valim 252d4faef7 Clarify typespecs handling 2024-06-11 21:14:11 +02:00
Philip Sampaio ac933f71f6 Add PowerShell scripts (#13592)
The idea is to provide them as alternative to bat scripts and potentially
replace all bat scripts in the future. One benefit of using PowerShell is
that we can also run it on Linux systems, facilitating dev and testing.
2024-06-11 12:17:36 +02:00
Steffen Deusch 63d0932304 Add test for mix test --only async/sync filter (#13651) 2024-06-10 23:07:21 +02:00
José Valim ce92b1cbb3 s/detail/details 2024-06-10 22:38:15 +02:00
Maksym Verbovyi 9a06273ad2 Correct example in the docs (#13638) 2024-06-10 16:37:57 +02:00
Lukasz Samson f3c3d3dde6 Fix crash when capturing ambiguous import 2024-06-10 16:37:10 +02:00
José Valim 7a4060cbf3 Add detail and span to typing diagnostics
Closes #13646.
2024-06-10 16:13:39 +02:00
Andrea Leopardi d5095f64d2 Add a missing dash to Process.sleep/1 docs 2024-06-10 10:19:14 +02:00
José Valim ecac839810 Load async and module tags in runner, closes #13639 2024-06-09 23:53:15 +02:00
José Valim c69938bae7 Use :dot_iex as the option name consistently 2024-06-09 23:28:31 +02:00
Chris Miller 26c6b1bcf9 Add dot_iex IEx.Config for .iex lookup (#13647) 2024-06-09 23:18:45 +02:00
José Valim ab7de3a779 Generate different tmp_dir for parameterized tests 2024-06-09 23:01:15 +02:00
Vsevolog Grigoriev f484960800 Handle arbitrarily high integer values in Process.sleep/1 (#13649)
Aligns Process.sleep/1 behaviour with OTP :timer.sleep/1 function.
https://www.erlang.org/doc/apps/stdlib/timer.html#sleep/1
2024-06-09 18:20:02 +02:00
Aleksei Matiushkin 4119ec4b2c format_elixir: true option for file generation (#13644) 2024-06-06 18:35:22 +02:00
Jean Klingler 8ca99ab24d Revert types from #13552 (#13645) 2024-06-06 23:01:53 +09:00
Jean Klingler cabd7d8506 Add more information to anonymous rescue hint (#13641) 2024-06-06 21:15:11 +09:00
Travis Vander Hoop b0459d2c44 Update case.ex with correct :since for register_test/6 (#13642)
while addressing the `ExUnit.Case.register_test/3` deprecation in https://github.com/elixir-wallaby/wallaby/pull/776, I wondered how long `register_test/6` had been around. In looking at the source and the docs, I uncovered what I believe is an incorrect `since` attribute
2024-06-06 14:06:00 +02:00
Theodor Fiedler 67b8b6c218 Extend Duration shift documentation (#13640) 2024-06-06 00:04:20 +02:00
José Valim 21840ed563 Do not assume all parser errors can be parsed into erlang terms 2024-06-05 12:21:23 +02:00
José Valim 09c602d10e Properly parse quoted identifiers in function calls 2024-06-05 12:03:56 +02:00
José Valim ad1ac3b783 Use a different name for the registry 2024-06-05 11:33:04 +02:00
Steffen Deusch 844c4c30de use a :queue to store modules in ExUnit.Server (#13636)
In order to have more deterministic test runs when using `--max-cases 1`
and `--max-requires 1` (https://github.com/elixir-lang/elixir/pull/13635)
(see also https://github.com/elixir-lang/elixir/pull/13589), we need to
run tests in compilation order (FIFO).

In the past, ExUnit.Server appended new tests to the front of a list,
which would result in the most recently added test to be run first.

Let's quickly demonstrate the problem this causes for deterministic runs
with a simple example:

Imagine a test (let's call if FooTest) that takes a non-deterministic
amount of time to run. For now let's assume that it sometimes takes 1
second and sometimes up to 5. And as async tests execute in parallel with
compilation of other test files, we could have the following scenario:

FooTest is compiled and because it's async it is immediately started.
It takes 1 second to run.
In this 1 second two more tests are compiled. First BarTest is prepended
to the list, then BazTest.
The order of test runs now is:

FooTest, BazTest, then whatever is last compiled while BazTest runs, ...

Now another run, FooTest takes 5 seconds to run.
While FooTest runs, more than two other tests are compiled.
The order of test runs is:

FooTest, LastCompiledTest, SecondLastCompiledTest, ..., BazTest, BarTest

This can be fixed either by appending new test modules to the end of the
list, or - and that's what this commit does - by using a `:queue`
instead.
2024-06-05 11:21:07 +02:00
José Valim d6e6924c89 Purge modules between tests 2024-06-05 11:20:27 +02:00
José Valim f712557db0 Update CHANGELOG 2024-06-05 11:08:05 +02:00
Steffen Deusch 14e9a07b78 Add --max-requires to mix test (#13635) 2024-06-04 23:51:21 +02:00
Steffen Deusch c28a4eb040 Add max_concurrency to Kernel.ParallelCompiler (#13634) 2024-06-04 16:04:15 +02:00
Jean Klingler 1f6eba887d Fix typos in IEx i/1 for improper lists (#13633) 2024-06-04 08:11:15 +09:00
José Valim 23824621f1 Update release instructions 2024-06-03 14:40:37 +02:00
Jean Klingler 48113d471e Add :report option to aggregate results over multiple processes (#13632) 2024-06-03 20:47:56 +09:00
José Valim b50d986631 Use __info__ callback directly (#13630) 2024-06-02 16:17:02 +02:00
José Valim 11aa0fc526 Optimize :term (#13625) 2024-06-02 14:58:57 +02:00
liamwhite ae60fa6bd5 Document bare decimal case on String.to_float/1 and Float.parse/1 (#13629) 2024-06-02 14:58:00 +02:00
Łukasz Samson 29bb5116d1 Allow to override the info callback in Macro.Env.define_import (#13628) 2024-06-02 14:02:33 +02:00
José Valim 478256b59e Avoid discarding information on conversion to #elixir_ex 2024-06-01 17:58:01 +02:00
José Valim 60f6515c24 Avoid nesting of capture inside macros
Unfortunately this makes it so the unused capture
warnings emit false positives, so this particular
warning was removed.

Closes #13609.
2024-06-01 17:08:18 +02:00
José Valim 7de811cfd5 Fix warnings and races in tests 2024-05-31 22:37:05 +02:00
José Valim ff26a8fac6 Deprecate Module.eval_quoted in favor of Code.eval_quote 2024-05-30 21:07:03 +02:00
José Valim b56ed82fe3 Type check the function type on fun.() 2024-05-30 20:37:37 +02:00
José Valim a52133fdf0 Remove more checks that can now be performed by the type system 2024-05-30 20:37:37 +02:00
Daniel Kukula ed67d6be18 implement dbg for code blocks (#13619) 2024-05-30 11:02:01 +02:00
Jean Klingler 91feadd479 Implement dbg for unless/2 expressions (#13620) 2024-05-30 15:46:57 +09:00
Jean Klingler c2c4cade84 Replace statement -> expression when relevant (#13621) 2024-05-30 15:46:40 +09:00
José Valim 8dabe63030 Reorganize testing structure 2024-05-29 22:22:33 +02:00
José Valim b665ddd6db Add parameterized tests (#13618)
Sometimes you want to run the same tests but with different parameters.
In ExUnit, it is possible to do so by passing a `:parameterize` key to
`ExUnit.Case`. The value must be a list of maps which will be the
parameters merged into the test context.

For example, Elixir has a module called `Registry`, which can have type
`:unique` or `:duplicate`, and can control its concurrency factor using
the `:partitions` option. If you have a number of tests that *behave the
same* across all of those values, you can parameterize those tests with:

    use ExUnit.Case,
      async: true,
      parameterize:
        for(kind <- [:unique, :duplicate],
            partitions <- [1, 8],
            do: %{kind: kind, partitions: partitions})

Then, in your tests, you can access the parameters as part of the context:

    test "starts a registry", %{kind: kind, partitions: partitions} do
      ...
    end

Use parameterized tests with care:

  * Although parameterized tests run concurrently when `async: true` is also given,
    abuse of parameterized tests may make your test suite slower

  * If you use parameterized tests and then find yourself adding conditionals
    in your tests to deal with different parameters, then parameterized tests
    may be the wrong solution to your problem. Consider creating separated
    tests and sharing logic between them using regular functions
2024-05-29 15:44:16 +02:00
José Valim a7cd3647e8 Port custom exceptions to ArgumentError
Those exceptions are not meant to be rescued,
so there is no benefit into providing a custom
module for them.
2024-05-29 11:23:04 +02:00
José Valim 1b7a7341e1 Improve ExUnit Callbacks docs 2024-05-29 10:53:13 +02:00
Daniel KukulaandJean Klingler 7cf57ec815 Implement dbg/2 for "if" expressions (#13603)
Co-authored-by: Jean Klingler <sabiwara@gmail.com>
2024-05-29 08:30:00 +02:00
José Valim 967c87b9d3 Simplify iso conversion, prefer Duration.new! 2024-05-29 00:05:48 +02:00
José Valim cd1529fc3c Remove code comment from release example 2024-05-29 00:05:48 +02:00
Jean Klingler a49a9d88e5 Add sort :per_call option to tprof (#13611) 2024-05-29 06:59:50 +09:00
Tangui fcc7ea20ef Implement Duration.to_iso8601/1 (#13604) 2024-05-28 23:52:17 +02:00
Theodor Fiedler 29fb214f14 Support plus sign prefix in Calendar.ISO.parse_duration/1 (#13613) 2024-05-28 21:12:47 +02:00
José Valim a64f28c8dd Only emit deprecation warning for EEx if handle_text/3 is missing 2024-05-28 21:12:09 +02:00
Jean Klingler c923f731d8 Add precision about --warnings-as-errors (#13610) 2024-05-28 17:04:03 +09:00
Theodor Fiedler 420a939f1b Support negative period prefix in Calendar.ISO.parse_duration/1 (#13608) 2024-05-27 23:05:41 +02:00
Łukasz Samson 642cc79bb7 Keep position meta on & capture special variables in expanded AST (#13607) 2024-05-27 21:31:04 +02:00
Jean Klingler 76fec7eda2 Implement mix profile.tprof (#13605) 2024-05-27 21:51:29 +09:00
Theodor Fiedler ae5be3106f Add Duration.from_iso8601/1 (#13473) 2024-05-27 11:47:38 +02:00
José Valim 47079bc0b2 Add more deprecations scheduled to v1.18 2024-05-26 21:44:10 +02:00
José Valim 19406b1f55 Add Keyword.intersect/2-3 2024-05-26 10:58:54 +02:00
José Valim 1b6c31a0eb Avoid the number of duplicate traces on imports 2024-05-25 11:10:22 +02:00
José Valim 1508494761 Remove assert_no_clauses check as we already handle -> 2024-05-25 10:55:42 +02:00
José Valim e29af5ac43 Avoid double tracing on captures 2024-05-25 10:47:57 +02:00
Yuri Pereira Constante 22371ce3a9 Generate cover HTML files in parallel (#13596) 2024-05-24 20:27:16 +02:00
Juergen Braungardt 5957b0b274 Fix documentation of option :stderr_to_stdout for System.cmd (#13597) 2024-05-24 19:58:03 +02:00
José Valim 43788c0b4b Use Mix.shell().error() for compiler tasks warnings 2024-05-24 19:57:18 +02:00
José Valim 25e6ce4271 Add pending deprecations 2024-05-24 13:11:29 +02:00
José Valim 54b3736eac Remove --werl support 2024-05-24 12:46:21 +02:00
José Valim 5a4356fd56 Start Elixir v1.18 2024-05-24 12:43:13 +02:00
401 changed files with 31269 additions and 10651 deletions
+1 -2
View File
@@ -17,6 +17,5 @@
# Float tests
float_assert: 1
],
normalize_bitstring_modifiers: false
]
]
-134
View File
@@ -1,134 +0,0 @@
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: 25
otp_version: "25.3"
upload_generic_version: upload_generic_version
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.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
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v16.0.0
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
with:
globs: |
lib/elixir/pages/**/*.md
+13 -8
View File
@@ -11,7 +11,6 @@ on:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
@@ -19,13 +18,16 @@ permissions:
jobs:
test_linux:
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "27.0"
- otp_version: "27.1"
deterministic: true
- otp_version: "27.1"
otp_latest: true
- otp_version: "27.0"
- otp_version: "26.0"
- otp_version: "25.3"
- otp_version: "25.0"
@@ -33,7 +35,7 @@ jobs:
development: true
- otp_version: maint
development: true
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
@@ -41,6 +43,9 @@ 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
@@ -65,19 +70,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.0"]
runs-on: windows-2019
otp_version: ["25.3", "26.2", "27.1"]
runs-on: windows-2022
steps:
- name: Configure Git
run: git config --global core.autocrlf input
@@ -104,7 +109,7 @@ jobs:
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
+286 -16
View File
@@ -2,16 +2,20 @@ 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:
@@ -20,6 +24,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
@@ -27,8 +32,29 @@ jobs:
--notes '' \
--draft \
${{ github.ref_name }}
release_pre_built:
needs: create_draft_release
- 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:
strategy:
fail-fast: true
matrix:
@@ -45,25 +71,269 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: ./.github/workflows/release_pre_built
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Upload Pre-built
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
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
- 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]
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' }}
steps:
- uses: actions/download-artifact@v4
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: "Sign files with Trusted Signing"
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
uses: azure/trusted-signing-action@v0.5.0
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: trusted-signing-elixir
certificate-profile-name: Elixir
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: |
gh release upload --clobber "${{ github.ref_name }}" \
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.sha{1,256}sum \
Docs.zip.sigstore
upload-builds-hex-pm:
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
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 }}
OTP_GENERIC_VERSION: "25"
steps:
- 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
@@ -19,8 +19,6 @@ 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
@@ -34,8 +32,6 @@ 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
@@ -46,7 +42,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@v3
- uses: actions/checkout@v4
if: ${{ inputs.build_docs }}
with:
repository: elixir-lang/ex_doc
@@ -64,6 +60,5 @@ 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
+368 -118
View File
@@ -1,167 +1,417 @@
# Changelog for Elixir v1.17
# Changelog for Elixir v1.18
## Warnings from gradual set-theoretic types
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.
This release introduces gradual set-theoretic types to infer types from patterns and guards and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
## Type system improvements
At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
* `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type.
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified allows keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
We focused on atoms and maps on this initial release as they are respectively the simplest and the most complex types representations, so we can stress the performance of the type system and quality of error messages. Modelling these types will also provide the most immediate benefits to Elixir developers. Assuming there is a variable named `user`, holding a `%User{}` struct with an `address` field, Elixir v1.17 will emit the following warnings at compile-time:
* Pattern matching against a map or a struct that does not have the given key, such as `%{adress: ...} = user` (notice `address` vs `adress`)
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
* Performing structural comparisons between non-overlapping types, such as `integer >= string`
* Building and pattern matching on binaries without the relevant specifiers, such as `<<string>>` (this warns because by default it expects an integer)
* Attempting to rescue an undefined exception or an exception that is not a struct
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind that not all maps have statically known keys, and the Elixir typechecker only infers types from patterns at the moment.
Future Elixir versions will infer and type check more constructs, bringing Elixir developers more warnings and quality of life improvements without changes to code. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
## Erlang/OTP support
This release adds support for Erlang/OTP 27 and drops support for Erlang/OTP 24. We recommend Elixir developers to migrate to Erlang/OTP 26 or later, especially on Windows. Support for WERL (a graphical user interface for the Erlang terminal on Windows) will be removed in Elixir v1.18.
## Adding `Duration` and `shift/2` functions
Elixir introduces the `Duration` data type and APIs to shift dates, times, and date times by a given duration, considering different calendars and time zones.
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
iex> Date.shift(~D[2016-01-31], month: 2)
~D[2016-03-31]
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
```
Note the operation is called `shift` (instead of `add`) since working with durations does not obey properties such as associativity. For instance, adding one month and then one month does not give the same result as adding two months:
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
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
User.drive({:ok, %User{}}, car_choices)
```
Still, durations are essential for building intervals, recurring events, and modelling scheduling complexities found in the world around us. For `DateTime`s, Elixir will correctly deal with time zone changes (such as Daylight Saving Time), but provisions are also available in case you want to surface conflicts (for example, you shifted to a wall clock that does not exist, because the clock has been moved forward by one hour). See `DateTime.shift/2` for examples.
Here is the warning:
Finally, a new `Kernel.to_timeout/1` function has been added, which helps developers normalize durations and integers to a timeout used by Process APIs. For example, to send a message after one hour, one can now write:
```
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:
│
22 │ User.drive({:ok, %User{}}, car_choices)
│ ~
│
└─ lib/foo.ex:22:10: Example.run/0
```
> 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
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
case User.drive(user, car_choices) do
{:ok, car} -> car
:error -> Logger.error("User cannot drive")
end
```
## v1.17.0-dev
And here is the warning:
```
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:
│
26 │ :error -> Logger.error("User cannot drive")
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ lib/foo.ex:26: Example.run/0
```
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).
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
```
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
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 |
| `%{binary() => _}` | Object |
| `%{atom() => _}` | Object |
| `%{integer() => _}` | 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 ...
```
### Decoding
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.2 (2024-01-22)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.find/1` that mirrors `Enum.find/2`
* [Code] Support cursor inside fn/rescue/catch/else/after inside `Code.Fragment.container_cursor_to_quoted/2`
* [Date] Add `Date.shift/2` to shift dates with duration and calendar-specific semantics
* [Date] Allow `Date` to accept years outside of `-9999..9999` range
* [DateTime] Add `DateTime.shift/2` to shift datetimes with duration and calendar-specific semantics
* [Duration] Add a new `Duration` data type
* [GenServer] Add `c:GenServer.format_status/1` callback
* [Kernel] Add `Kernel.get_in/1` with safe nil-handling for access and structs
* [Kernel] Add `Kernel.is_non_struct_map/1` guard
* [Kernel] Add `Kernel.to_timeout/1`
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
* [Kernel] Support integers in uppercase sigils
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
* [Process] Add `Process.set_label/1`
* [String] Add `String.byte_slice/3` to slice a string to a maximum number of bytes while keeping it UTF-8 encoded
* [System] Support `use_stdio: false` in `System.cmd/3` and `System.shell/2`
* [Time] Add `Time.shift/2` to shift times with duration and calendar-specific semantics
#### ExUnit
* [ExUnit] Propagate the test process itself as a caller in `start_supervised`
* [ExUnit] Include max cases in ExUnit reports
#### IEx
* [IEx.Helpers] Warns if `recompile` was called and the current working directory changed
* [IEx.Helpers] Add `c/0` as an alias to `continue/0`
* [IEx.Pry] Add `IEx.Pry.annotate_quoted/3` to annotate a quoted expression with pry breakpoints
#### Logger
* [Logger] Format `:gen_statem` reports using Elixir data structures
* [Logger] Include process label in logger events
* [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
#### Mix
* [mix deps] Add `:depth` option to `Mix.SCM.Git`, thus supporting shallow clones of Git dependencies
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix] Print compilation lock waiting message to stderr
* [mix] Add an environment variable to optionally disable compilation locking
### 2. Bug fixes
#### Elixir
* [bin/elixir.bat] Improve handling of quotes and exclamation marks in flags
* [Code] Address a bug where AST nodes for `(a -> b)` were not wrapped as part of the literal encoder
* [Kernel] Resolve inconsistencies of how `..` and `...` are handled at the AST level
* [Kernel] Fix parsing precedence of ambiguous operators followed by containers
* [Kernel] Do not expand code in `quote bind_quoted: ...` twice
* [Kernel] Respect `:line` property when `:file` is given as option to `quote`
* [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
#### ExUnit
* [ExUnit] Fix crash when diffing bitstring specifiers
#### IEx
* [IEx.Helpers] Update the history size whenever history is pruned
* [IEx.Autocomplete] Fix crashing whhen autocompleting structs with runtime values
#### Mix
* [mix deps] Fix error message for diverged SCM definition in sibling
* [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
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
* [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
* [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead
* [Kernel] Single-quote charlists are deprecated, use `~c` instead
* [Kernel] Deprecate escaping closing delimiter in uppercase sigils
* [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead
* [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead
* [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
#### ExUnit
#### Mix
* [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons
* [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`
## v1.16
## v1.17
The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md).
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).
+29 -32
View File
@@ -2,10 +2,10 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL_MAKE := erl -make
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,11 +17,11 @@ 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 | tail -1) )
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.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
.PHONY: install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
.NOTPARALLEL:
#==> Functions
@@ -65,17 +65,21 @@ 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 $(APP) elixir
compile: erlang elixir
erlang: $(PARSER)
erlang: $(ELIXIR)
$(ELIXIR): $(PARSER) lib/elixir/src/*
$(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}' $<
@@ -83,29 +87,27 @@ $(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 lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
elixir: stdlib $(EEX) mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
$(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/security.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;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -144,6 +146,7 @@ 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/
@@ -154,16 +157,10 @@ 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: clean_man
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/
@@ -173,7 +170,9 @@ clean_residual_files:
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
$(Q) "$(MAKE)" clean_man
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
#==> Documentation tasks
@@ -305,17 +304,15 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1
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
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/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
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
clean_man:
rm -f man/elixir.1
+2 -1
View File
@@ -3,9 +3,10 @@ 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-2015
Copyright Ericsson AB 1996-2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+11 -2
View File
@@ -103,7 +103,9 @@ 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).
Additionally, you may choose to run the test suite with `make clean test`.
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`.
## Contributing
@@ -137,13 +139,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 (including Erlang modules):
To recompile all (including Erlang modules):
```sh
make compile
+9 -7
View File
@@ -2,21 +2,23 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
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", generate a new tag, and push it
4. Commit changes above with title "Release vVERSION" and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
5. Once GitHub actions completes, generate a new tag, and push it
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
6. Wait until GitHub Actions publish artifacts to the draft release
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
## Creating a new vMAJOR.MINOR branch (after first rc)
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)
### In the new branch
@@ -28,7 +30,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
2. Start new /CHANGELOG.md
+3 -4
View File
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.17 | Development
1.16 | Bug fixes and security patches
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.16 | Security patches only
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.17.0-dev
1.18.2
+3 -7
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.17.0-dev
ELIXIR_VERSION=1.18.2
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)
-v|--no-halt|--color|--no-color)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -182,10 +182,6 @@ 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))
+6 -17
View File
@@ -1,6 +1,6 @@
@echo off
set ELIXIR_VERSION=1.17.0-dev
set ELIXIR_VERSION=1.18.2
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
@@ -24,13 +24,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.
@@ -95,7 +95,6 @@ if "!par!"=="" (
shift
set par="!par:"=\"!"
rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
rem ******* ELIXIR PARAMETERS **********************
@@ -109,6 +108,8 @@ 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)
@@ -128,10 +129,6 @@ if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on
:run
setlocal disabledelayedexpansion
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 not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
)
@@ -139,17 +136,9 @@ if not defined useIEx (
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
if defined ELIXIR_CLI_DRY_RUN (
if defined useWerl (
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
if defined useWerl (
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
exit /B %ERRORLEVEL%
:end
+1 -4
View File
@@ -16,14 +16,11 @@ 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
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% %*
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
:end
endlocal
Regular → Executable
+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
+23 -10
View File
@@ -48,8 +48,14 @@ defmodule EEx.Compiler do
end
end
# TODO: Deprecate this on Elixir v1.18
# TODO: Remove me on Elixir v2.0
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}}
@@ -299,6 +305,14 @@ 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
@@ -316,8 +330,7 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
@@ -340,13 +353,6 @@ 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)
@@ -359,6 +365,13 @@ 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,15 +43,6 @@ 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
+12 -54
View File
@@ -126,51 +126,21 @@ defmodule EEx.TokenizerTest do
end
test "EEx comments" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
ExUnit.CaptureIO.capture_io(:stderr, fn ->
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
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}
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end)
end
test "EEx multi-line comments" do
@@ -321,15 +291,6 @@ 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}},
@@ -384,9 +345,6 @@ 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
|
+10 -9
View File
@@ -198,15 +198,6 @@ 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 %>")
@@ -543,6 +534,16 @@ 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,
%% warn_missing_spec,
%% warn_untyped_record,
%% Enable this when we require Erlang/OTP 27+
%% warnings_as_errors,
debug_info,
{outdir, "ebin/"}
]}.
+3 -3
View File
@@ -350,7 +350,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 Enum.at/1 or the functions in the List module\
you can use Enum.at/2 or the functions in the List module\
"""
end
@@ -418,8 +418,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 mostify a list element by index, \
you can Access.at/1 or the functions in the List module\
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\
"""
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
unless Module.has_attribute?(__MODULE__, :doc) do
if not 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, [any], GenServer.options()) :: on_start
@spec start_link(module, atom, [term], 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, [any], GenServer.options()) :: on_start
@spec start(module, atom, [term], 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) :: any
@spec get(agent, module, atom, [term], timeout) :: term
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) :: any
@spec get_and_update(agent, module, atom, [term], timeout) :: term
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end
+11 -12
View File
@@ -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`.
> #### Environment in libraries {: .tip}
> #### Application environment in libraries {: .info}
>
> 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.
> 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).
> #### Reading the environment of other applications {: .warning}
>
@@ -673,13 +673,6 @@ 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.
@@ -796,6 +789,12 @@ 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)
+2 -1
View File
@@ -9,7 +9,8 @@ defmodule Behaviour do
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used.
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
is documented in `Module`.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
+6 -12
View File
@@ -764,8 +764,9 @@ defmodule Date do
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 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.
@@ -1072,13 +1073,7 @@ defmodule Date do
def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
# 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
calendar.year_of_era(year, month, day)
end
@doc """
@@ -1158,12 +1153,11 @@ defmodule Date do
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when year in -9999..9999 do
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, year: year, month: month, day: day}, _)
when calendar == Calendar.ISO do
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
+22 -7
View File
@@ -28,7 +28,12 @@ defmodule DateTime do
## Time zone database
Many functions in this module require a time zone database.
By default, it uses the default time zone database returned by
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
`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}`
@@ -43,7 +48,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices
To use them, first make sure it is added as a dependency in `mix.exs`.
To use one of 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
@@ -1025,9 +1030,12 @@ 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
@@ -1341,6 +1349,11 @@ 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",
@@ -1708,8 +1721,9 @@ defmodule DateTime do
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 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.
@@ -2078,7 +2092,8 @@ defmodule DateTime do
)
case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
when calendar != Calendar.ISO or year in -9999..9999 ->
"~U[" <> formatted <> suffix(calendar) <> "]"
_ ->
+265 -9
View File
@@ -26,7 +26,7 @@ defmodule Duration do
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
It is important to notice that shifting is not an arithmetic operation.
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:
@@ -46,9 +46,19 @@ defmodule Duration do
* 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.
@@ -124,6 +134,8 @@ defmodule Duration do
"""
@type duration :: t | [unit_pair]
@microseconds_per_second 1_000_000
@doc """
Creates a new `Duration` struct from given `unit_pairs`.
@@ -181,9 +193,9 @@ defmodule Duration do
## Examples
iex> Duration.add(%Duration{week: 2, day: 1}, %Duration{day: 2})
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(%Duration{microsecond: {400, 3}}, %Duration{microsecond: {600, 6}})
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}
"""
@@ -211,9 +223,9 @@ defmodule Duration do
## Examples
iex> Duration.subtract(%Duration{week: 2, day: 1}, %Duration{day: 2})
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(%Duration{microsecond: {400, 6}}, %Duration{microsecond: {600, 3}})
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}
"""
@@ -239,9 +251,9 @@ defmodule Duration do
## Examples
iex> Duration.multiply(%Duration{day: 1, minute: 15, second: -10}, 3)
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(%Duration{microsecond: {200, 4}}, 3)
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}
"""
@@ -264,9 +276,9 @@ defmodule Duration do
## Examples
iex> Duration.negate(%Duration{day: 1, minute: 15, second: -10})
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(%Duration{microsecond: {500000, 4}})
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}
"""
@@ -283,4 +295,248 @@ defmodule Duration do
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
+85 -3
View File
@@ -18,7 +18,8 @@ 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.
ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times.
@@ -29,7 +30,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`). No functions exist to parse ISO 8601 durations or time intervals.
`10:01:25,5`).
#### Examples
@@ -663,6 +664,87 @@ 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.
@@ -1628,7 +1710,7 @@ defmodule Calendar.ISO do
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
microsecond: {0, _precision}
}) do
[
month: year * 12 + month,
+49 -29
View File
@@ -110,30 +110,12 @@ 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
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
utc_now(:native, calendar)
end
@doc """
@@ -158,7 +140,20 @@ 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
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
{: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)
end
@doc """
@@ -252,7 +247,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)
@@ -317,7 +312,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!(
@@ -578,8 +573,9 @@ defmodule NaiveDateTime do
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 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.
@@ -758,6 +754,9 @@ 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])
@@ -992,6 +991,8 @@ 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}})
@@ -1000,7 +1001,11 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
"""
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
@spec from_erl(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1019,13 +1024,19 @@ 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(), Calendar.calendar()) :: t
@spec from_erl!(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
@@ -1249,6 +1260,10 @@ 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,
@@ -1450,10 +1465,15 @@ defmodule NaiveDateTime do
calendar: calendar
} = naive_datetime
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
if calendar != Calendar.ISO or year in -9999..9999 do
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]"
"~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
end
defp suffix(Calendar.ISO), do: ""
+15 -12
View File
@@ -115,7 +115,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,13 +355,21 @@ 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(), Calendar.calendar()) ::
@spec from_erl(
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -377,6 +385,8 @@ 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})
@@ -521,7 +531,7 @@ defmodule Time do
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
unless valid? do
if not valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
@@ -797,15 +807,8 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
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
{:ok, value} = convert(time, calendar)
value
end
@doc """
+99 -38
View File
@@ -123,6 +123,11 @@ 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.
@@ -180,7 +185,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
@@ -244,8 +249,8 @@ defmodule Code do
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:relative_paths,
:warnings_as_errors
:infer_signatures,
:relative_paths
]
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@@ -507,14 +512,9 @@ defmodule Code do
## Options
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.
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.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -659,6 +659,8 @@ defmodule Code do
## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error
reporting
@@ -682,28 +684,34 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords.
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
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`,
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 `true`. This option changes the AST.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`.
Defaults to `true`. This option changes the AST.
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.
## Design principles
The formatter was designed under three principles.
First, the formatter never changes the semantics of the code.
First, the formatter never changes the semantics of the code by default.
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
@@ -937,9 +945,8 @@ defmodule Code do
## Code comments
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 handles code comments and guarantees 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
@@ -951,9 +958,19 @@ defmodule Code do
# world
hello
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
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
fn
arg1 ->
@@ -982,6 +999,21 @@ 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
@@ -1075,6 +1107,8 @@ 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)
@@ -1528,8 +1562,8 @@ defmodule Code do
## Examples
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
Code.compiler_options(infer_signatures: false)
#=> %{infer_signatures: true}
"""
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1558,6 +1592,12 @@ 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.
@@ -1586,16 +1626,19 @@ defmodule Code do
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. Defaults to `true`.
This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
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
`: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.
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.
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.
* `: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.
@@ -1604,13 +1647,19 @@ 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
@@ -1656,6 +1705,18 @@ 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, " <>
@@ -1685,7 +1746,7 @@ defmodule Code do
:ok
end
# TODO: Make this option have no effect on Elixir v2.0
# TODO: Remove this option on Elixir v2.0
# TODO: Warn if mode is :warn on Elixir v1.19
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
:elixir_config.put(:on_undefined_variable, value)
+159 -65
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,30 +31,37 @@ 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 on operands when they are the parent
# 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
@required_parens_on_binary_operands [
:|>,
:<<<,
:>>>,
:|>,
:<~,
:~>,
:<<~,
:~>>,
:<~>,
:"<|>",
:"^^^",
:+++,
:---,
:in,
:"^^^",
:"//",
:++,
:--,
:..,
:<>
:+++,
:---,
:<>,
:..
]
@locals_without_parens [
@@ -182,8 +189,10 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
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)
syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils =
@@ -208,8 +217,9 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
}
end
@@ -266,7 +276,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(:variable, state.inspect_opts), state}
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -397,7 +407,7 @@ defmodule Code.Formatter do
{@double_heredoc
|> concat(string)
|> concat(@double_heredoc)
|> color(:string, state.inspect_opts)
|> color_doc(:string, state.inspect_opts)
|> force_unfit(), state}
else
string = escape_string(string, @double_quote)
@@ -405,7 +415,7 @@ defmodule Code.Formatter do
{@double_quote
|> concat(string)
|> concat(@double_quote)
|> color(:string, state.inspect_opts), state}
|> color_doc(:string, state.inspect_opts), state}
end
end
@@ -437,7 +447,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color("nil", nil, state.inspect_opts), state}
{color_doc("nil", nil, state.inspect_opts), state}
end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -454,7 +464,7 @@ defmodule Code.Formatter do
end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color(:atom, state.inspect_opts), state}
|> color_doc(:atom, state.inspect_opts), state}
end
# &1
@@ -477,6 +487,53 @@ 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
@@ -492,7 +549,7 @@ defmodule Code.Formatter do
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
@@ -524,20 +581,12 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do
{left, state} =
case left_arg do
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
{:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), 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
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
state}
{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -561,8 +610,11 @@ 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), state}
{inspect(unknown, printable_limit: :infinity), state}
end
## Blocks
@@ -632,7 +684,7 @@ defmodule Code.Formatter do
Atom.to_string(op)
end
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
@@ -738,11 +790,11 @@ defmodule Code.Formatter do
doc =
cond do
op in @no_space_binary_operators ->
op_doc = color(op_string, :operator, state.inspect_opts)
op_doc = color_doc(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators ->
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
op_doc = color_doc(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true ->
@@ -752,7 +804,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(" " <> op_string, :operator, state.inspect_opts)
op_doc = color_doc(" " <> 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))
@@ -789,14 +841,13 @@ defmodule Code.Formatter do
op_string = Atom.to_string(op)
cond do
# 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) ->
# 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 ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the parent requires parens or the precedence is inverted or
# it is in the wrong side, then we *need* parenthesis.
# 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.
(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
@@ -953,7 +1004,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)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -1000,7 +1051,9 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state)
fun_doc =
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1068,7 +1121,7 @@ defmodule Code.Formatter do
fun
|> Atom.to_string()
|> string()
|> color(:call, state.inspect_opts)
|> color_doc(:call, state.inspect_opts)
|> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1431,7 +1484,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} =
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1509,8 +1562,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color("]", :list, state.inspect_opts)
left_bracket = color_doc("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state}
end
@@ -1542,8 +1595,8 @@ defmodule Code.Formatter do
end
defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
end
defp tuple_to_algebra(meta, args, join, state) do
@@ -1553,8 +1606,8 @@ defmodule Code.Formatter do
{args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color("}", :tuple, state.inspect_opts)
left_bracket = color_doc("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts)
if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1564,16 +1617,11 @@ defmodule Code.Formatter do
end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color(:boolean, inspect_opts)
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
end
defp atom_to_algebra(nil, _, inspect_opts) do
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)
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
end
defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1586,7 +1634,7 @@ defmodule Code.Formatter do
_ -> ":\\\\"
end
string(string) |> color(:atom, inspect_opts)
string(string) |> color_doc(:atom, inspect_opts)
end
defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1599,7 +1647,7 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
@@ -1616,7 +1664,7 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color(:number, inspect_otps)
|> color_doc(:number, inspect_otps)
end
defp float_to_algebra(text, inspect_otps) do
@@ -1624,7 +1672,7 @@ defmodule Code.Formatter do
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color(string, :number, inspect_otps)
color_doc(string, :number, inspect_otps)
end
defp insert_underscores("-" <> digits) do
@@ -2389,6 +2437,10 @@ 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.
@@ -2434,7 +2486,7 @@ defmodule Code.Formatter do
end
defp get_charlist_quotes(:heredoc, state) do
if state.normalize_charlists_as_sigils do
if state.migrate_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc}
else
{@single_heredoc, @single_heredoc}
@@ -2443,7 +2495,7 @@ defmodule Code.Formatter do
defp get_charlist_quotes({:regular, chunks}, state) do
cond do
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
!state.migrate_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
@@ -2452,4 +2504,46 @@ 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
+144 -28
View File
@@ -320,6 +320,9 @@ defmodule Code.Fragment do
_ ->
{{:local_or_var, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
@@ -362,6 +365,14 @@ 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
@@ -562,13 +573,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, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function returns keywords as `{:keyword, 'do'}`
* This function returns keywords as `{:keyword, ~c"do"}`
* This function never returns `:expr`
@@ -590,7 +601,9 @@ defmodule Code.Fragment do
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist},
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
@@ -640,7 +653,16 @@ defmodule Code.Fragment do
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{rest, _} = strip_spaces(rest, 0)
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
@@ -656,6 +678,9 @@ 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)
@@ -683,6 +708,9 @@ 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
@@ -715,6 +743,16 @@ 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)
@@ -1061,6 +1099,13 @@ 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.
@@ -1083,46 +1128,117 @@ 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 = [cursor_completion: true, emit_warnings: false] ++ opts
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
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 = :lists.reverse(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)
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} ->
{:ok, ast}
# 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
{:error, error} ->
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
tokens =
:lists.reverse(
rev_tokens,
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
Enum.map(rev_terminators, fn tuple ->
{line, column, info} = elem(tuple, 1)
put_elem(tuple, 1, {line, column + 5, info})
end)
)
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, ast} -> {:ok, ast}
{:error, _} -> {:error, error}
end
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
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
+61 -29
View File
@@ -71,11 +71,11 @@ defmodule Code.Identifier do
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: iolist(), remaining :: binary()}
{escaped :: binary(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
when (is_binary(binary) and ((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,64 +83,96 @@ defmodule Code.Identifier do
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
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
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0), do: [?\\, ?0]
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
@escaped_bom :binary.bin_to_list("\\uFEFF")
defp escape_char(65279), do: @escaped_bom
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
defp escape_char(char)
# 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)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<char::utf8>>
<<acc::binary, char::utf8>>
end
defp escape_char(char) when char < 0x100 do
defp escape_char(char, acc) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
[?\\, ?x, to_hex(a), to_hex(b)]
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
end
defp escape_char(char) when char < 0x10000 do
defp escape_char(char, acc) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
end
defp escape_char(char) when char < 0x1000000 do
defp escape_char(char, acc) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
<<acc::binary, ?\\, ?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}
+43 -30
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
@@ -126,13 +126,13 @@ 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
defp do_normalize({:., meta, [left, right]}, state) do
# it should not be wrapped. However, it should be escaped if applicable.
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) 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
@@ -174,16 +174,17 @@ defmodule Code.Normalizer do
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
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]}
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, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
else
_ ->
normalize_call(quoted, state)
end
@@ -288,21 +289,18 @@ 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
list =
# It's a charlist, we normalize it as a ~C sigil
string =
if state.escape do
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist)
else
list
List.to_string(list)
end
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
{:__block__, meta, [list]}
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
else
meta =
if line = state.parent_meta[:line] do
@@ -349,14 +347,14 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
Keyword.has_key?(meta, :do) ->
# 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]
line = state.parent_meta[:line] || meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
@@ -366,11 +364,20 @@ defmodule Code.Normalizer do
last_args =
case last_arg do
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
{:__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]
end
[] ->
@@ -384,6 +391,12 @@ 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)
-9
View File
@@ -27,11 +27,6 @@ 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
@@ -317,10 +312,6 @@ 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
+28 -2
View File
@@ -41,7 +41,8 @@ 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, read our [library guidelines](library-guidelines.md).
> current project. For more information, see ["Using application configuration for
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
## Migrating from `use Mix.Config`
@@ -143,7 +144,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
unless Keyword.keyword?(opts) do
if not Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
@@ -194,6 +195,31 @@ 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.
+12 -7
View File
@@ -215,10 +215,7 @@ defmodule DynamicSupervisor do
extra_arguments: [term()]
}
@typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@typedoc "Options given to `start_link/1` and `init/1` functions"
@type init_option ::
{:strategy, strategy()}
| {:max_restarts, non_neg_integer()}
@@ -277,7 +274,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor
unless Module.has_attribute?(__MODULE__, :doc) do
if not Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -345,9 +342,11 @@ 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([option | init_option]) :: Supervisor.on_start()
@spec start_link([init_option | GenServer.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)
@@ -381,9 +380,15 @@ 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, [option]) :: Supervisor.on_start()
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end
+136 -30
View File
@@ -837,7 +837,7 @@ defmodule Enum do
end
def dedup(enumerable) do
Enum.reduce(enumerable, [], fn x, acc ->
reduce(enumerable, [], fn x, acc ->
case acc do
[^x | _] -> acc
_ -> [x | acc]
@@ -1257,6 +1257,14 @@ 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
@@ -1266,6 +1274,7 @@ 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
@@ -1273,6 +1282,8 @@ 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
@@ -1538,7 +1549,7 @@ defmodule Enum do
do: Map.merge(collectable, :maps.from_list(enumerable))
defp into_map(enumerable, collectable),
do: Enum.reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
do: reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
defp into_protocol(enumerable, collectable) do
{initial, fun} = Collectable.into(collectable)
@@ -1581,7 +1592,7 @@ defmodule Enum do
"""
@spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t()
def into(enumerable, [], transform) do
Enum.map(enumerable, transform)
map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
@@ -1594,9 +1605,9 @@ defmodule Enum do
def into(enumerable, %{} = collectable, transform) do
if map_size(collectable) == 0 do
enumerable |> Enum.map(transform) |> :maps.from_list()
enumerable |> map(transform) |> :maps.from_list()
else
Enum.reduce(enumerable, collectable, fn entry, acc ->
reduce(enumerable, collectable, fn entry, acc ->
{key, val} = transform.(entry)
Map.put(acc, key, val)
end)
@@ -2416,13 +2427,17 @@ 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
@@ -2702,11 +2717,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 backward
# Slide a range of elements towards the head of the list
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 forward
# Slide a range of elements towards the tail of the list
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
[:a, :e, :f, :b, :c, :d, :g]
@@ -2793,7 +2808,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} =
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
{pre, post} =
cond do
index < start -> {[item | pre], post}
@@ -3338,7 +3353,7 @@ defmodule Enum do
def sort_by(enumerable, mapper, :desc) when is_function(mapper, 1) do
enumerable
|> Enum.reduce([], &[{&1, mapper.(&1)} | &2])
|> reduce([], &[{&1, mapper.(&1)} | &2])
|> List.keysort(1, :asc)
|> List.foldl([], &[elem(&1, 0) | &2])
end
@@ -3443,6 +3458,8 @@ 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])
@@ -3469,11 +3486,47 @@ 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([])
@@ -3490,6 +3543,40 @@ 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`.
@@ -3845,13 +3932,11 @@ defmodule Enum do
Returns the `enumerable` with each element wrapped in a tuple
alongside its index or according to a given function.
May receive a function or an integer offset.
If an integer offset is given as `fun_or_offset`, it will index from the given
offset instead of from zero.
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.
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.
## Examples
@@ -4051,7 +4136,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
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
R.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
end
@doc """
@@ -4086,7 +4171,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
Stream.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
R.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
end
## Helpers
@@ -4351,6 +4436,9 @@ 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
@@ -4492,8 +4580,16 @@ defmodule Enum do
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
enumerable |> fun.() |> slice_exact(start, amount, step, count)
# TODO: Deprecate me in Elixir v1.18.
{: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.
{: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
@@ -4503,10 +4599,6 @@ 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
@@ -4565,11 +4657,18 @@ 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: Deprecate me in Elixir v1.18.
# TODO: Remove me on v2.0
{: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
@@ -4580,9 +4679,6 @@ 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
@@ -4758,6 +4854,16 @@ 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: []
+132 -55
View File
@@ -49,7 +49,7 @@ defmodule Exception do
@doc """
Receives the exception struct and must return its message.
Most commonly exceptions have a message field which by default is accessed
Many 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 +346,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 +407,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
@@ -1212,7 +1212,7 @@ defmodule SyntaxError do
})
when not is_nil(snippet) and not is_nil(column) do
snippet =
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
format_message(file, line, column, snippet)
end
@@ -1225,7 +1225,7 @@ defmodule SyntaxError do
description: description
}) do
snippet =
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil)
:elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
padded = " " <> String.replace(snippet, "\n", "\n ")
format_message(file, line, column, padded)
@@ -1316,7 +1316,7 @@ defmodule TokenMissingError do
description: description
}) do
snippet =
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
format_message(file, line, column, snippet)
end
@@ -1581,13 +1581,15 @@ 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
{message, hint_type} = message(reason, module, function, arity)
message <> if(hint_type == :suggest_module, do: hint_for_missing_alias(module), else: "")
end
def message(%{message: message}) do
@@ -1648,12 +1650,25 @@ defmodule UndefinedFunctionError do
end
defp hint(module, function, arity, :suggest_function) do
behaviour_hint(module, function, arity) <>
hint_for_loaded_module(module, function, arity, nil)
hint_for_behaviour(module, function, arity) <>
hint_for_loaded_module(module, function, arity)
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.", "")
@@ -1674,9 +1689,9 @@ defmodule UndefinedFunctionError do
["\n * ", Exception.format_mfa(module, function, arity)]
end)
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
". Did you mean:\n#{suggestions}\n"
else
""
hint_for_missing_alias(module)
end
end
@@ -1693,7 +1708,7 @@ defmodule UndefinedFunctionError do
end
@doc false
def hint_for_loaded_module(module, function, arity, exports) do
def hint_for_loaded_module(module, function, arity) do
cond do
macro_exported?(module, function, arity) ->
". However, there is a macro with the same name and arity. " <>
@@ -1703,7 +1718,7 @@ defmodule UndefinedFunctionError do
", #{message}"
true ->
IO.iodata_to_binary(did_you_mean(module, function, exports))
IO.iodata_to_binary(did_you_mean(module, function))
end
end
@@ -1714,11 +1729,39 @@ defmodule UndefinedFunctionError do
end
end
@function_threshold 0.77
@max_suggestions 5
defp hint_for_behaviour(module, function, arity) do
case behaviours_for(module) do
[] ->
""
defp did_you_mean(module, function, exports) do
exports = exports || exports_for(module)
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)
result =
case Keyword.take(exports, [function]) do
@@ -1748,35 +1791,6 @@ 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)
@@ -1785,8 +1799,7 @@ 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
@@ -1797,8 +1810,7 @@ defmodule UndefinedFunctionError do
end
rescue
# In case the module was removed while we are computing this
UndefinedFunctionError ->
[]
UndefinedFunctionError -> []
end
end
@@ -1985,8 +1997,23 @@ defmodule Protocol.UndefinedError do
@impl true
def message(%{protocol: protocol, value: value, description: description}) do
"protocol #{inspect(protocol)} not implemented for #{inspect(value)} of type " <>
value_type(value) <> maybe_description(description) <> maybe_available(protocol)
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}
"""
end
defp value_type(%{__struct__: struct}), do: "#{inspect(struct)} (a struct)"
@@ -2131,6 +2158,56 @@ 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
+11 -11
View File
@@ -139,6 +139,8 @@ 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")
@@ -575,18 +577,14 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
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.
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 a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples
iex> Float.to_charlist(7.0)
@@ -594,7 +592,7 @@ defmodule Float do
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) when is_float(float) do
def to_charlist(float) do
:erlang.float_to_list(float, [:short])
end
@@ -614,6 +612,8 @@ 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,7 +621,7 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) when is_float(float) do
def to_string(float) do
:erlang.float_to_binary(float, [:short])
end
+2 -2
View File
@@ -46,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) ::
{:ok, state}
| {:ok, state, :hibernate}
| {:error, reason :: any}
when state: any
| {:error, reason :: term}
when state: term
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state}
+7 -7
View File
@@ -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 :: any}
when state: any
| {:stop, reason :: term}
when state: term
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
@@ -845,7 +845,7 @@ defmodule GenServer do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
unless Module.has_attribute?(__MODULE__, :doc) do
if not Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -945,7 +945,7 @@ defmodule GenServer do
end
defmacro __before_compile__(env) do
unless Module.defines?(env.module, {:init, 1}) do
if not 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 +1018,7 @@ defmodule GenServer do
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, any, options) :: on_start
@spec start_link(module, term, 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 +1028,7 @@ defmodule GenServer do
See `start_link/3` for more information.
"""
@spec start(module, any, options) :: on_start
@spec start(module, term, 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
+104 -92
View File
@@ -139,6 +139,85 @@ 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.
@@ -155,7 +234,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -175,7 +254,7 @@ defimpl Inspect, for: BitString do
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color(IO.iodata_to_binary(inspected), :string, opts)
color_doc(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
@@ -186,12 +265,12 @@ defimpl Inspect, for: BitString do
end
defp inspect_bitstring("", opts) do
color("<<>>", :binary, opts)
color_doc("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, opts) do
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
left = color_doc("<<", :binary, opts)
right = color_doc(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
@@ -228,7 +307,7 @@ end
defimpl Inspect, for: List do
def inspect([], opts) do
color("[]", :list, opts)
color_doc("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
@@ -259,9 +338,9 @@ defimpl Inspect, for: List do
lists
end
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
open = color_doc("[", :list, opts)
sep = color_doc(",", :list, opts)
close = color_doc("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
@@ -271,7 +350,7 @@ defimpl Inspect, for: List do
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
end
color(IO.iodata_to_binary(inspected), :charlist, opts)
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
@@ -283,7 +362,7 @@ defimpl Inspect, for: List do
@doc false
def keyword({key, value}, opts) do
key = color(Macro.inspect_atom(:key, key), :atom, opts)
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@@ -301,9 +380,9 @@ end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
open = color_doc("{", :tuple, opts)
sep = color_doc(",", :tuple, opts)
close = color_doc("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
@@ -322,7 +401,7 @@ defimpl Inspect, for: Map do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color(" => ", :map, opts)
sep = color_doc(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
@@ -339,9 +418,9 @@ defimpl Inspect, for: Map do
end
defp map_container_doc(list, name, opts, fun) do
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
open = color_doc("%" <> name <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
end
end
@@ -349,7 +428,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(inspected, :number, opts)
color_doc(inspected, :number, opts)
end
defp base_to_value(base) do
@@ -387,10 +466,10 @@ 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
:erlang.float_to_list(float, [:short])
Float.to_charlist(float)
end
color(IO.iodata_to_binary(formatted), :number, opts)
color_doc(IO.iodata_to_binary(formatted), :number, opts)
end
end
@@ -413,7 +492,7 @@ defimpl Inspect, for: Regex do
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color(source, :regex, opts)
color_doc(source, :regex, opts)
end
end
@@ -548,73 +627,6 @@ 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)}
@@ -636,9 +648,9 @@ defimpl Inspect, for: Any do
end
def inspect(map, name, infos, opts) do
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
+121 -71
View File
@@ -333,6 +333,7 @@ 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
@@ -438,7 +439,7 @@ defmodule Inspect.Algebra do
"""
@doc since: "1.6.0"
@spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(t, [term], 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
@@ -455,7 +456,7 @@ defmodule Inspect.Algebra do
container_each(collection, inspect_opts.limit, inspect_opts, fun, [], break == :maybe)
flex? = simple? or break == :flex
docs = fold_doc(docs, &join(&1, &2, flex?, separator))
docs = fold(docs, &join(&1, &2, flex?, separator))
case flex? do
true -> group(concat(concat(left, nest(docs, 1)), right))
@@ -525,6 +526,32 @@ 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 """
@@ -588,6 +615,21 @@ 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
@@ -604,21 +646,16 @@ defmodule Inspect.Algebra do
"""
@spec concat([t]) :: t
def concat(docs) when is_list(docs) do
fold_doc(docs, &concat(&1, &2))
fold(docs, &concat(&1, &2))
end
@doc ~S"""
Colors a document if the `color_key` has a color in the options.
Colors a document with the given color (preceding the document itself).
"""
@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
@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)
end
@doc ~S"""
@@ -918,6 +955,10 @@ 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.
@@ -929,21 +970,22 @@ defmodule Inspect.Algebra do
iex> docs = ["A", "B", "C"]
iex> docs =
...> Inspect.Algebra.fold_doc(docs, fn doc, acc ->
...> Inspect.Algebra.fold(docs, fn doc, acc ->
...> Inspect.Algebra.concat([doc, "!", acc])
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
"""
@spec fold_doc([t], (t, t -> t)) :: t
def fold_doc(docs, folder_fun)
@doc since: "1.18.0"
@spec fold([t], (t, t -> t)) :: t
def fold(docs, folder_fun)
def fold_doc([], _folder_fun), do: empty()
def fold_doc([doc], _folder_fun), do: doc
def fold([], _folder_fun), do: empty()
def fold([doc], _folder_fun), do: doc
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold_doc(docs, folder_fun))
def fold([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold(docs, folder_fun))
@doc ~S"""
Formats a given document for a given width.
@@ -973,20 +1015,17 @@ 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
) :: boolean()
) :: :fit | :no_fit | :break_next
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -995,63 +1034,68 @@ 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: false
defp fits?(_, _, _, []), do: true
defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t)
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)
## 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?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true
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
## Breaks
defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true
defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit
defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit
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: 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, _, _, [{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, 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,
@@ -1062,10 +1106,14 @@ 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: [ansi(c) | format(w, k, [{i, m, x} | 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, [{_, _, 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)
@@ -1073,7 +1121,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) do
if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do
[s | format(w, k, t)]
else
[indent(i) | format(w, i, t)]
@@ -1103,11 +1151,17 @@ 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
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])
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])
end
end
@@ -1136,10 +1190,6 @@ 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
+12 -9
View File
@@ -189,13 +189,16 @@ 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
do_digits(integer, base, [])
case integer do
0 -> [0]
_integer -> digits(integer, base, [])
end
end
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
defp digits(integer, base, acc),
do: digits(div(integer, base), base, [rem(integer, base) | acc])
@doc """
Returns the integer represented by the ordered `digits`.
@@ -217,16 +220,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
do_undigits(digits, base, 0)
undigits(digits, base, 0)
end
defp do_undigits([], _base, acc), do: acc
defp undigits([], _base, acc), do: acc
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
@doc """
Parses a text representation of an integer.
+22 -8
View File
@@ -12,6 +12,8 @@ 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,
@@ -131,9 +133,11 @@ 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.
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.
@@ -184,9 +188,11 @@ 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.
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.
@@ -290,6 +296,8 @@ 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!")
@@ -565,6 +573,8 @@ 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:
@@ -596,8 +606,9 @@ 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.
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".
This reads from the IO as UTF-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
@@ -650,14 +661,17 @@ 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. This reads from the IO device as a raw binary.
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.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
Finally, do not use this function on IO devices in Unicode
mode as it will return the wrong result.
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.
`binstream/0` has been introduced in Elixir v1.12.0,
while `binstream/2` has been available since v1.0.0.
+1 -1
View File
@@ -350,7 +350,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
unless no_wrap, do: newline_after_block(options)
if !no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
+410
View File
@@ -0,0 +1,410 @@
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
:elixir_json.encode_map(value, encoder)
end
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 |
| `%{binary() => _}` | Object |
| `%{atom() => _}` | Object |
| `%{integer() => _}` | 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: :elixir_json.encode_map(value, encoder)
def protocol_encode(value, encoder),
do: JSON.Encoder.encode(value, encoder)
end
+125 -111
View File
@@ -238,7 +238,7 @@ defmodule Kernel do
When comparing two numbers of different types (a number being either
an integer or a float), a conversion to the type with greater precision
will always occur, unless the comparison operator used is either `===/2`
or `!==`. A float will be considered more precise than an integer, unless
or `!==/2`. A float will be considered more precise than an integer, unless
the float is greater/less than +/-9007199254740992.0 respectively,
at which point all the significant figures of the float are to the left
of the decimal point. This behavior exists so that the comparison of large
@@ -247,9 +247,7 @@ defmodule Kernel do
The collection types are compared using the following rules:
* Tuples are compared by size, then element by element.
* Maps are compared by size, then by keys in ascending term order,
then by values in key order. In the specific case of maps' key
ordering, integers are always considered to be less than floats.
* Maps are compared by size, then by key-value pairs.
* Lists are compared element by element.
* Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.
* Atoms are compared using their string value, codepoint by codepoint.
@@ -302,7 +300,7 @@ defmodule Kernel do
@compile {:inline, bootstrapped?: 1}
case :code.ensure_loaded(Kernel) do
{:module, _} ->
defp bootstrapped?(_), do: true
defp bootstrapped?(module), do: is_atom(module)
{:error, _} ->
defp bootstrapped?(module), do: :code.ensure_loaded(module) == {:module, module}
@@ -630,7 +628,7 @@ defmodule Kernel do
"""
@doc guard: true
@spec hd(nonempty_maybe_improper_list(elem, any)) :: elem when elem: term
@spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term
def hd(list) do
:erlang.hd(list)
end
@@ -1598,7 +1596,7 @@ defmodule Kernel do
This function is optimized so the complexity of `a -- b` is proportional
to `length(a) * log(length(b))`. See also the [Erlang efficiency
guide](https://www.erlang.org/doc/efficiency_guide/retired_myths.html).
guide](https://www.erlang.org/doc/system/efficiency_guide.html).
Inlined by the compiler.
@@ -1905,8 +1903,8 @@ defmodule Kernel do
## Implemented in Elixir
defp optimize_boolean({:case, meta, args}) do
{:case, [{:optimize_boolean, true} | meta], args}
defp annotate_case(extra, {:case, meta, args}) do
{:case, extra ++ meta, args}
end
@doc """
@@ -1973,7 +1971,8 @@ defmodule Kernel do
end
defp build_boolean_check(operator, check, true_clause, false_clause) do
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(check) do
false -> unquote(false_clause)
@@ -2006,7 +2005,8 @@ defmodule Kernel do
defmacro !{:!, _, [value]} do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
@@ -2019,7 +2019,8 @@ defmodule Kernel do
defmacro !value do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(value) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
@@ -2217,28 +2218,22 @@ defmodule Kernel do
case Macro.expand(message, __CALLER__) do
message when is_binary(message) ->
quote do
:erlang.error(
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
)
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
end
{:<<>>, _, _} = message ->
quote do
:erlang.error(
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
)
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
end
alias when is_atom(alias) ->
quote do
:erlang.error(:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace)))
:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace))
end
message ->
quote do
:erlang.error(
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
)
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
end
end
end
@@ -2452,8 +2447,6 @@ defmodule Kernel do
* when updating a struct, as in `struct!(%SomeStruct{}, key: :value)`,
it is equivalent to `%SomeStruct{struct | key: :value}` and therefore this
function will check if every given key-value belongs to the struct.
However, updating structs does not enforce keys, as keys are enforced
only when building;
"""
@spec struct!(module | struct, Enumerable.t()) :: struct
@@ -2973,7 +2966,7 @@ defmodule Kernel do
(term | nil -> {current_value, new_value} | :pop)
) :: {current_value, new_structure :: structure}
when structure: Access.t(),
keys: nonempty_list(any),
keys: nonempty_list(term),
current_value: Access.value(),
new_value: Access.value()
def get_and_update_in(data, keys, fun)
@@ -3313,7 +3306,7 @@ defmodule Kernel do
end
defp nest_pop_in(:map, h, [{:access, key}]) do
quote do
quote generated: true do
case unquote(h) do
nil -> {nil, nil}
h -> Access.pop(h, unquote(key))
@@ -3334,7 +3327,7 @@ defmodule Kernel do
end
defp nest_pop_in(_, h, [{:access, key}]) do
quote do
quote generated: true do
case unquote(h) do
nil -> :pop
h -> Access.pop(h, unquote(key))
@@ -3424,7 +3417,7 @@ defmodule Kernel do
## Examples
iex> is_nil(1)
iex> is_nil(1 + 2)
false
iex> is_nil(nil)
@@ -3649,7 +3642,7 @@ defmodule Kernel do
:ok
true ->
pos = :elixir_locals.cache_env(__CALLER__)
pos = :elixir_module.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
@@ -3916,7 +3909,8 @@ defmodule Kernel do
end
defp build_if(condition, do: do_clause, else: else_clause) do
optimize_boolean(
annotate_case(
[optimize_boolean: true, type_check: :expr],
quote do
case unquote(condition) do
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
@@ -3956,6 +3950,8 @@ defmodule Kernel do
"Math still works"
"""
# TODO: Deprecate this on Elixir v1.22.
@doc deprecated: "Use if/2 instead"
defmacro unless(condition, clauses) do
build_unless(condition, clauses)
end
@@ -4070,7 +4066,7 @@ defmodule Kernel do
end
defp stepless_range(nil, first, last, _caller) do
quote(do: Elixir.Range.new(unquote(first), unquote(last)))
quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last))))
end
defp stepless_range(:guard, first, last, caller) do
@@ -4232,15 +4228,18 @@ defmodule Kernel do
defmacro left && right do
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
x
_ ->
unquote(right)
_ ->
unquote(right)
end
end
end
)
end
@doc """
@@ -4272,15 +4271,18 @@ defmodule Kernel do
defmacro left || right do
assert_no_match_or_guard_scope(__CALLER__.context, "||")
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
annotate_case(
[type_check: :expr],
quote do
case unquote(left) do
x when :"Elixir.Kernel".in(x, [false, nil]) ->
unquote(right)
x ->
x
x ->
x
end
end
end
)
end
@doc """
@@ -4582,6 +4584,9 @@ defmodule Kernel do
defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context),
do: fun.(var)
defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var),
do: fun.(var)
defp in_var(true, ast, fun) do
quote do
var = unquote(ast)
@@ -5008,12 +5013,14 @@ defmodule Kernel do
assert_no_match_or_guard_scope(env.context, "defmodule/2")
expanded = expand_module_alias(alias, env)
module_meta = module_meta(alias)
{expanded, with_alias} =
case is_atom(expanded) do
true ->
{full, old, opts} = alias_defmodule(alias, expanded, env)
# Expand the module considering the current environment/nesting
meta = [defined: full] ++ alias_meta(alias)
meta = [defined: full] ++ module_meta
{full, {:require, meta, [old, opts]}}
false ->
@@ -5045,6 +5052,7 @@ defmodule Kernel do
unquote(with_alias)
:elixir_module.compile(
unquote(module_meta),
unquote(expanded),
unquote(escaped),
unquote(module_vars),
@@ -5054,8 +5062,8 @@ defmodule Kernel do
end
end
defp alias_meta({:__aliases__, meta, _}), do: meta
defp alias_meta(_), do: []
defp module_meta({_, meta, _}), do: meta
defp module_meta(_), do: []
# We don't want to trace :alias_reference since we are defining the alias
defp expand_module_alias({:__aliases__, meta, list} = alias, env) do
@@ -5314,7 +5322,7 @@ defmodule Kernel do
key
end
pos = :elixir_locals.cache_env(env)
pos = :elixir_module.cache_env(env)
quote do
:elixir_def.store_definition(unquote(kind), unquote(store), unquote(pos))
@@ -5326,13 +5334,10 @@ defmodule Kernel do
A struct is a tagged map that allows developers to provide
default values for keys, tags to be used in polymorphic
dispatches and compile time assertions. For more information
about structs, please check `%/2`.
dispatches and compile time assertions.
It is only possible to define a struct per module, as the
struct is tied to the module itself. Calling `defstruct/1`
also defines a `__struct__/0` function that returns the
struct itself.
struct is tied to the module itself.
## Examples
@@ -5366,6 +5371,13 @@ defmodule Kernel do
defstruct [:title, :content, :author]
end
Once a struct is defined, it is possible to create them as follows:
%Post{title: "Hello world!"}
For more information on creating, updating, and pattern matching on
structs, please check `%/2`.
## Deriving
Although structs are maps, by default structs do not implement
@@ -5395,12 +5407,12 @@ defmodule Kernel do
defstruct name: nil, age: nil
end
For each protocol in `@derive`, Elixir will assert the protocol has
been implemented for `Any`. If the `Any` implementation defines a
`__deriving__/3` callback, 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. For more information on
the `__deriving__/3` callback, see `Protocol.derive/3`.
For each protocol in `@derive`, 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. For more information, see
`Protocol.derive/3`.
## Enforcing keys
@@ -5452,7 +5464,7 @@ defmodule Kernel do
"""
defmacro defstruct(fields) do
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
{struct, derive, kv, body} =
{struct, derive, escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
case derive do
@@ -5460,7 +5472,7 @@ defmodule Kernel do
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
end
def __struct__(), do: @__struct__
def __struct__(), do: unquote(escaped_struct)
def __struct__(unquote(kv)), do: unquote(body)
Kernel.Utils.announce_struct(__MODULE__)
@@ -5547,27 +5559,8 @@ defmodule Kernel do
# it is the bootstrapped version or not.
Elixir.Kernel.@(impl(true))
# TODO: Change the implementation on v2.0 to simply call Kernel.struct!/2
def exception(args) when Kernel.is_list(args) do
struct = __struct__()
{valid, invalid} = Enum.split_with(args, fn {k, _} -> Map.has_key?(struct, k) end)
case invalid do
[] ->
:ok
_ ->
IO.warn(
"the following fields are unknown when raising " <>
"#{Kernel.inspect(__MODULE__)}: #{Kernel.inspect(invalid)}. " <>
"Please make sure to only give known fields when raising " <>
"or redefine #{Kernel.inspect(__MODULE__)}.exception/1 to " <>
"discard unknown fields. Future Elixir versions will raise on " <>
"unknown fields given to raise/2"
)
end
Kernel.struct!(struct, valid)
struct!(__MODULE__, args)
end
defoverridable exception: 1
@@ -5692,16 +5685,15 @@ defmodule Kernel do
end
@doc """
Generates a macro suitable for use in guard expressions.
Defines a macro suitable for use in guard expressions.
It raises at compile time if the definition uses expressions that aren't
allowed in guards, and otherwise creates a macro that can be used both inside
or outside guards.
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Note the convention in Elixir is to prefix all guards that return a boolean
with the `is_` prefix, such as `is_list/1`. If, however, the function/macro
returns a boolean and is not allowed in guards, it should have no prefix and
end with a question mark, such as `Keyword.keyword?/1`.
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
around boolean-returning guards.
## Example
@@ -5738,12 +5730,17 @@ defmodule Kernel do
end
@doc """
Generates a private macro suitable for use in guard expressions.
Defines a private macro suitable for use in guard expressions.
It raises at compile time if the definition uses expressions that aren't
allowed in guards, and otherwise creates a private macro that can be used
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a private macro that can be used
both inside or outside guards in the current module.
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
around boolean-returning guards.
Similar to `defmacrop/2`, `defguardp/1` must be defined before its use
in the current module.
"""
@@ -5761,8 +5758,11 @@ defmodule Kernel do
"only a single when clause is allowed"
{call, impls} ->
case Macro.decompose_call(call) do
{_name, args} ->
case decompose_args(call) do
:error ->
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
args ->
validate_variable_only_args!(call, args)
macro_definition =
@@ -5784,13 +5784,20 @@ defmodule Kernel do
Elixir.Kernel.@(doc(guard: true))
unquote(macro_definition)
end
_invalid_definition ->
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
end
end
end
defp decompose_args({name, _, args}) when is_atom(name) and is_atom(args), do: []
defp decompose_args({name, _, args}) when is_atom(name) and is_list(args), do: args
defp decompose_args({{:unquote, _, _}, _, args}) when is_atom(args), do: []
defp decompose_args({{:unquote, _, _}, _, args}) when is_list(args), do: args
defp decompose_args(_), do: :error
defp validate_variable_only_args!(call, args) do
Enum.each(args, fn
{ref, _meta, context} when is_atom(ref) and is_atom(context) ->
@@ -6197,9 +6204,8 @@ defmodule Kernel do
"""
@doc since: "1.17.0"
@spec to_timeout([component, ...] | timeout() | Duration.t()) :: timeout()
when component: [{unit, non_neg_integer()}, ...],
unit: :week | :day | :hour | :minute | :second | :millisecond
@spec to_timeout([{unit, non_neg_integer()}] | timeout() | Duration.t()) :: timeout()
when unit: :week | :day | :hour | :minute | :second | :millisecond
def to_timeout(duration)
def to_timeout(:infinity), do: :infinity
@@ -6431,29 +6437,37 @@ defmodule Kernel do
"""
defmacro sigil_r(term, modifiers)
defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
binary = :elixir_interpolation.unescape_string(string, &regex_unescape_map/1)
regex = Regex.compile!(binary, :binary.list_to_bin(options))
Macro.escape(regex)
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
binary = :elixir_interpolation.unescape_string(binary, &regex_unescape_map/1)
compile_regex(binary, options)
end
defmacro sigil_r({:<<>>, meta, pieces}, options) do
binary = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
tuple = {:<<>>, meta, unescape_tokens(pieces, &regex_unescape_map/1)}
compile_regex(tuple, options)
end
defp regex_unescape_map(:newline), do: true
defp regex_unescape_map(_), do: false
@doc false
defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do
defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
IO.warn(
"~R/.../ is deprecated, use ~r/.../ instead",
Macro.Env.stacktrace(__CALLER__)
)
regex = Regex.compile!(string, :binary.list_to_bin(options))
Macro.escape(regex)
compile_regex(binary, options)
end
defp compile_regex(binary_or_tuple, options) do
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
true ->
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
false ->
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
end
end
@doc ~S"""
@@ -6691,7 +6705,7 @@ defmodule Kernel do
defp to_calendar_struct(type, fields) do
quote do
%{unquote_splicing([__struct__: type] ++ fields)}
%unquote(type){unquote_splicing(fields)}
end
end
+17 -11
View File
@@ -9,6 +9,7 @@ defmodule Kernel.CLI do
compile: [],
no_halt: false,
compiler_options: [],
warnings_as_errors: false,
errors: [],
verbose_compile: false,
profile: nil,
@@ -195,7 +196,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_locals] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@@ -294,6 +295,16 @@ 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, true)
parse_argv(t, config)
end
## Compiler
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
@@ -315,8 +326,7 @@ defmodule Kernel.CLI do
end
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
parse_argv(t, %{config | compiler_options: compiler_options})
parse_argv(t, %{config | warnings_as_errors: true})
end
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
@@ -499,15 +509,11 @@ defmodule Kernel.CLI do
]
end
profile_opts =
if config.profile do
[profile: config.profile]
else
[]
end
output = IO.chardata_to_string(config.output)
opts = verbose_opts ++ profile_opts
opts =
verbose_opts ++
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
{:ok, _, _} -> :ok
+41 -26
View File
@@ -8,6 +8,7 @@ defmodule Kernel.LexicalTracker do
@moduledoc false
@timeout :infinity
@behaviour :gen_server
@warn_key 0
@doc """
Returns all references in this lexical scope.
@@ -16,6 +17,24 @@ 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.
@@ -39,11 +58,6 @@ 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})
@@ -93,16 +107,12 @@ defmodule Kernel.LexicalTracker do
@doc false
def collect_unused_imports(pid) do
unused(pid, :unused_imports)
:gen_server.call(pid, :unused_imports, @timeout)
end
@doc false
def collect_unused_aliases(pid) do
unused(pid, :unused_aliases)
end
defp unused(pid, tag) do
:gen_server.call(pid, tag, @timeout)
:gen_server.call(pid, :unused_aliases, @timeout)
end
# Callbacks
@@ -123,11 +133,17 @@ defmodule Kernel.LexicalTracker do
@doc false
def handle_call(:unused_aliases, _from, state) do
{:reply, Enum.sort(state.aliases), state}
aliases = for {alias, meta} when is_list(meta) <- state.aliases, do: {alias, meta}
{:reply, Enum.sort(aliases), state}
end
def handle_call(:unused_imports, _from, state) do
{:reply, Enum.sort(state.imports), state}
imports =
for {module, %{@warn_key => _} = map} <- state.imports do
{module, Map.delete(map, @warn_key)}
end
{:reply, Enum.sort(imports), state}
end
def handle_call(:references, _from, state) do
@@ -171,8 +187,8 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | imports: imports, references: references}}
end
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
def handle_cast({:alias_dispatch, module}, state) do
{:noreply, put_in(state.aliases[module], :used)}
end
def handle_cast({:import_quoted, module, function, arities}, state) do
@@ -210,20 +226,19 @@ defmodule Kernel.LexicalTracker do
end
def handle_cast({:add_import, module, fas, meta, warn}, state) do
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}
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)}}
end
end
def handle_cast({:add_alias, module, meta, warn}, state) do
if warn do
{:noreply, put_in(state.aliases[module], meta)}
else
{:noreply, state}
end
def handle_cast({:warn_import, module}, state) do
{:noreply, put_in(state.imports[module][@warn_key], true)}
end
@doc false
+48 -44
View File
@@ -31,11 +31,11 @@ defmodule Kernel.ParallelCompiler do
dest = :erlang.get(:elixir_compiler_dest)
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
{_parent, checker} = Module.ParallelChecker.get()
{_parent, cache} = Module.ParallelChecker.get()
task =
Task.async(fn ->
Module.ParallelChecker.put(compiler_pid, checker)
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)
@@ -158,6 +158,10 @@ 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()) ::
@@ -204,6 +208,10 @@ 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()) ::
@@ -239,36 +247,26 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
schedulers = max(:erlang.system_info(:schedulers_online), 2)
{:ok, checker} = Module.ParallelChecker.start_link(schedulers)
schedulers =
Keyword.get_lazy(options, :max_concurrency, fn ->
max(:erlang.system_info(:schedulers_online), 2)
end)
{:ok, cache} = Module.ParallelChecker.start_link(schedulers)
{status, modules_or_errors, info} =
try do
outcome = spawn_workers(schedulers, checker, files, output, options)
{outcome, Code.get_compiler_option(:warnings_as_errors)}
spawn_workers(schedulers, cache, files, output, options)
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}, _} ->
{: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} ->
{:error, errors, info}
after
Module.ParallelChecker.stop(checker)
Module.ParallelChecker.stop(cache)
end
# TODO: Require this to be set from Elixir v1.19
@@ -353,32 +351,33 @@ defmodule Kernel.ParallelCompiler do
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_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
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
)
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
defp profile(%{profile: :none}, _what, fun), do: fun.()
defp profile(%{profile: {:time, _, _}}, what, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
what = if is_binary(what), do: what, else: what.()
IO.puts(:stderr, "[profile] Finished #{what} 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
@@ -414,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: checker} = state
%{output: output, dest: dest, checker: cache} = state
parent = self()
file = Path.expand(file)
{pid, ref} =
:erlang.spawn_monitor(fn ->
Module.ParallelChecker.put(parent, checker)
Module.ParallelChecker.put(parent, cache)
:erlang.put(:elixir_compiler_info, {parent, self()})
:erlang.put(:elixir_compiler_file, file)
@@ -462,7 +461,10 @@ defmodule Kernel.ParallelCompiler do
end)
[] = files
cycle_return = each_cycle_return(state.each_cycle.())
cycle_return =
profile(state, "cycle resolution", fn -> each_cycle_return(state.each_cycle.()) end)
state = cycle_timing(result, state)
case cycle_return do
@@ -506,13 +508,15 @@ 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.
waiting_list = Map.to_list(waiting)
deadlocked =
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
profile(state, "deadlock resolution", fn ->
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)
if deadlocked do
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
+23 -16
View File
@@ -112,26 +112,24 @@ 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"}
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.
An update operation specific for structs is also available:
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.
%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}
## Pattern matching on struct names
@@ -1048,7 +1046,7 @@ defmodule Kernel.SpecialForms do
end
end
require Hygiene
require ContextHygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
@@ -1541,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
@@ -1663,7 +1661,7 @@ defmodule Kernel.SpecialForms do
...> end
{:error, :wrong_data}
The `else` block works like a `case` clause: it can have multiple clauses,
The `else` block works like a `case`: 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.
@@ -1788,6 +1786,15 @@ 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
+32 -39
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
unless :ets.member(set, {kind, name, arity}) do
if not :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_locals.get_cached_env(pos),
:elixir_module.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_locals.get_cached_env(pos)
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
case type_to_signature(expr) do
{name, arity} = type_pair ->
@@ -290,8 +290,9 @@ defmodule Kernel.Typespec do
:lists.filter(fun, types)
end
defp translate_type({kind, {:"::", _, [{name, _, args}, definition]}, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
when is_list(meta) do
caller = :elixir_module.get_cached_env(pos)
state = clean_local_state(state)
args =
@@ -320,7 +321,7 @@ defmodule Kernel.Typespec do
invalid_args = :lists.filter(&(not valid_variable_ast?(&1)), args)
unless invalid_args == [] do
if invalid_args != [] do
invalid_args = :lists.join(", ", :lists.map(&Macro.to_string/1, invalid_args))
message =
@@ -335,7 +336,7 @@ defmodule Kernel.Typespec do
IO.warn(message, caller)
end
{{kind, {name, arity}, caller.line, type, export}, state}
{{kind, {name, arity}, meta, type, export}, state}
end
defp valid_variable_ast?({variable_name, _, context})
@@ -348,17 +349,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_locals.get_cached_env(pos)
caller = :elixir_module.get_cached_env(pos)
translate_spec(kind, spec, guard, caller, state)
end
defp translate_spec({kind, spec, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
caller = :elixir_module.get_cached_env(pos)
translate_spec(kind, spec, [], caller, state)
end
defp translate_spec(kind, {:"::", meta, [{name, _, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" do
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
translate_spec(kind, meta, name, args, return, guard, caller, state)
end
@@ -380,7 +381,7 @@ defmodule Kernel.Typespec do
ensure_no_defaults!(args)
state = clean_local_state(state)
unless Keyword.keyword?(guard) do
if not Keyword.keyword?(guard) do
error = "expected keywords as guard in type specification, got: #{Macro.to_string(guard)}"
compile_error(caller, error)
end
@@ -401,7 +402,7 @@ defmodule Kernel.Typespec do
ensure_no_unused_local_vars!(caller, state.local_vars)
arity = length(args)
{{kind, {name, arity}, caller.line, spec}, state}
{{kind, {name, arity}, meta, spec}, state}
end
# TODO: Remove char_list type by v2.0
@@ -567,27 +568,23 @@ 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 =
module
|> Macro.struct!(caller)
|> Map.delete(:__struct__)
|> Map.to_list()
struct_info = Macro.struct_info!(module, caller)
unless Keyword.keyword?(fields) do
if not Keyword.keyword?(fields) do
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
end
types =
:lists.map(
fn
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
end,
:lists.sort(struct)
struct_info
)
fun = fn {field, _} ->
unless Keyword.has_key?(struct, field) do
if not Enum.any?(struct_info, &(&1.field == field)) do
compile_error(
caller,
"undefined field #{inspect(field)} on struct #{inspect(module)}"
@@ -596,7 +593,7 @@ defmodule Kernel.Typespec do
end
:lists.foreach(fun, fields)
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
_ ->
compile_error(
@@ -630,7 +627,7 @@ defmodule Kernel.Typespec do
)
fun = fn {field, _} ->
unless Keyword.has_key?(fields, field) do
if not Keyword.has_key?(fields, field) do
compile_error(caller, "undefined field #{field} on record #{inspect(tag)}")
end
end
@@ -671,14 +668,7 @@ 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)
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, location(meta), :fun, fun_args}, state}
{{:type, location(meta), :fun, [args, spec]}, state}
end
# Handle type operator
@@ -754,7 +744,7 @@ defmodule Kernel.Typespec do
) do
remote = Module.get_attribute(caller.module, attr)
unless is_atom(remote) and remote != nil do
if not (is_atom(remote) and remote != nil) do
message =
"invalid remote in typespec: #{Macro.to_string(orig)} (@#{attr} is #{inspect(remote)})"
@@ -948,7 +938,7 @@ defmodule Kernel.Typespec do
## Helpers
# This is a backport of Macro.expand/2 because we want to expand
# This is a modified 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
@@ -956,9 +946,12 @@ defmodule Kernel.Typespec do
receiver
[head | tail] ->
case Macro.expand_once(head, env) do
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
_ -> alias
case Macro.expand(head, env) do
head when is_atom(head) ->
:elixir_aliases.concat([head | tail])
_ ->
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
end
end
end
+21 -18
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, :__struct__) do
if :ets.member(set, {:elixir, :struct}) do
raise ArgumentError,
"defstruct has already been called for " <>
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
@@ -119,6 +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)
rescue
@@ -129,6 +131,7 @@ defmodule Kernel.Utils do
end
key when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
{key, nil}
other ->
@@ -163,23 +166,24 @@ defmodule Kernel.Utils do
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
body =
case bootstrapped? do
true ->
case enforce_keys do
[] ->
quote do
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
quote line: 0, generated: true do
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote do
quote line: 0, generated: true do
{map, keys} =
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
{%{map | key => val}, List.delete(keys, key)}
end)
@@ -197,10 +201,10 @@ defmodule Kernel.Utils do
end
false ->
quote do
quote line: 0, generated: true do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
@__struct__,
unquote(escaped_struct),
kv
)
end
@@ -208,17 +212,13 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ attribute is during expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# The complete metadata goes into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
%{field: key, default: val}
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), quote(do: kv), body}
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
error_keys ->
raise ArgumentError,
@@ -330,8 +330,11 @@ 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)))
true ->
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
end
end
end
@@ -396,8 +399,8 @@ defmodule Kernel.Utils do
end
end
defp literal_quote(ast) do
{:quote, [], [[do: ast]]}
defp literal_quote(ast, args) do
{:quote, [], [args, [do: ast]]}
end
defp literal_unquote(ast) do
+31 -1
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 the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
to keywords and how they compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
guide.
## Examples
@@ -960,6 +960,36 @@ 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.
+57 -46
View File
@@ -633,27 +633,39 @@ defmodule List do
[other]
end
@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]
@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.
def zip([]), do: []
def zip(list_of_lists) when is_list(list_of_lists), do: do_zip(list_of_lists, [])
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
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.
@@ -924,6 +936,34 @@ 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.
@@ -1372,33 +1412,4 @@ 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
:erlang.float_to_list(term, [:short])
Float.to_charlist(term)
end
end
+267 -66
View File
@@ -146,8 +146,6 @@ 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)
@@ -159,10 +157,18 @@ defmodule Macro do
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 `;`)
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`
* `: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.
@@ -495,10 +501,8 @@ 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)
@@ -778,23 +782,23 @@ defmodule Macro do
## Options
* `:unquote` - when true, this function leaves `unquote/1` and
`unquote_splicing/1` statements unescaped, effectively unquoting
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions 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 metadata from escaped AST
* `:prune_metadata` - when `true`, removes most 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, `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,
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,
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` and `:no_parens`.
amount of metadata is kept, such as `:line`, `:no_parens` and `:delimiter`.
## Comparison to `quote/2`
@@ -816,7 +820,8 @@ defmodule Macro do
iex> value = {:a, :b, :c}
iex> quote do: unquote(value)
{:a, :b, :c}
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
Did you forget to escape term using Macro.escape/1?
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
@@ -829,30 +834,36 @@ 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 """
Expands the struct given by `module` in the given `env`.
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
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 `CompileError` if the struct is not available.
From Elixir v1.12, calling this function also adds an export
dependency on the given struct.
It will raise `ArgumentError` if the struct is not available.
"""
@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
@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
end
@doc """
@@ -897,8 +908,8 @@ defmodule Macro do
defp find_invalid(bin) when is_binary(bin), do: nil
defp find_invalid(fun) when is_function(fun) do
unless Function.info(fun, :env) == {:env, []} and
Function.info(fun, :type) == {:type, :external} do
if Function.info(fun, :env) != {:env, []} or
Function.info(fun, :type) != {:type, :external} do
{:error, fun}
end
end
@@ -1127,6 +1138,10 @@ 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)))
@@ -1135,7 +1150,9 @@ defmodule Macro do
"""
@spec to_string(t()) :: String.t()
def to_string(tree) do
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
doc =
Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98)
IO.iodata_to_binary(doc)
end
@@ -1484,7 +1501,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)
@@ -1834,13 +1851,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) do
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match 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) do
when is_atom(atom) and is_atom(field) and env.context != :match do
if Map.has_key?(env, field) do
{maybe_escape_map(Map.get(env, field)), true}
else
@@ -1935,7 +1952,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
@@ -2138,9 +2155,8 @@ defmodule Macro do
@doc """
Converts the given argument to a string with the underscore-slash format.
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.
The argument must either be an atom, representing an Elixir module,
or a string representing a module without the `Elixir.` prefix.
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
@@ -2174,7 +2190,7 @@ defmodule Macro do
"Foo.Bar"
"""
@spec underscore(module() | atom() | String.t()) :: String.t()
@spec underscore(atom() | String.t()) :: String.t()
def underscore(atom_or_string)
def underscore(atom) when is_atom(atom) do
@@ -2318,6 +2334,15 @@ 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
@@ -2366,14 +2391,14 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
def inspect_atom(source_format, atom)
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
def inspect_atom(source_format, atom, opts \\ [])
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end
def inspect_atom(:literal, atom) when is_atom(atom) do
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2385,7 +2410,7 @@ defmodule Macro do
end
:quoted ->
{escaped, _} = Code.Identifier.escape(binary, ?")
escaped = inspect_atom_escape(opts, binary, ?")
IO.iodata_to_binary([?:, ?", escaped, ?"])
_ ->
@@ -2393,7 +2418,7 @@ defmodule Macro do
end
end
def inspect_atom(:key, atom) when is_atom(atom) do
def inspect_atom(:key, atom, opts) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2401,7 +2426,7 @@ defmodule Macro do
IO.iodata_to_binary([?", binary, ?", ?:])
:quoted ->
{escaped, _} = Code.Identifier.escape(binary, ?")
escaped = inspect_atom_escape(opts, binary, ?")
IO.iodata_to_binary([?", escaped, ?", ?:])
_ ->
@@ -2409,7 +2434,7 @@ defmodule Macro do
end
end
def inspect_atom(:remote_call, atom) when is_atom(atom) do
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
binary = Atom.to_string(atom)
case inner_classify(atom) do
@@ -2421,13 +2446,22 @@ defmodule Macro do
if type in [:not_callable, :alias] do
binary
else
elem(Code.Identifier.escape(binary, ?"), 0)
inspect_atom_escape(opts, binary, ?")
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
@@ -2454,7 +2488,7 @@ defmodule Macro do
#
defp inner_classify(atom) when is_atom(atom) do
cond do
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
:not_callable
# <|>, ^^^, and ~~~ are deprecated
@@ -2552,13 +2586,13 @@ defmodule Macro do
header = dbg_format_header(env)
quote do
to_debug = unquote(dbg_ast_to_debuggable(code))
to_debug = unquote(dbg_ast_to_debuggable(code, env))
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
end
end
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
@@ -2591,7 +2625,7 @@ defmodule Macro do
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
# Logic operators.
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast, _env)
when op in unquote(dbg_decomposed_binary_operators) do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
@@ -2599,11 +2633,22 @@ defmodule Macro do
quote do
unquote(acc_var) = []
unquote(dbg_boolean_tree(ast, acc_var, result_var))
{:logic_op, Enum.reverse(unquote(acc_var))}
{:logic_op, Enum.reverse(unquote(acc_var)), unquote(result_var)}
end
end
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
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
clauses_returning_index =
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
{:->, meta, [left, {right, index}]}
@@ -2621,7 +2666,7 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast, _env) do
modified_clauses =
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
hd(
@@ -2642,8 +2687,104 @@ 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) do
defp dbg_ast_to_debuggable(ast, _env) do
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
@@ -2674,6 +2815,18 @@ 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
@@ -2711,15 +2864,27 @@ defmodule Macro do
{[first_formatted | rest_formatted], result}
end
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
{_ast, final_value} = List.last(components)
defp dbg_format_ast_to_debug({:logic_op, components, value}, options) do
formatted =
Enum.map(components, fn {ast, value} ->
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
end)
{formatted, final_value}
{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}
end
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
@@ -2755,6 +2920,42 @@ 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
+25 -6
View File
@@ -102,8 +102,10 @@ defmodule Macro.Env do
]
# Define the __struct__ callbacks by hand for bootstrap reasons.
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(:elixir_quote.escape(struct, :none, false))
{_struct, [], escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(escaped_struct)
def __struct__(unquote(kv)), do: unquote(body)
@doc """
@@ -349,6 +351,11 @@ defmodule Macro.Env do
* #{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__
@@ -367,15 +374,27 @@ defmodule Macro.Env do
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) :: {:ok, t} | {:error, String.t()}
@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)
result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
maybe_define_error(result, :elixir_import)
end
@@ -426,7 +445,7 @@ defmodule Macro.Env do
maybe_define_error(result, :elixir_aliases)
end
defp maybe_define_error({:ok, env}, _mod),
defp maybe_define_error({:ok, _info, env}, _mod),
do: {:ok, env}
defp maybe_define_error({:error, reason}, mod),
@@ -583,7 +602,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, :elixir_env.env_to_ex(env))
quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
+13 -16
View File
@@ -96,11 +96,11 @@ defmodule Map do
iex> %{map | one: "one"}
%{one: "one", two: 2}
Or any other key:
Or any other keys:
iex> other_map = %{"three" => 3, "four" => 4}
iex> %{other_map | "three" => "three"}
%{"four" => 4, "three" => "three"}
iex> other_map = %{"three" => 3, "four" => 4, "five" => 5}
iex> %{other_map | "three" => "three", "four" => "four"}
%{"five" => 5, "four" => "four", "three" => "three"}
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
@@ -128,6 +128,8 @@ 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)
@@ -136,9 +138,7 @@ defmodule Map do
"""
@doc since: "1.14.0"
@spec from_keys([key], value) :: map
def from_keys(keys, value) do
:maps.from_keys(keys, value)
end
defdelegate from_keys(keys, value), to: :maps
@doc """
Returns all keys from `map`.
@@ -709,10 +709,10 @@ defmodule Map do
end
@doc """
Removes the value associated with `key` in `map` and returns the value
and the updated map, or it raises if `key` is not present.
Removes and returns the value associated with `key` in `map` alongside
the updated map, or raises if `key` is not present.
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
Behaves the same as `pop/3` but raises a `KeyError` exception if `key` is not present in `map`.
## Examples
@@ -1011,9 +1011,8 @@ defmodule Map do
@doc """
Converts a `struct` to map.
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.
It accepts a struct and simply removes the `__struct__` field
from the given struct.
## Example
@@ -1021,15 +1020,13 @@ 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(), (any() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
@spec split_with(MapSet.t(), (term() -> 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}}
+103 -70
View File
@@ -270,6 +270,9 @@ 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
@@ -441,7 +444,8 @@ 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
* `@callback` - provides a specification for a behaviour callback (and generates
a `behaviour_info/1` function in the module, see below)
* `@macrocallback` - provides a specification for a macro behaviour callback
* `@optional_callbacks` - specifies which behaviour callbacks and macro
behaviour callbacks are optional
@@ -590,6 +594,78 @@ 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}
@@ -628,7 +704,8 @@ defmodule Module do
@callback __info__(:macros) :: keyword()
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
@doc """
Returns information about module attributes used by Elixir.
@@ -669,6 +746,10 @@ 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`)."
@@ -753,50 +834,7 @@ defmodule Module do
:elixir_module.is_open(module)
end
@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
@deprecated "Use Code.eval_quoted/3 instead"
def eval_quoted(module_or_env, quoted, binding \\ [], opts \\ [])
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts)
@@ -827,7 +865,8 @@ defmodule Module do
the given quoted expressions.
The line where the module is defined and its file **must**
be passed as options.
be passed as options. See `Code.env_for_eval/1` for a complete
list of options.
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
@@ -871,7 +910,7 @@ defmodule Module do
end
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
unless Keyword.has_key?(opts, :file) do
if not Keyword.has_key?(opts, :file) do
raise ArgumentError, "expected :file to be given as option"
end
@@ -882,7 +921,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(module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
:elixir_module.compile(meta, module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
end
@doc """
@@ -1381,15 +1420,7 @@ 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)
case :elixir_def.take_definition(module, {name, arity}) do
false ->
false
_ ->
:elixir_locals.yank({name, arity}, module)
true
end
:elixir_def.take_definition(module, {name, arity}) != false
end
@doc """
@@ -1418,8 +1449,7 @@ defmodule Module do
"overridable because it was not defined"
clause ->
neighbours = :elixir_locals.yank(tuple, module)
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
:elixir_overridable.record_overridable(module, tuple, clause)
end
other ->
@@ -1643,11 +1673,11 @@ defmodule Module do
case :ets.lookup(set, key) do
[{_, _, :accumulate, traces}] ->
trace_attribute(true, module, traces, set, key, [])
trace_attribute(true, module, traces, set, key, nil, [])
reverse_values(:ets.take(bag, {:accumulate, key}), [])
[{_, value, _, traces}] ->
trace_attribute(module, traces)
trace_attribute(module, key, nil, traces)
:ets.delete(set, key)
value
@@ -1910,7 +1940,7 @@ defmodule Module do
_ ->
message =
case :ets.lookup(set, :__struct__) do
case :ets.lookup(set, {:elixir, :struct}) do
[] ->
"warning: module attribute @derive was set but never used (it must come before defstruct)"
@@ -1964,15 +1994,15 @@ defmodule Module do
default
[{_, _, :accumulate, traces}] ->
trace_attribute(trace?, module, traces, set, key, [])
trace_attribute(trace?, module, traces, set, key, caller_line, [])
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
trace_attribute(trace?, module, traces, set, key, caller_line, [{3, :used}])
value
[{_, value, _, traces}] ->
trace_attribute(trace?, module, traces, set, key, [])
trace_attribute(trace?, module, traces, set, key, caller_line, [])
value
[] when is_integer(caller_line) ->
@@ -2000,12 +2030,15 @@ defmodule Module do
end
end
defp trace_attribute(module, traces) do
defp trace_attribute(module, key, caller_line, 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
@@ -2022,10 +2055,10 @@ defmodule Module do
)
end
defp trace_attribute(trace?, module, traces, set, key, updates) do
defp trace_attribute(trace?, module, traces, set, key, caller_line, updates) do
updates =
if trace? and traces != [] do
trace_attribute(module, traces)
trace_attribute(module, key, caller_line, traces)
updates ++ [{4, []}]
else
updates
@@ -2229,7 +2262,7 @@ defmodule Module do
end
defp preprocess_attribute(:nifs, value) do
unless function_arity_list?(value) do
if not 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)}"
+22 -27
View File
@@ -4,7 +4,7 @@ defmodule Module.Behaviour do
@doc false
def callbacks(behaviour) do
for callback <- behaviour_info(behaviour, :callbacks) do
for callback <- behaviour.behaviour_info(:callbacks) do
{pair, _kind} = normalize_macro_or_function_callback(callback)
pair
end
@@ -33,9 +33,7 @@ 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
@@ -57,8 +55,8 @@ defmodule Module.Behaviour do
warn(context, {:module_does_not_define_behaviour, context.module, behaviour})
true ->
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
callbacks = behaviour_info(behaviour, :callbacks)
optional_callbacks = behaviour.behaviour_info(:optional_callbacks)
callbacks = behaviour.behaviour_info(:callbacks)
Enum.reduce(callbacks, context, &add_callback(&2, &1, behaviour, optional_callbacks))
end
end)
@@ -144,7 +142,7 @@ defmodule Module.Behaviour do
end
defp impl_behaviours({function, arity}, defaults, kind, value, behaviours, callbacks) do
impls = for n <- arity..(arity - defaults), do: {function, n}
impls = for n <- (arity - defaults)..arity, do: {function, n}
impl_behaviours(impls, kind, value, behaviours, callbacks)
end
@@ -266,13 +264,6 @@ 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
@@ -284,7 +275,11 @@ defmodule Module.Behaviour do
end
end
def format_warning({:undefined_behaviour, module, behaviour}) do
def format_diagnostic(warning) do
%{message: IO.iodata_to_binary(format_warning(warning))}
end
defp format_warning({:undefined_behaviour, module, behaviour}) do
[
"@behaviour ",
inspect(behaviour),
@@ -294,12 +289,12 @@ defmodule Module.Behaviour do
]
end
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do
defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
end
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
[
"the behaviour ",
inspect(behaviour),
@@ -311,7 +306,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
[
"conflicting behaviours found. Callback ",
format_definition(kind, callback),
@@ -325,7 +320,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:missing_callback, module, callback, kind, behaviour}) do
defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" is not implemented (in module ",
@@ -334,7 +329,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" was implemented as \"",
@@ -347,14 +342,14 @@ defmodule Module.Behaviour do
]
end
def format_warning({:private_function, callback, kind}) do
defp format_warning({:private_function, callback, kind}) do
[
format_definition(kind, callback),
" is private, @impl attribute is always discarded for private functions/macros"
]
end
def format_warning({:no_behaviours, callback, kind, value}) do
defp format_warning({:no_behaviours, callback, kind, value}) do
[
"got \"@impl ",
inspect(value),
@@ -364,7 +359,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
[
"got \"@impl false\" for ",
format_definition(kind, callback),
@@ -373,7 +368,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:impl_defined, callback, kind, callbacks}) do
defp format_warning({:impl_defined, callback, kind, callbacks}) do
[
"got \"@impl true\" for ",
format_definition(kind, callback),
@@ -382,7 +377,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -392,7 +387,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -403,7 +398,7 @@ defmodule Module.Behaviour do
]
end
def format_warning({:missing_impl, callback, kind, behaviour}) do
defp format_warning({:missing_impl, callback, kind, behaviour}) do
[
"module attribute @impl was not set for ",
format_definition(kind, callback),
-258
View File
@@ -1,258 +0,0 @@
# 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
+163 -176
View File
@@ -5,20 +5,19 @@ 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
:gen_server.start_link(__MODULE__, schedulers, [])
:proc_lib.start_link(__MODULE__, :init, [schedulers])
end
@doc """
Stops the parallel checker process.
"""
def stop(checker) do
def stop({checker, _table}) do
send(checker, {__MODULE__, :stop})
:ok
end
@@ -29,26 +28,30 @@ defmodule Module.ParallelChecker do
def get do
case :erlang.get(:elixir_checker_info) do
{parent, nil} ->
{:ok, checker} = start_link()
put(parent, checker)
{parent, checker}
{:ok, cache} = start_link()
put(parent, cache)
{parent, cache}
{parent, checker} ->
{parent, checker}
{parent, cache} ->
{parent, cache}
end
end
@doc """
Stores the parallel checker information.
"""
def put(pid, checker) when is_pid(pid) and is_pid(checker) do
:erlang.put(:elixir_checker_info, {pid, checker})
def put(pid, {checker, table}) when is_pid(pid) do
:erlang.put(:elixir_checker_info, {pid, {checker, table}})
end
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, checker}, module, info, log?) do
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
ref = make_ref()
spawned =
@@ -56,20 +59,25 @@ defmodule Module.ParallelChecker do
mon_ref = Process.monitor(pid)
receive do
{^ref, :cache, ets} ->
{^ref, :cache} ->
Process.link(pid)
module_map =
if is_map(info) do
info
else
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
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
end
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
@@ -78,8 +86,8 @@ defmodule Module.ParallelChecker do
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_map do
check_module(module_map, {checker, ets}, log?)
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
else
[]
end
@@ -113,12 +121,12 @@ defmodule Module.ParallelChecker do
case :erlang.get(:elixir_checker_info) do
{_, nil} -> :ok
{_, checker} -> verify(checker, [])
{_, cache} -> verify(cache, [])
end
result
after
{_, checker} = :erlang.get(:elixir_checker_info)
{_, cache} = :erlang.get(:elixir_checker_info)
if previous != :undefined do
:erlang.put(:elixir_checker_info, previous)
@@ -126,7 +134,7 @@ defmodule Module.ParallelChecker do
:erlang.erase(:elixir_checker_info)
end
checker && stop(checker)
cache && stop(cache)
end
_ ->
@@ -141,13 +149,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(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
spawn({self(), checker}, module, file, log?)
inner_spawn(self(), checker, table, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -181,20 +189,8 @@ defmodule Module.ParallelChecker do
Test cache.
"""
def test_cache do
{: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
{:ok, cache} = start_link()
cache
end
@doc """
@@ -203,74 +199,81 @@ defmodule Module.ParallelChecker do
or if the function does not exist return `{:error, :function}`.
"""
@spec fetch_export(cache(), module(), atom(), 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
{: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)
[{_key, false}] ->
{:error, :module}
[{_key, mode}] ->
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
case :ets.lookup(table, {module, {fun, arity}}) do
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
[] -> {:error, :function}
end
end
end
@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
## Module checking
defp check_module(module_map, cache, log?) do
%{
module: module,
file: file,
line: line,
compile_opts: compile_opts,
definitions: definitions,
uses_behaviours: uses_behaviours,
impls: impls
} = module_map
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
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,
uses_behaviours,
behaviours,
impls,
definitions
)
warnings =
diagnostics =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(log?)
|> emit_warnings(file, log?)
module_map
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
warnings
diagnostics
end
defp module_map_to_module_tuple(module_map) do
%{
module: module,
file: file,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls,
after_verify: after_verify
} = 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}
end
defp extract_no_warn_undefined(compile_opts) do
@@ -291,42 +294,60 @@ defmodule Module.ParallelChecker do
## Warning helpers
defp group_warnings(warnings) do
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()
{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)
end
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)
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)
diagnostics
end)
end
defp print_warning(message, [diagnostic]) do
:elixir_errors.print_warning(message, diagnostic)
defp print_diagnostics([diagnostic]) do
:elixir_errors.print_diagnostic(diagnostic, true)
end
defp print_warning(message, grouped_warnings) do
:elixir_errors.print_warning_group(message, grouped_warnings)
defp print_diagnostics(diagnostics) do
:elixir_errors.print_diagnostics(diagnostics)
end
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
file = Path.absname(file)
%{
severity: :warning,
source: file,
source: source,
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
@@ -351,15 +372,15 @@ defmodule Module.ParallelChecker do
## Cache
defp cache_module({server, ets}, module) do
if lock(server, module) do
defp cache_module({checker, table}, module) do
if lock(checker, 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(ets, module, contents.exports)
cache_chunk(table, module, contents.exports)
else
_ ->
# Otherwise, if the module is loaded, use its info
@@ -367,37 +388,28 @@ defmodule Module.ParallelChecker do
true ->
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(ets, module, exports, deprecated, mode)
cache_info(table, 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
exports = Map.new(Enum.map(exports, &{&1, :def}))
cache_info(ets, module, exports, %{}, :erlang)
cache_info(table, module, exports, %{}, %{}, :erlang)
else
_ ->
:ets.insert(ets, {{:cached, module}, false})
:ets.insert(table, {module, false})
end
end
end
unlock(server, module)
unlock(checker, module)
end
end
defp info_exports(module) do
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}
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
rescue
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
_ -> {:erlang, module.module_info(:exports)}
end
defp info_deprecated(module) do
@@ -406,70 +418,49 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
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
defp cache_from_module_map(table, map) do
exports =
[{{:__info__, 1}, :def}] ++
[{:__info__, 1}] ++
behaviour_exports(map) ++
definitions_to_exports(map.definitions)
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated, :elixir)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
module_map_to_module_tuple(map)
end
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}
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)})
end)
:ets.insert(ets, {{:cached, module}, mode})
:ets.insert(table, {module, mode})
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})
{{fun, arity}, kind}
defp cache_chunk(table, module, exports) do
Enum.each(exports, fn {{fun, arity}, info} ->
# TODO: Match on signature directly in Elixir v1.22+
:ets.insert(
table,
{{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)}
)
end)
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
:ets.insert(ets, {{:cached, module}, :elixir})
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(table, {module, :elixir})
end
defp behaviour_exports(%{defines_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
defp behaviour_exports(%{defines_behaviour: true}), do: [{:behaviour_info, 1}]
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}, :def}]
[{:behaviour_info, 1}]
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
@@ -485,22 +476,22 @@ defmodule Module.ParallelChecker do
## Server callbacks
def init(schedulers) do
ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
table = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
:proc_lib.init_ack({:ok, {self(), table}})
state = %{
ets: ets,
waiting: %{},
modules: [],
spawned: 0,
schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
}
{:ok, state}
:gen_server.enter_loop(__MODULE__, [], state)
end
def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
def handle_call(:start, _from, %{modules: modules} = state) do
for {pid, ref} <- modules do
send(pid, {ref, :cache, ets})
send(pid, {ref, :cache})
end
for {_pid, ref} <- modules do
@@ -512,14 +503,10 @@ 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 => froms} ->
waiting = Map.put(state.waiting, module, [from | froms])
%{^module => from_list} ->
waiting = Map.put(state.waiting, module, [from | from_list])
{:noreply, %{state | waiting: waiting}}
%{} ->
@@ -529,8 +516,8 @@ defmodule Module.ParallelChecker do
end
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
froms = Map.fetch!(waiting, module)
Enum.each(froms, &:gen_server.reply(&1, false))
from_list = Map.fetch!(waiting, module)
Enum.each(from_list, &:gen_server.reply(&1, false))
waiting = Map.delete(waiting, module)
{:reply, :ok, %{state | waiting: waiting}}
end
+355 -47
View File
@@ -1,48 +1,304 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
alias Module.Types.{Expr, Pattern}
# 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}
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
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
context = context()
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause}
false -> false
end
end
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
handler = &local_handler(&1, &2, &3, &4, finder)
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
try do
warnings_from_clause(meta, args, guards, body, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
error =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
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 error, __STACKTRACE__
end
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)
end)
context = warn_unused_clauses(defs, stack, context)
context.warnings
end
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> meta_file
:error -> file
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)
clauses_indexes =
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
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
defp local_handler(_meta, fun_arity, stack, context, finder) do
case context.local_sigs do
%{^fun_arity => {kind, inferred, _mapping}} ->
{kind, inferred, context}
%{^fun_arity => kind} when is_atom(kind) ->
{kind, :none, context}
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
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
# 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
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
:error -> stack
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
@@ -51,17 +307,9 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end
defp warnings_from_clause(meta, args, guards, body, stack, context) do
with {:ok, _types, context} <- Pattern.of_head(args, guards, meta, stack, context),
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
context.warnings
else
{:error, context} -> context.warnings
end
end
@doc false
def stack(file, module, function, no_warn_undefined, cache) do
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
when mode in @modes do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
@@ -71,12 +319,14 @@ defmodule Module.Types do
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
no_warn_undefined: no_warn_undefined,
# A list of cached modules received from the parallel compiler
# A tuple with cache information (may be nil)
cache: cache,
# If variable refinements is enabled or not
refine: true
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
}
end
@@ -85,8 +335,66 @@ defmodule Module.Types do
%{
# A list of all warnings found so far
warnings: [],
# Information about all vars and their types
vars: %{}
# 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: %{}
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
end
defp restore_context(%{vars: vars, failed: failed}, later_context) do
%{later_context | vars: vars, failed: failed}
end
## Diagnostics
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
}
end
## Module errors
def format_error({:unused_args, {name, arity}}),
do: "default values for the optional arguments in #{name}/#{arity} are never used"
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"
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"
def format_error({:unused_def, {name, arity}, :defp}),
do: "function #{name}/#{arity} is unused"
def format_error({:unused_def, {name, arity}, :defmacrop}),
do: "macro #{name}/#{arity} is unused"
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})"
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_error({:incorrect_dispatch, {f, a}, _module}),
do: "cannot invoke macro #{f}/#{a} before its definition"
end
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+190 -47
View File
@@ -2,6 +2,8 @@ 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.
"""
@@ -14,6 +16,37 @@ defmodule Module.Types.Helpers do
end
end
@doc """
Unpacks a list into head elements and tail.
"""
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]
@doc """
Returns the AST metadata.
"""
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Attaches span information.
"""
def with_span(meta, name) do
:elixir_env.calculate_span(meta, name)
end
## Warnings
@doc """
Formatted hints in typing errors.
"""
@@ -41,43 +74,161 @@ defmodule Module.Types.Helpers do
"""
#{hint()} when you rescue without specifying exception names, \
the variable is assigned a type of a struct but all of its fields are unknown
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
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
@doc """
Format previously collected traces.
"""
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
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]
end
@doc """
Formats a var for pretty printing.
"""
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)})"
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
defp expr_hints(_), do: []
@doc """
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
"""
def expr_to_string(expr) do
expr
|> reverse_rewrite()
|> Macro.to_string()
end
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
|> Macro.prewalk(fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
|> Macro.to_string()
end
defp erl_to_ex(
:erlang,
:error,
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
meta
) do
{:raise, meta, [expr]}
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}
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
end
end
@doc """
Returns the AST metadata.
"""
def get_meta({_, meta, _}), do: meta
def get_meta(_other), do: []
@doc """
Indents new lines.
"""
@@ -86,51 +237,43 @@ defmodule Module.Types.Helpers do
end
@doc """
Emits a warnings.
Emits a warning.
"""
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
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]}
end
@doc """
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
Emits an error.
In practice an error is a warning that halts other errors from being collected.
"""
def reduce_ok(list, acc, fun) do
do_reduce_ok(list, acc, fun)
end
def error(module, warning, meta, stack, context) do
case context do
%{failed: true} ->
context
defp do_reduce_ok([head | tail], acc, fun) do
case fun.(head, acc) do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
{:error, reason} ->
{:error, reason}
%{failed: false} ->
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, %{context | failed: true})
end
end
end
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
@doc """
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
and stops on `{:error, reason}`.
The type to return when there is an error.
"""
def map_reduce_ok(list, acc, fun) do
do_map_reduce_ok(list, {[], acc}, fun)
end
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 do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
def error_type, do: Module.Types.Descr.dynamic()
end
+295 -461
View File
@@ -2,8 +2,6 @@ defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
alias Module.ParallelChecker
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@@ -26,6 +24,23 @@ defmodule Module.Types.Of do
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.
"""
@@ -47,8 +62,8 @@ defmodule Module.Types.Of do
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error,
warn(__MODULE__, {:refine_var, old_type, type, var, context}, meta, stack, context)}
{:error, error_type(),
error({:refine_var, old_type, type, var, context}, meta, stack, context)}
else
{:ok, new_type, context}
end
@@ -80,65 +95,94 @@ defmodule Module.Types.Of do
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
{_optional?, value_type} ->
{:ok, value_type, context}
{value_type, context}
reason ->
{:ok, dynamic(),
warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
{error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
end
end
@doc """
Builds a closed map.
"""
def closed_map(pairs, extra \\ [], stack, context, of_fun) do
result =
reduce_ok(pairs, {true, extra, [], context}, fn
{key, value}, {closed?, single, multiple, context} ->
with {:ok, keys, context} <- of_finite_key_type(key, stack, context, of_fun),
{:ok, value_type, context} <- of_fun.(value, stack, context) do
case keys do
:none ->
{:ok, {false, single, multiple, context}}
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)
end)
end
[key] when multiple == [] ->
{:ok, {closed?, [{key, value_type} | single], multiple, context}}
@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)
keys ->
{:ok, {closed?, single, [{keys, value_type} | multiple], context}}
end
{dynamic(), context}
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)
case keys do
:none ->
fallback = union(fallback, value_type)
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
{dynamic?, fallback, [], [], assert, context}
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
end
end)
with {:ok, {closed?, single, multiple, context}} <- result do
map =
case Enum.reverse(multiple) do
[] ->
pairs = Enum.reverse(single)
if closed?, do: closed_map(pairs), else: open_map(pairs)
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
pairs = Enum.reverse(single, [{key, type} | t])
if closed?, do: closed_map(pairs), else: open_map(pairs)
end
|> Enum.reduce(&union/2)
end
{:ok, map, context}
end
end
defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{:ok, [key], context}
end
defp of_finite_key_type(key, stack, context, of_fun) do
with {:ok, key_type, context} <- of_fun.(key, stack, context) do
case atom_fetch(key_type) do
{:finite, list} -> {:ok, list, context}
_ -> {:ok, :none, context}
[{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)
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
@@ -153,45 +197,53 @@ defmodule Module.Types.Of do
end
@doc """
Handles structs creation.
Handles instantiation of a new struct.
"""
def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun)
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.
with {:ok, args_types, context} <-
map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) ->
with {:ok, type, context} <- of_fun.(value, stack, context) do
{:ok, {key, type}, context}
end
end) do
struct(struct, args_types, default_handling, meta, stack, context)
{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 """
Struct handling assuming the args have already been converted.
Builds a type from the struct info.
"""
# TODO: Allow structs fields to be defined. If the fields are defined,
# then the struct is no longer dynamic. And we need to validate args
# against the struct types.
# TODO: Use the struct default values to define the default types.
def struct(struct, args_types, default_handling, meta, stack, context) do
context = remote(struct, :__struct__, 0, meta, stack, context)
def struct_type(struct, info, args_types \\ []) do
term = term()
defaults =
for %{field: field} <- struct.__info__(:struct), field != :__struct__ do
{field, term}
end
pairs =
case default_handling do
:merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types
:skip_defaults -> [{:__struct__, atom([struct])} | args_types]
:only_defaults -> [{:__struct__, atom([struct])} | defaults]
end
{:ok, dynamic(closed_map(pairs)), context}
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)
end
## Binary
@@ -202,211 +254,122 @@ 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, _of_fun) do
{:ok, context}
def binary([], _kind, _stack, context) do
context
end
def binary([head], kind, stack, context, of_fun) do
binary_segment(head, kind, [head], stack, context, of_fun)
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
end
def binary([head | tail], kind, stack, context, of_fun) do
case binary_segment(head, kind, [head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
{:error, reason} -> {:error, reason}
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
defp binary_many([last], kind, stack, context, of_fun) do
binary_segment(last, kind, [@prefix, last], stack, context, of_fun)
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, of_fun) do
case binary_segment(head, kind, [@prefix, head, @suffix], stack, context, of_fun) do
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
{:error, reason} -> {:error, reason}
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, _of_fun)
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
when is_binary(left) or is_number(left) do
{:ok, context}
context
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context, of_fun) do
expected_type = specifier_info(kind, right)
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
expr = {:<<>>, meta, args}
with {:ok, _type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do
{:ok, context}
end
{_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_info(kind, {:-, _, [left, _right]}), do: specifier_info(kind, left)
defp specifier_info(:expr, {:float, _, _}), do: @integer_or_float
defp specifier_info(:expr, {:utf8, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf16, _, _}), do: @integer_or_binary
defp specifier_info(:expr, {:utf32, _, _}), do: @integer_or_binary
defp specifier_info(:pattern, {:utf8, _, _}), do: @integer
defp specifier_info(:pattern, {:utf16, _, _}), do: @integer
defp specifier_info(:pattern, {:utf32, _, _}), do: @integer
defp specifier_info(:pattern, {:float, _, _}), do: @float
defp specifier_info(_kind, {:integer, _, _}), do: @integer
defp specifier_info(_kind, {:bits, _, _}), do: @binary
defp specifier_info(_kind, {:bitstring, _, _}), do: @binary
defp specifier_info(_kind, {:bytes, _, _}), do: @binary
defp specifier_info(_kind, {:binary, _, _}), do: @binary
defp specifier_info(_kind, _specifier), do: @integer
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
## Apply
def apply(:erlang, name, [left, right], expr, stack, context)
when name in [:>=, :"=<", :>, :<, :min, :max] do
result = if name in [:min, :max], do: union(left, right), else: boolean()
cond do
match?({false, _}, map_fetch(left, :__struct__)) or
match?({false, _}, map_fetch(right, :__struct__)) ->
warning = {:struct_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
number_type?(left) and number_type?(right) ->
{:ok, result, context}
empty?(intersection(left, right)) ->
warning = {:mismatched_comparison, expr, context}
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
true ->
{:ok, result, context}
end
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
end
def apply(mod, name, args, expr, stack, context) do
case :elixir_rewrite.inline(mod, name, length(args)) do
{mod, name} -> apply(mod, name, args, expr, stack, context)
false -> {:ok, dynamic(), 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
## Remote
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
def remote(type, fun, arity, hints \\ [], expr, meta, stack, context) do
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} ->
context =
Enum.reduce(mods, context, fn mod, context ->
remote(mod, fun, arity, meta, stack, context)
end)
{mods, context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], warn(warning, meta, stack, context)}
{[], error(warning, meta, stack, context)}
end
end
@doc """
Checks a module is a valid remote.
"""
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
if Keyword.get(meta, :runtime_module, false) do
context
else
ParallelChecker.preload_module(stack.cache, module)
check_export(module, fun, arity, meta, stack, context)
end
end
defp check_export(module, fun, arity, meta, stack, context) do
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
{:ok, mode, :def, reason} ->
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
{:ok, mode, :defmacro, reason} ->
context = warn({:unrequired_module, module, fun, arity}, meta, stack, context)
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, stack) do
warn({:undefined_module, module, fun, arity}, meta, stack, context)
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, stack) do
exports = ParallelChecker.all_exports(stack.cache, module)
warn({:undefined_function, module, fun, arity, exports}, meta, stack, context)
else
context
end
end
end
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
if reason do
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> :string.titlecase()
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
{: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({:deprecated, module, fun, arity, reason}, meta, stack, context)
_ ->
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, _stack), do: false
defp warn_undefined?(_module, :module_info, 0, _stack), do: false
defp warn_undefined?(_module, :module_info, 1, _stack), do: false
defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false
defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, stack) do
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
## Warning helpers
@doc """
Intersects two types and emit an incompatible warning if empty.
Intersects two types and emit an incompatible error if empty.
"""
def intersect(actual, {expected, expr}, stack, context) do
def intersect(actual, expected, expr, stack, context) do
type = intersection(actual, expected)
if empty?(type) do
{:error, incompatible_warn(expr, expected, actual, stack, context)}
{error_type(), incompatible_error(expr, expected, actual, stack, context)}
else
{:ok, type, context}
{type, context}
end
end
@@ -416,259 +379,130 @@ defmodule Module.Types.Of do
This is a generic warning for when the expected/actual types
themselves may come from several different circumstances.
"""
def incompatible_warn(expr, expected_type, actual_type, stack, context) do
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}
warn(__MODULE__, warning, meta, stack, context)
error(warning, meta, stack, context)
end
defp warn(warning, meta, stack, context) do
warn(__MODULE__, warning, meta, stack, context)
defp error(warning, meta, stack, context) do
error(__MODULE__, warning, meta, stack, context)
end
## Traces
def format_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 => data} -> {:ok, Map.put(versions, version, data)}
%{} -> {:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
|> Enum.map(&format_trace/1)
end
defp format_trace(%{off_traces: []}) do
[]
end
defp format_trace(%{name: name, context: context, off_traces: traces}) do
traces =
traces
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
location =
file
|> Path.relative_to_cwd()
|> Exception.format_file_line(meta[:line])
|> String.replace_suffix(":", "")
[
"""
# type: #{to_quoted_string(type) |> indent(4)}
# from: #{location}
\
""",
case formatter do
:default -> [expr |> expr_to_string() |> indent(4), ?\n]
formatter -> formatter.(expr)
end,
format_hints(expr_hints(expr))
]
end)
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
end
defp format_var({var, _, context}), do: format_var(var, context)
defp format_var(var, nil), do: "\"#{var}\""
defp format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
defp expr_hints(_), do: []
## Warning formatting
def format_warning({:refine_var, old_type, new_type, var, context}) do
[
"""
incompatible types assigned to #{format_var(var)}:
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(var, context),
"\ntyping violation found at:"
]
%{
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)
])
}
end
def format_warning({:incompatible, expr, expected_type, actual_type, hints, context}) do
[
"""
incompatible types in expression:
def format_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
traces = collect_traces(expr, context)
#{expr_to_string(expr) |> indent(4)}
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in expression:
expected type:
#{expr_to_string(expr) |> indent(4)}
#{to_quoted_string(expected_type) |> indent(4)}
got type:
but got type:
#{to_quoted_string(actual_type) |> indent(4)}
#{to_quoted_string(actual_type) |> indent(4)}
""",
format_traces(expr, context),
format_hints(hints),
"\ntyping violation found at:"
]
but expected type:
#{to_quoted_string(expected_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
}
end
def format_warning({:badmap, expr, type, key, context}) do
[
"""
expected a map or struct when accessing .#{key} in expression:
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
but got type:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(expr, context),
format_hints([:dot]),
"\ntyping violation found at:"
]
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
end
def format_warning({:badkey, expr, type, key, context}) do
[
"""
unknown key .#{key} in expression:
def format_diagnostic({:badmap, expr, type, key, context}) do
traces = collect_traces(expr, context)
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a map or struct when accessing .#{key} in expression:
the given type does not have the given key:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(expr, context),
"\ntyping violation found at:"
]
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints([:dot])
])
}
end
def format_warning({:badmodule, expr, type, fun, arity, hints, context}) do
[
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
def format_diagnostic({:badkey, expr, type, key, context}) do
traces = collect_traces(expr, context)
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
%{
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:
but got type:
#{expr_to_string(expr) |> indent(4)}
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(expr, context),
format_hints(hints),
"\ntyping violation found at:"
]
end
the given type does not have the given key:
def format_warning({:undefined_module, module, fun, arity}) do
top =
if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}"
else
Exception.format_mfa(module, fun, arity)
end
[
top,
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_warning({:undefined_function, module, :__struct__, 0, _exports}) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
end
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_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
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
def format_warning({:mismatched_comparison, expr, context}) do
[
"""
comparison between incompatible types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(expr, context),
"""
While Elixir can compare across all types, you are comparing \
across types which are always distinct, and the result is either \
always true or always false
""",
"\ntyping violation found at:"
]
end
def format_warning({:struct_comparison, expr, context}) do
[
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(expr, context),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Struct that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison
""",
"\ntyping violation found at:"
]
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
defp dot_var?(expr) do
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -865,7 +865,13 @@ defmodule OptionParser do
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
"#{option} : Missing argument of type #{type}"
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
end
else
msg = "#{option} : Unknown option"
+111 -49
View File
@@ -96,7 +96,7 @@ defmodule PartitionSupervisor do
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
You can do start the dynamic supervisors under a `PartitionSupervisor`:
You can 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, DynamicSupervisor) do
case Keyword.get(opts, :name, PartitionSupervisor) 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]
unless name do
if !name do
raise ArgumentError, "the :name option must be given to PartitionSupervisor"
end
{child_spec, opts} = Keyword.pop(opts, :child_spec)
unless child_spec do
if !child_spec do
raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor"
end
{partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online())
unless is_integer(partitions) and partitions >= 1 do
if not (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)
unless is_function(with_arguments, 2) do
if not 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)
unless is_list(args) do
if not 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, name, partition]}
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
Map.merge(map, %{id: partition, start: start, modules: modules})
end
auto_shutdown = Keyword.get(opts, :auto_shutdown, :never)
unless auto_shutdown == :never do
if 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, name, partition) do
def start_child(mod, fun, args, partition) do
case apply(mod, fun, args) do
{:ok, pid} ->
register_child(name, partition, pid)
register_child(partition, pid)
{:ok, pid}
{:ok, pid, info} ->
register_child(name, partition, pid)
register_child(partition, pid)
{:ok, pid, info}
other ->
@@ -297,33 +297,97 @@ defmodule PartitionSupervisor do
end
end
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)
defp register_child(partition, pid) do
:ets.insert(Process.get(:ets_table), {partition, pid})
end
@impl true
def init({name, partitions, children, init_opts}) do
init_partitions(name, partitions)
table = init_table(name)
:ets.insert(table, {:partitions, partitions, partitions})
Process.put(:ets_table, table)
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
end
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})
defp init_table(name) when is_atom(name) do
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
end
defp init_partitions({:via, _, _}, partitions) do
child_spec = {Registry, keys: :unique, name: @registry}
defp init_table({:via, _, _}) do
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
ensure_registry()
Registry.register(@registry, self(), table)
table
end
unless Process.whereis(@registry) do
Supervisor.start_child(:elixir_sup, child_spec)
defp ensure_registry do
if Process.whereis(@registry) == nil do
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
end
end
Registry.register(@registry, self(), partitions)
@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)
end
@doc """
@@ -332,24 +396,27 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
{_name, partitions} = name_partitions(name)
partitions
name |> table() |> partitions(name)
end
# 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
defp partitions(table, name) do
try do
{name, :ets.lookup_element(name, :partitions, 2)}
:ets.lookup_element(table, :partitions, 2)
rescue
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
end
defp name_partitions(name) when is_tuple(name) do
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
with pid when is_pid(pid) <- GenServer.whereis(name),
[name_partitions] <- Registry.lookup(@registry, pid) do
name_partitions
[{_, table}] <- Registry.lookup(@registry, pid) do
table
else
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
@@ -374,7 +441,7 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec which_children(name()) :: [
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
Supervisor.which_children(name)
@@ -428,22 +495,17 @@ defmodule PartitionSupervisor do
@doc false
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
{name, partitions} = name_partitions(name)
table = table(name)
partitions = partitions(table, name)
if partitions == 0 do
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
end
partition =
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
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)
:ets.lookup_element(table, partition, 2)
end
@doc false
+3 -3
View File
@@ -884,7 +884,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 `cwd`.
* A symbolic link in the path points to something above the root of `relative_to`.
## Examples
@@ -906,10 +906,10 @@ defmodule Path do
"""
@doc since: "1.14.0"
@spec safe_relative(t, t) :: {:ok, binary} | :error
def safe_relative(path, cwd \\ File.cwd!()) do
def safe_relative(path, relative_to \\ File.cwd!()) do
path = IO.chardata_to_string(path)
case :filelib.safe_relative_path(path, cwd) do
case :filelib.safe_relative_path(path, relative_to) do
:unsafe -> :error
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
end
+24 -2
View File
@@ -88,6 +88,9 @@ 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.
"""
@@ -233,6 +236,12 @@ 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
@@ -299,7 +308,15 @@ 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
@@ -552,10 +569,15 @@ defmodule Process do
send(pid, {:ping, ref_and_alias})
receive do: msg -> msg
receive do: (msg -> msg)
#=> :pong
receive do: msg -> msg
ref_and_alias = Process.monitor(pid, alias: :reply_demonitor)
#=> #Reference<0.906660723.3006791681.40191>
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
#=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc}
"""
+114 -125
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`.
## A real case
## Example
In Elixir, we have two nouns for checking how many items there
are in a data structure: `length` and `size`. `length` means the
@@ -102,8 +102,7 @@ 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`
@@ -165,56 +164,16 @@ defmodule Protocol do
The `@spec` above expresses that all types allowed to implement the
given protocol are valid argument types for the given function.
## Reflection
## Configuration
Any protocol module contains three extra functions:
The following module attributes are available to configure a protocol:
* `__protocol__/1` - returns the protocol information. The function takes
one of the following atoms:
* `@fallback_to_any` - when true, enables protocol dispatch to
fallback to any
* `: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
* `@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
## Consolidation
@@ -222,7 +181,8 @@ 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.
consolidated protocol is equivalent to invoking two remote functions - one to
identify the correct implementation, and another to call the implementation.
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
@@ -268,6 +228,41 @@ 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)
@@ -380,52 +375,50 @@ defmodule Protocol do
@doc """
Derives the `protocol` for `module` with the given options.
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.
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.
## Examples
defprotocol Derivable do
def ok(arg)
end
@impl true
defmacro __deriving__(module, options) do
# If you need to load struct metadata, you may call:
# struct_info = Macro.struct_info!(module, __CALLER__)
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(Macro.escape(struct)), unquote(options)}
{:ok, arg, unquote(options)}
end
end
end
end
def ok(arg) do
{:ok, arg}
end
def ok(arg)
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}, %ImplStruct{a: 0, b: 0}, []}
#=> {:ok, %ImplStruct{a: 0, b: 0}, []}
Explicit derivations can now be called via `__deriving__/3`:
If the struct has already been defined, you can call this macro:
# 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}, %ImplStruct{a: 0, b: 0}, :oops}
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
"""
defmacro derive(protocol, module, options \\ []) do
@@ -497,7 +490,8 @@ defmodule Protocol do
defp extract_matching_by_attribute(paths, prefix, callback) do
for path <- paths,
path = to_charlist(path),
# Do not use protocols as they may be consolidating
path = if(is_list(path), do: path, else: String.to_charlist(path)),
file <- list_dir(path),
mod = extract_from_file(path, file, prefix, callback),
do: mod
@@ -699,25 +693,14 @@ 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,
defp: 1,
defp: 2,
defdelegate: 2,
defguard: 1,
defguardp: 1,
defmacro: 1,
defmacro: 2,
defmacrop: 1,
defmacrop: 2
]
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
# Import the new dsl that holds the new def
# Import the new `def` that is used by protocols
import Protocol, only: [def: 1]
# Compile with debug info for consolidation
@@ -736,19 +719,17 @@ defmodule Protocol do
end
end
defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos})
when kind in [:callback, :macrocallback] do
defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa(
{kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
)
when kind in [:callback, :macrocallback] do
{_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
) do
[{{name, length(List.wrap(args))}, meta}]
end
defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do
defp callback_ast_to_fa({_kind, _clause, _pos}) do
[]
end
@@ -766,13 +747,10 @@ defmodule Protocol do
end
defp warn(message, env, line) when is_integer(line) do
stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace()
IO.warn(message, stacktrace)
IO.warn(message, %{env | line: line})
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
@@ -783,8 +761,11 @@ 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.map(
:lists.foreach(
fn {name, arity} = fa ->
warn(
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -799,7 +780,7 @@ defmodule Protocol do
macrocallback_metas = callback_metas(env.module, :macrocallback)
macrocallbacks = :maps.keys(macrocallback_metas)
:lists.map(
:lists.foreach(
fn {name, arity} = fa ->
warn(
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
@@ -826,7 +807,7 @@ defmodule Protocol do
quote bind_quoted: [built_in: __built_in__()] do
any_impl_for =
if @fallback_to_any do
quote do: unquote(__MODULE__.Any)
__MODULE__.Any
else
nil
end
@@ -872,6 +853,9 @@ 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
@@ -880,23 +864,27 @@ defmodule Protocol do
end
else
Kernel.def impl_for!(data) do
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
impl_for(data) ||
raise(Protocol.UndefinedError,
protocol: __MODULE__,
value: data,
description: unquote(undefined_impl_description)
)
end
end
# Internal handler for Structs
Kernel.defp struct_impl_for(struct) do
target =
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
{:module, module} -> module
{:error, _} -> unquote(any_impl_for)
end
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}
unless Module.defines_type?(__MODULE__, {:t, 0}) do
if not Module.defines_type?(__MODULE__, {:t, 0}) do
@typedoc """
All the types that implement this protocol.
"""
@@ -910,9 +898,9 @@ defmodule Protocol do
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))
@spec __protocol__(:consolidated?) :: boolean
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
@spec __protocol__(:functions) :: [{atom(), arity()}]
@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)
@@ -920,12 +908,6 @@ 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)
@@ -989,14 +971,12 @@ 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, struct, [], env)
derive(proto, for, [], env)
{proto, opts} when is_atom(proto) ->
derive(proto, for, struct, opts, env)
derive(proto, for, opts, env)
end
:lists.foreach(foreach, :lists.flatten(derives))
@@ -1004,21 +984,30 @@ defmodule Protocol do
:ok
end
defp derive(protocol, for, struct, opts, env) do
defp derive(protocol, for, opts, env) do
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
assert_protocol!(protocol, extra)
__ensure_defimpl__(protocol, for, env)
assert_impl!(protocol, Any, 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)
{Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]}
end
# 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, impl, :__deriving__, args, env, fn mod, fun, args ->
:elixir_module.expand_callback(env.line, mod, :__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 = Module.concat(protocol, Any)
funs =
for {fun, arity} <- protocol.__protocol__(:functions) do
args = Macro.generate_arguments(arity, nil)
+16 -2
View File
@@ -184,8 +184,22 @@ defmodule Range do
@spec new(limit, limit) :: t
def new(first, last) when is_integer(first) and is_integer(last) do
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.18
step = if first <= last, do: 1, else: -1
step =
if first <= last do
1
else
# TODO: Remove me on v2.0
IO.warn_once(
{__MODULE__, :new},
fn ->
"Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention"
end,
3
)
-1
end
%Range{first: first, last: last, step: step}
end
+6 -4
View File
@@ -36,10 +36,9 @@ defmodule Record do
## Reflection
A list of all records in a module, if any, can be retrieved by reading the
`@__records__` module attribute. It returns a list of maps with the record
kind, name, tag, and fields. The attribute is only available inside the
module definition.
The record tag and its fields are stored as metadata in the "Docs" chunk
of the record definition macro. You can retrieve the documentation for
a module by calling `Code.fetch_docs/1`.
"""
@doc """
@@ -253,6 +252,7 @@ defmodule Record do
tag = tag || name
fields = Record.__record__(__MODULE__, :defrecord, name, tag, kv)
@doc record: {tag, fields}
defmacro unquote(name)(args \\ []) do
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
end
@@ -312,6 +312,8 @@ defmodule Record do
error_on_duplicate_record(module, name)
fields = fields(kind, kv)
# TODO: Remove me on Elixir v2.0
Module.register_attribute(module, :__records__, accumulate: true)
Module.put_attribute(module, :__records__, %{
+17 -42
View File
@@ -186,27 +186,7 @@ defmodule Regex do
defstruct re_pattern: nil, source: "", opts: [], re_version: ""
@type re_option ::
:unicode
| :caseless
| :dotall
| :multiline
| :extended
| :firstline
| :ungreedy
| :anchored
| :dollar_endonly
| :no_auto_capture
| :newline
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [re_option()]}
@type capture_option ::
:all | :first | :all_but_first | :none | :all_names | [binary() | atom()]
@type run_option ::
{:return, :binary | :index}
| {:capture, capture_option()}
| {:offset, non_neg_integer()}
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [term]}
defmodule CompileError do
@moduledoc """
@@ -242,7 +222,7 @@ defmodule Regex do
{:ok, Regex.compile!("foo", [:caseless])}
"""
@spec compile(binary, binary | [re_option()]) :: {:ok, t} | {:error, any}
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term}
def compile(source, opts \\ "") when is_binary(source) do
compile(source, opts, version())
end
@@ -270,7 +250,7 @@ defmodule Regex do
@doc """
Compiles the regular expression and raises `Regex.CompileError` in case of errors.
"""
@spec compile!(binary, binary | [re_option()]) :: t
@spec compile!(binary, binary | [term]) :: t
def compile!(source, options \\ "") when is_binary(source) do
case compile(source, options) do
{:ok, regex} -> regex
@@ -377,7 +357,7 @@ defmodule Regex do
["d", ""]
"""
@spec run(t, binary, [run_option()]) :: nil | [binary] | [{integer, integer}]
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
def run(regex, string, options \\ [])
def run(%Regex{} = regex, string, options) when is_binary(string) do
@@ -412,7 +392,7 @@ defmodule Regex do
nil
"""
@spec named_captures(t, String.t(), [{:return, :binary | :index}]) :: map | nil
@spec named_captures(t, String.t(), [term]) :: map | nil
def named_captures(regex, string, options \\ []) when is_binary(string) do
names = names(regex)
options = Keyword.put(options, :capture, names)
@@ -456,7 +436,7 @@ defmodule Regex do
[:caseless]
"""
@spec opts(t) :: [re_option()]
@spec opts(t) :: [term]
def opts(%Regex{opts: opts}) do
opts
end
@@ -528,7 +508,7 @@ defmodule Regex do
[["cd"], ["ce"]]
"""
@spec scan(t(), String.t(), [run_option()]) :: [[String.t()]] | [[{integer(), integer()}]]
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
def scan(regex, string, options \\ [])
def scan(%Regex{} = regex, string, options) when is_binary(string) do
@@ -587,40 +567,35 @@ defmodule Regex do
## Examples
iex> Regex.split(~r{-}, "a-b-c")
iex> Regex.split(~r/-/, "a-b-c")
["a", "b", "c"]
iex> Regex.split(~r{-}, "a-b-c", parts: 2)
iex> Regex.split(~r/-/, "a-b-c", parts: 2)
["a", "b-c"]
iex> Regex.split(~r{-}, "abc")
iex> Regex.split(~r/-/, "abc")
["abc"]
iex> Regex.split(~r{}, "abc")
iex> Regex.split(~r//, "abc")
["", "a", "b", "c", ""]
iex> Regex.split(~r{a(?<second>b)c}, "abc")
iex> Regex.split(~r/a(?<second>b)c/, "abc")
["", ""]
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second])
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
["a", "c"]
iex> Regex.split(~r{(x)}, "Elixir", include_captures: true)
iex> Regex.split(~r/(x)/, "Elixir", include_captures: true)
["Eli", "x", "ir"]
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second], include_captures: true)
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second], include_captures: true)
["a", "b", "c"]
iex> Regex.split(~r{-}, "-a-b--c", trim: true)
iex> Regex.split(~r/-/, "-a-b--c", trim: true)
["a", "b", "c"]
"""
@spec split(t, String.t(),
parts: pos_integer() | :infinity,
trim: boolean(),
on: capture_option(),
include_captures: boolean()
) :: [String.t()]
@spec split(t, String.t(), [term]) :: [String.t()]
def split(regex, string, options \\ [])
def split(%Regex{}, "", opts) do
+162 -38
View File
@@ -331,7 +331,7 @@ defmodule Registry do
def start_link(options) do
keys = Keyword.get(options, :keys)
unless keys in @keys do
if keys not in @keys do
raise ArgumentError,
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
end
@@ -350,27 +350,27 @@ defmodule Registry do
meta = Keyword.get(options, :meta, [])
unless Keyword.keyword?(meta) do
if not Keyword.keyword?(meta) do
raise ArgumentError, "expected :meta to be a keyword list, got: #{inspect(meta)}"
end
partitions = Keyword.get(options, :partitions, 1)
unless is_integer(partitions) and partitions >= 1 do
if not (is_integer(partitions) and partitions >= 1) do
raise ArgumentError,
"expected :partitions to be a positive integer, got: #{inspect(partitions)}"
end
listeners = Keyword.get(options, :listeners, [])
unless is_list(listeners) and Enum.all?(listeners, &is_atom/1) do
if not (is_list(listeners) and Enum.all?(listeners, &is_atom/1)) do
raise ArgumentError,
"expected :listeners to be a list of named processes, got: #{inspect(listeners)}"
end
compressed = Keyword.get(options, :compressed, false)
unless is_boolean(compressed) do
if not is_boolean(compressed) do
raise ArgumentError,
"expected :compressed to be a boolean, got: #{inspect(compressed)}"
end
@@ -471,7 +471,7 @@ defmodule Registry do
"""
@doc since: "1.4.0"
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [any()]}
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
def dispatch(registry, key, mfa_or_fun, opts \\ [])
when is_atom(registry) and is_function(mfa_or_fun, 1)
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
@@ -609,6 +609,65 @@ defmodule Registry do
end
end
@doc """
Out-of-band locking of the given `lock_key` for the duration of `function`.
Only one function can execute under the same `lock_key` at a given
time. The given function always runs in the caller process.
The `lock_key` has its own namespace and therefore does not clash or
overlap with the regular registry keys. In other words, locking works
out-of-band from the regular Registry operations. See the "Use cases"
section below.
Locking behaves the same regardless of the registry type.
## Use cases
The Registry is safe and concurrent out-of-the-box. You are not required
to use this function when interacting with the Registry. Furthermore,
`Registry` with `:unique` keys can already act as a process-lock for any
given key. For example, you can ensure only one process runs at a given
time for a given `:key` by doing:
name = {:via, Registry, {MyApp.Registry, :key, :value}}
# Do not attempt to start if we are already running
if pid = GenServer.whereis(name) do
pid
else
case GenServer.start_link(__MODULE__, :ok, name: name) do
{:ok, pid} -> pid
{:error, {:already_started, pid}} -> pid
end
end
Process locking gives you plenty of flexibility and fault isolation and
is enough for most cases.
This function is useful only when spawning processes is not an option,
for example, when copying the data to another process could be too
expensive. Or when the work must be done within the current process
for other reasons. In such cases, this function provides a scalable
mechanism for managing locks on top of the registry's infrastructure.
## Examples
iex> Registry.start_link(keys: :unique, name: Registry.LockTest)
iex> Registry.lock(Registry.LockTest, :hello, fn -> :ok end)
:ok
iex> Registry.lock(Registry.LockTest, :world, fn -> self() end)
self()
"""
@doc since: "1.18.0"
def lock(registry, lock_key, function)
when is_atom(registry) and is_function(function, 0) do
{_kind, partitions, _, pid_ets, _} = info!(registry)
{pid_server, _pid_ets} = pid_ets || pid_ets!(registry, lock_key, partitions)
Registry.Partition.lock(pid_server, lock_key, function)
end
@doc """
Returns `{pid, value}` pairs under the given `key` in `registry` that match `pattern`.
@@ -757,28 +816,28 @@ defmodule Registry do
In the example below we register the current process and look it up
both from itself and other processes:
iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
iex> Registry.start_link(keys: :unique, name: Registry.UniqueValuesTest)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
iex> {:ok, _} = Registry.register(Registry.UniqueValuesTest, "hello", :world)
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[:world]
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", self()) end) |> Task.await()
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", self()) end) |> Task.await()
[]
iex> parent = self()
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", parent) end) |> Task.await()
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", parent) end) |> Task.await()
[:world]
The same applies to duplicate registries:
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateValuesTest)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :world)
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[:world]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateLookupTest, "hello", self()))
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateValuesTest, "hello", self()))
[:another, :world]
"""
@@ -1182,8 +1241,8 @@ defmodule Registry do
def count(registry) when is_atom(registry) do
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
acc + safe_size(key_ets!(registry, partition_index))
Enum.sum_by(0..(partitions - 1), fn partition_index ->
safe_size(key_ets!(registry, partition_index))
end)
{_kind, 1, key_ets} ->
@@ -1258,9 +1317,8 @@ defmodule Registry do
:ets.select_count(key_ets, spec)
{:duplicate, partitions, _key_ets} ->
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
count = :ets.select_count(key_ets!(registry, partition_index), spec)
acc + count
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
end
end
@@ -1304,12 +1362,12 @@ defmodule Registry do
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort()
[{"hello", self(), :value}, {"world", self(), :value}]
Get all keys in the registry:
If you want to get keys, you can pass a separate selector:
iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
iex> Registry.start_link(keys: :unique, name: Registry.SelectKeysTest)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "hello", :value)
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "world", :value)
iex> Registry.select(Registry.SelectKeysTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
["hello", "world"]
"""
@@ -1352,9 +1410,8 @@ defmodule Registry do
case key_info!(registry) do
{_kind, partitions, nil} ->
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
count = :ets.select_count(key_ets!(registry, partition_index), spec)
acc + count
Enum.sum_by(0..(partitions - 1), fn partition_index ->
:ets.select_count(key_ets!(registry, partition_index), spec)
end)
{_kind, 1, key_ets} ->
@@ -1435,7 +1492,7 @@ defmodule Registry do
end
defp unlink_if_unregistered(pid_server, pid_ets, self) do
unless :ets.member(pid_ets, self) do
if not :ets.member(pid_ets, self) do
Process.unlink(pid_server)
end
end
@@ -1502,7 +1559,7 @@ defmodule Registry.Partition do
@moduledoc false
# This process owns the equivalent key and pid ETS tables
# and is responsible for monitoring processes that map to
# and is responsible for linking to processes that map to
# its own pid table.
use GenServer
@all_info -1
@@ -1527,13 +1584,25 @@ defmodule Registry.Partition do
@doc """
Starts the registry partition.
The process is only responsible for monitoring, demonitoring
and cleaning up when monitored processes crash.
The process is only responsible for linking and cleaning up when processes crash.
"""
def start_link(registry, arg) do
GenServer.start_link(__MODULE__, arg, name: registry)
end
@doc """
Runs function with a lock.
"""
def lock(pid, key, lock) do
ref = GenServer.call(pid, {:lock, key})
try do
lock.()
after
send(pid, {:unlock, key, ref})
end
end
## Callbacks
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
@@ -1554,7 +1623,7 @@ defmodule Registry.Partition do
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
end
{:ok, pid_ets}
{:ok, {pid_ets, %{}}}
end
# The key partition is a set for unique keys,
@@ -1588,7 +1657,21 @@ defmodule Registry.Partition do
{:reply, :ok, state}
end
def handle_info({:EXIT, pid, _reason}, ets) do
def handle_call({:lock, key}, from, {ets, lock}) do
lock =
case lock do
%{^key => queue} ->
Map.put(lock, key, :queue.in(from, queue))
%{} ->
go(from, key)
Map.put(lock, key, :queue.new())
end
{:noreply, {ets, lock}}
end
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
entries = :ets.take(ets, pid)
for {_pid, key, key_ets, _counter} <- entries do
@@ -1609,6 +1692,47 @@ defmodule Registry.Partition do
end
end
{:noreply, ets}
{:noreply, {ets, lock}}
end
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
unlock(key, state)
end
def handle_info({:unlock, key, ref}, state) do
Process.demonitor(ref, [:flush])
unlock(key, state)
end
defp unlock(key, {ets, lock}) do
%{^key => queue} = lock
lock =
case dequeue(queue, key) do
:empty -> Map.delete(lock, key)
{:not_empty, queue} -> Map.put(lock, key, queue)
end
{:noreply, {ets, lock}}
end
defp dequeue(queue, key) do
case :queue.out(queue) do
{:empty, _} ->
:empty
{{:value, {pid, _tag} = from}, queue} ->
if node(pid) != node() or Process.alive?(pid) do
go(from, key)
{:not_empty, queue}
else
dequeue(queue, key)
end
end
end
defp go({pid, _tag} = from, key) do
ref = Process.monitor(pid, tag: {:unlock, key})
GenServer.reply(from, ref)
end
end
+1 -1
View File
@@ -90,7 +90,7 @@ defmodule Set do
if target1 == target2 do
target1.intersection(set1, set2)
else
Enumerable.reduce(set1, {:cont, target1.new}, fn v, acc ->
Enumerable.reduce(set1, {:cont, target1.new()}, fn v, acc ->
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
end)
|> elem(1)
+27 -145
View File
@@ -31,8 +31,11 @@ defmodule Stream do
Due to their laziness, streams are useful when working with large
(or even infinite) collections. When chaining many operations with `Enum`,
intermediate lists are created, while `Stream` creates a recipe of
computations that are executed at a later moment. Let's see another
example:
computations that are executed at a later moment. Then when the
stream is consumed later on, most commonly by using a function in
the `Enum` module, the stream will emit its elements one by one.
Let's see another example:
1..3
|> Enum.map(&IO.inspect(&1))
@@ -90,11 +93,21 @@ defmodule Stream do
This module also provides many convenience functions for creating streams,
like `Stream.cycle/1`, `Stream.unfold/2`, `Stream.resource/3` and more.
Note the functions in this module are guaranteed to return enumerables.
Since enumerables can have different shapes (structs, anonymous functions,
and so on), the functions in this module may return any of those shapes
and this may change at any time. For example, a function that today
returns an anonymous function may return a struct in future releases.
> #### Do not check for `Stream` structs
>
> While some functions in this module may return the `Stream` struct,
> you must never explicitly check for the `Stream` struct, as streams
> may come in several shapes, such as `IO.Stream`, `File.Stream`, or
> even `Range`s.
>
> The functions in this module only guarantee to return enumerables
> and their implementation (structs, anonymous functions, etc) may
> change at any time. For example, a function that returns an anonymous
> function today may return a struct in future releases.
>
> Instead of checking for a particular type, you must instead write
> assertive code that assumes you have an enumerable, using the functions
> in the `Enum` or `Stream` module accordingly.
"""
@doc false
@@ -868,7 +881,7 @@ defmodule Stream do
"""
@spec transform(Enumerable.t(), acc, fun) :: Enumerable.t()
when fun: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
acc: any
acc: term
def transform(enum, acc, reducer) when is_function(reducer, 2) do
&do_transform(enum, fn -> acc end, reducer, &1, &2, nil, fn acc -> acc end)
end
@@ -883,7 +896,7 @@ defmodule Stream do
when start_fun: (-> acc),
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
after_fun: (acc -> term),
acc: any
acc: term
def transform(enum, start_fun, reducer, after_fun)
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(after_fun, 1) do
&do_transform(enum, start_fun, reducer, &1, &2, nil, after_fun)
@@ -910,7 +923,7 @@ defmodule Stream do
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
last_fun: (acc -> {Enumerable.t(), acc} | {:halt, acc}),
after_fun: (acc -> term),
acc: any
acc: term
def transform(enum, start_fun, reducer, last_fun, after_fun)
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(last_fun, 1) and
is_function(after_fun, 1) do
@@ -950,6 +963,8 @@ defmodule Stream do
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
{_, vals} ->
# Do not attempt to call the resource again, it has either done or halted
next = fn _ -> {:done, []} end
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
end
end
@@ -962,7 +977,6 @@ defmodule Stream do
last_fun.(user_acc)
catch
kind, reason ->
next.({:halt, []})
after_fun.(user_acc)
:erlang.raise(kind, reason, __STACKTRACE__)
else
@@ -1361,140 +1375,8 @@ defmodule Stream do
@doc since: "1.12.0"
@spec zip_with(enumerables, (Enumerable.t() -> term)) :: Enumerable.t()
when enumerables: [Enumerable.t()] | Enumerable.t()
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
&zip_list(enumerables, &1, &2, zip_fun)
else
&zip_enum(enumerables, &1, &2, zip_fun)
end
end
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
{:halted, acc}
end
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
end
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
case zip_list_heads_tails(enumerables, [], []) do
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
:error -> {:done, acc}
end
end
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
zip_list_heads_tails(rest, [head | heads], [tail | tails])
end
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
:error
end
defp zip_list_heads_tails([], heads, tails) do
{:lists.reverse(heads), :lists.reverse(tails)}
end
defp zip_enum(enumerables, acc, fun, zip_fun) do
step = fn x, acc ->
{:suspend, :lists.reverse([x | acc])}
end
enum_funs =
Enum.map(enumerables, fn enum ->
{&Enumerable.reduce(enum, &1, step), [], :cont}
end)
do_zip_enum(enum_funs, acc, fun, zip_fun)
end
# This implementation of do_zip_enum/4 works for any number of streams to zip
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
do_zip_close(zips)
{:halted, acc}
end
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
end
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
{:done, acc}
end
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
try do
do_zip_next(zips, acc, callback, [], [], zip_fun)
catch
kind, reason ->
do_zip_close(zips)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:next, buffer, acc} ->
do_zip_enum(buffer, acc, callback, zip_fun)
{:done, _acc} = other ->
other
end
end
# do_zip_next/6 computes the next tuple formed by
# the next element of each zipped stream.
defp do_zip_next(
[{_, [], :halt} | zips],
acc,
_callback,
_yielded_elems,
buffer,
_zip_fun
) do
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
case fun.({:cont, []}) do
{:suspended, [elem | next_acc], fun} ->
next_buffer = [{fun, next_acc, :cont} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, [elem | next_acc]} ->
next_buffer = [{fun, next_acc, :halt} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, []} ->
# The current zipped stream terminated, so we close all the streams
# and return {:halted, acc} (which is returned as is by do_zip/3).
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
end
defp do_zip_next(
[{fun, zip_acc, zip_op} | zips],
acc,
callback,
yielded_elems,
buffer,
zip_fun
) do
[elem | rest] = zip_acc
next_buffer = [{fun, rest, zip_op} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
end
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
# "yielded_elems" is a reversed list of results for the current iteration of
# zipping. That is to say, the nth element from each of the enums being zipped.
# It needs to be reversed and passed to the zipping function so it can do it's thing.
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
end
defp do_zip_close(zips) do
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
def zip_with(enumerables, zip_fun) do
R.zip_with(enumerables, zip_fun)
end
## Sources
+139 -3
View File
@@ -2,6 +2,142 @@ defmodule Stream.Reducers do
# Collection of reducers and utilities shared by Enum and Stream.
@moduledoc false
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
&zip_list(enumerables, &1, &2, zip_fun)
else
&zip_enum(enumerables, &1, &2, zip_fun)
end
end
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
{:halted, acc}
end
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
end
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
case zip_list_heads_tails(enumerables, [], []) do
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
:error -> {:done, acc}
end
end
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
zip_list_heads_tails(rest, [head | heads], [tail | tails])
end
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
:error
end
defp zip_list_heads_tails([], heads, tails) do
{:lists.reverse(heads), :lists.reverse(tails)}
end
defp zip_enum(enumerables, acc, fun, zip_fun) do
step = fn x, acc ->
{:suspend, :lists.reverse([x | acc])}
end
enum_funs =
Enum.map(enumerables, fn enum ->
{&Enumerable.reduce(enum, &1, step), [], :cont}
end)
do_zip_enum(enum_funs, acc, fun, zip_fun)
end
# This implementation of do_zip_enum/4 works for any number of streams to zip
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
do_zip_close(zips)
{:halted, acc}
end
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
end
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
{:done, acc}
end
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
try do
do_zip_next(zips, acc, callback, [], [], zip_fun)
catch
kind, reason ->
do_zip_close(zips)
:erlang.raise(kind, reason, __STACKTRACE__)
else
{:next, buffer, acc} ->
do_zip_enum(buffer, acc, callback, zip_fun)
{:done, _acc} = other ->
other
end
end
# do_zip_next/6 computes the next tuple formed by
# the next element of each zipped stream.
defp do_zip_next(
[{_, [], :halt} | zips],
acc,
_callback,
_yielded_elems,
buffer,
_zip_fun
) do
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
case fun.({:cont, []}) do
{:suspended, [elem | next_acc], fun} ->
next_buffer = [{fun, next_acc, :cont} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, [elem | next_acc]} ->
next_buffer = [{fun, next_acc, :halt} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
{_, []} ->
# The current zipped stream terminated, so we close all the streams
# and return {:halted, acc} (which is returned as is by do_zip/3).
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
end
defp do_zip_next(
[{fun, zip_acc, zip_op} | zips],
acc,
callback,
yielded_elems,
buffer,
zip_fun
) do
[elem | rest] = zip_acc
next_buffer = [{fun, rest, zip_op} | buffer]
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
end
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
# "yielded_elems" is a reversed list of results for the current iteration of
# zipping. That is to say, the nth element from each of the enums being zipped.
# It needs to be reversed and passed to the zipping function so it can do it's thing.
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
end
defp do_zip_close(zips) do
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
end
def chunk_every(chunk_by, enumerable, count, step, leftover) do
limit = :erlang.max(count, step)
@@ -151,10 +287,10 @@ defmodule Stream.Reducers do
defmacro reject(callback, fun \\ nil) do
quote do
fn entry, acc ->
unless unquote(callback).(entry) do
next(unquote(fun), entry, acc)
else
if unquote(callback).(entry) do
skip(acc)
else
next(unquote(fun), entry, acc)
end
end
end
+32 -15
View File
@@ -18,7 +18,7 @@ defmodule String do
"hello world"
The functions in this module act according to
[The Unicode Standard, Version 15.1.0](http://www.unicode.org/versions/Unicode15.1.0/).
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
## Interpolation
@@ -326,7 +326,7 @@ defmodule String do
recur_printable?(string, character_limit)
end
defp recur_printable?(_string, 0), do: true
defp recur_printable?(<<_::binary>>, 0), do: true
defp recur_printable?(<<>>, _character_limit), do: true
for char <- 0x20..0x7E do
@@ -357,9 +357,10 @@ defmodule String do
@doc ~S"""
Divides a string into substrings at each Unicode whitespace
occurrence with leading and trailing whitespace ignored. Groups
of whitespace are treated as a single occurrence. Divisions do
not occur on non-breaking whitespace.
occurrence with leading and trailing whitespace ignored.
Groups of whitespace are treated as a single occurrence.
Divisions do not occur on non-breaking whitespace.
## Examples
@@ -375,6 +376,11 @@ defmodule String do
iex> String.split("no\u00a0break")
["no\u00a0break"]
Removes empty strings, like when using `trim: true` in `String.split/3`.
iex> String.split(" ")
[]
"""
@spec split(t) :: [t]
defdelegate split(binary), to: String.Break
@@ -640,10 +646,12 @@ defmodule String do
The offset is capped to the length of the string. Returns a tuple with
two elements.
Note: keep in mind this function splits on graphemes and for such it
has to linearly traverse the string. If you want to split a string or
a binary based on the number of bytes, use `Kernel.binary_part/3`
instead.
> #### Linear Access {: .warning}
>
> This function splits on graphemes and for such it has to linearly traverse
> the string.
> If you want to split a string or a binary based on the number of bytes,
> use `Kernel.binary_part/3` instead.
## Examples
@@ -2225,6 +2233,12 @@ defmodule String do
Returns the grapheme at the `position` of the given UTF-8 `string`.
If `position` is greater than `string` length, then it returns `nil`.
> #### Linear Access {: .warning}
>
> This function has to linearly traverse the string.
> If you want to access a string or a binary in constant time based on the
> number of bytes, use `Kernel.binary_slice/3` or `:binary.at/2` instead.
## Examples
iex> String.at("elixir", 0)
@@ -2961,9 +2975,9 @@ defmodule String do
@doc """
Returns a float whose text representation is `string`.
`string` must be the string representation of a float including a decimal point.
In order to parse a string without decimal point as a float then `Float.parse/1`
should be used. Otherwise, an `ArgumentError` will be raised.
`string` must be the string representation of a float including leading digits and a decimal
point. To parse a string without decimal point as a float, refer to `Float.parse/1`. Otherwise,
an `ArgumentError` will be raised.
Inlined by the compiler.
@@ -2978,6 +2992,9 @@ defmodule String do
String.to_float("3")
** (ArgumentError) argument error
String.to_float(".3")
** (ArgumentError) argument error
"""
@spec to_float(String.t()) :: float
def to_float(string) when is_binary(string) do
@@ -3043,10 +3060,10 @@ defmodule String do
end
defp bag_difference(bag1, bag2) do
Enum.reduce(bag1, 0, fn {char, count1}, sum ->
Enum.sum_by(bag1, fn {char, count1} ->
case bag2 do
%{^char => count2} -> sum + max(count1 - count2, 0)
%{} -> sum + count1
%{^char => count2} -> max(count1 - count2, 0)
%{} -> count1
end
end)
end
+1 -1
View File
@@ -57,6 +57,6 @@ end
defimpl String.Chars, for: Float do
def to_string(term) do
:erlang.float_to_binary(term, [:short])
Float.to_string(term)
end
end
+6
View File
@@ -188,6 +188,12 @@ defmodule StringIO do
{:noreply, state}
end
# Fail fast if someone tries to use it with a File API
def handle_info({:file_request, from, reply_as, _req}, state) do
send(from, {:file_reply, reply_as, {:error, :enotsup}})
{:noreply, state}
end
def handle_info({:DOWN, _, _, _, _}, state) do
{:stop, :shutdown, state}
end
+2 -2
View File
@@ -525,7 +525,7 @@ defmodule Supervisor do
import Supervisor.Spec
@behaviour Supervisor
unless Module.has_attribute?(__MODULE__, :doc) do
if not Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -595,7 +595,7 @@ defmodule Supervisor do
@typedoc "The supervisor reference."
@type supervisor :: pid | name | {atom, node}
@typedoc "Options given to `start_link/2` and `c:init/1`."
@typedoc "Options given to `start_link/2` and `init/2`."
@type init_option ::
{:strategy, strategy}
| {:max_restarts, non_neg_integer}
+1 -1
View File
@@ -168,7 +168,7 @@ defmodule Supervisor.Spec do
) :: {:ok, tuple}
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
def supervise(children, options) do
unless strategy = options[:strategy] do
if !(strategy = options[:strategy]) do
raise ArgumentError, "expected :strategy option to be given"
end
+5 -5
View File
@@ -1054,7 +1054,7 @@ defmodule System do
* `:arg0` - sets the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect
if `use_stdio` is `false``.
if `use_stdio` is `false`.
* `:use_stdio` - `true` by default, setting it to false allows direct
interaction with the terminal from the callee
@@ -1101,7 +1101,7 @@ defmodule System do
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
assert_no_null_byte!(command, "System.cmd/3")
unless Enum.all?(args, &is_binary/1) do
if not Enum.all?(args, &is_binary/1) do
raise ArgumentError, "all arguments for System.cmd/3 must be binaries"
end
@@ -1239,7 +1239,7 @@ defmodule System do
This time is monotonically increasing and starts in an unspecified
point in time.
"""
@spec monotonic_time(time_unit) :: integer
@spec monotonic_time(time_unit | :native) :: integer
def monotonic_time(unit) do
:erlang.monotonic_time(normalize_time_unit(unit))
end
@@ -1265,7 +1265,7 @@ defmodule System do
case of time warps although the VM works towards aligning
them. This time is not monotonic.
"""
@spec system_time(time_unit) :: integer
@spec system_time(time_unit | :native) :: integer
def system_time(unit) do
:erlang.system_time(normalize_time_unit(unit))
end
@@ -1316,7 +1316,7 @@ defmodule System do
`monotonic_time/1`), gives the Erlang system time that corresponds
to that monotonic time.
"""
@spec time_offset(time_unit) :: integer
@spec time_offset(time_unit | :native) :: integer
def time_offset(unit) do
:erlang.time_offset(normalize_time_unit(unit))
end
+5 -5
View File
@@ -329,7 +329,7 @@ defmodule Task do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
unless Module.has_attribute?(__MODULE__, :doc) do
if not Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -610,7 +610,7 @@ defmodule Task do
* `:zip_input_on_exit` - (since v1.14.0) adds the original
input to `:exit` tuples. The value emitted for that task is
`{:exit, {input, reason}}`, where `input` is the collection element
that caused an exited during processing. Defaults to `false`.
that caused an exit during processing. Defaults to `false`.
## Example
@@ -716,7 +716,7 @@ defmodule Task do
iex> strings = ["long string", "longer string", "there are many of these"]
iex> stream = Task.async_stream(strings, fn text -> text |> String.codepoints() |> Enum.count() end)
iex> Enum.reduce(stream, 0, fn {:ok, num}, acc -> num + acc end)
iex> Enum.sum_by(stream, fn {:ok, num} -> num end)
47
See `async_stream/5` for discussion, options, and more examples.
@@ -842,14 +842,14 @@ defmodule Task do
Process.demonitor(ref, [:flush])
{url, state} = pop_in(state.tasks[ref])
IO.puts "Got #{inspect(result)} for URL #{inspect url}"
IO.puts("Got #{inspect(result)} for URL #{inspect url}")
{:noreply, state}
end
# If the task fails...
def handle_info({:DOWN, ref, _, _, reason}, state) do
{url, state} = pop_in(state.tasks[ref])
IO.puts "URL #{inspect url} failed with reason #{inspect(reason)}"
IO.puts("URL #{inspect url} failed with reason #{inspect(reason)}")
{:noreply, state}
end
end
+3 -3
View File
@@ -209,15 +209,15 @@ defmodule Task.Supervised do
ordered = Keyword.get(options, :ordered, true)
zip_input_on_exit = Keyword.get(options, :zip_input_on_exit, false)
unless is_integer(max_concurrency) and max_concurrency > 0 do
if not (is_integer(max_concurrency) and max_concurrency > 0) do
raise ArgumentError, ":max_concurrency must be an integer greater than zero"
end
unless on_timeout in [:exit, :kill_task] do
if on_timeout not in [:exit, :kill_task] do
raise ArgumentError, ":on_timeout must be either :exit or :kill_task"
end
unless (is_integer(timeout) and timeout >= 0) or timeout == :infinity do
if not ((is_integer(timeout) and timeout >= 0) or timeout == :infinity) do
raise ArgumentError, ":timeout must be either a positive integer or :infinity"
end
+3 -3
View File
@@ -74,7 +74,7 @@ defmodule Task.Supervisor do
@typedoc "Option values used by `start_link`"
@type option ::
DynamicSupervisor.option()
GenServer.option()
| DynamicSupervisor.init_option()
@typedoc """
@@ -498,7 +498,7 @@ defmodule Task.Supervisor do
Starts a task as a child of the given `supervisor`.
Task.Supervisor.start_child(MyTaskSupervisor, fn ->
IO.puts "I am running in a task"
IO.puts("I am running in a task")
end)
Note that the spawned process is not linked to the caller, but
@@ -614,7 +614,7 @@ defmodule Task.Supervisor do
defp build_stream(supervisor, link_type, enumerable, fun, options) do
shutdown = Keyword.get(options, :shutdown, 5000)
unless (is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill do
if not ((is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill) do
raise ArgumentError, ":shutdown must be either a positive integer or :brutal_kill"
end
+2 -16
View File
@@ -87,22 +87,8 @@ defmodule Tuple do
:erlang.insert_element(index + 1, tuple, value)
end
@doc """
Inserts an element at the end of a tuple.
Returns a new tuple with the element appended at the end, and contains
the elements in `tuple` followed by `value` as the last element.
Inlined by the compiler.
## Examples
iex> tuple = {:foo, :bar}
iex> Tuple.append(tuple, :baz)
{:foo, :bar, :baz}
"""
@spec append(tuple, term) :: tuple
@doc false
@deprecated "Use insert_at instead"
def append(tuple, value) do
:erlang.append_element(tuple, value)
end
+21 -16
View File
@@ -367,21 +367,27 @@ defmodule URI do
@doc """
Percent-encodes all characters that require escaping in `string`.
By default, this function is meant to escape the whole URI, and
therefore it will only escape characters which are foreign in
all parts of a URI. Reserved characters (such as `:` and `/`)
or unreserved (such as letters and numbers) are not escaped.
The optional `predicate` argument specifies a function used to detect whether
a byte in the `string` should be escaped:
Because different components of a URI require different escaping
rules, this function also accepts a `predicate` function as an optional
argument. If passed, this function will be called with each byte
in `string` as its argument and should return a truthy value (anything other
than `false` or `nil`) if the given byte should be left as is, or
return a falsy value (`false` or `nil`) if the character should be
escaped. Defaults to `URI.char_unescaped?/1`.
* if the function returns a truthy value, the byte should be kept as-is.
* if the function returns a falsy value, the byte should be escaped.
See `encode_www_form/1` if you are interested in escaping reserved
characters too.
The `predicate` argument can use some built-in functions:
* `URI.char_unescaped?/1` (default) - reserved characters (such as `:`
and `/`) or unreserved (such as letters and numbers) are kept as-is.
It's typically used to encode the whole URI.
* `URI.char_unreserved?/1` - unreserved characters (such as letters and
numbers) are kept as-is. It's typically used to encode components in
a URI, such as query or fragment.
* `URI.char_reserved?/1` - Reserved characters (such as `:` and `/`) are
kept as-is.
And, you can also use custom functions.
See `encode_www_form/1` if you are interested in encoding `string` as
"x-www-form-urlencoded".
## Examples
@@ -693,8 +699,7 @@ defmodule URI do
> #### `:authority` field {: .info}
>
> This function sets the field `:authority` for backwards-compatibility reasons
> but it is deprecated.
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
## Examples
@@ -902,7 +907,7 @@ defmodule URI do
%{rel | path: remove_dot_segments_from_path(rel.path)}
end
def merge(base, %URI{host: host} = rel) when host != nil do
def merge(%URI{} = base, %URI{host: host} = rel) when host != nil do
%{rel | scheme: base.scheme, path: remove_dot_segments_from_path(rel.path)}
end
+1 -1
View File
@@ -686,7 +686,7 @@ end
defimpl Inspect, for: Version.Requirement do
def inspect(%Version.Requirement{source: source}, opts) do
colorized = Inspect.Algebra.color("\"" <> source <> "\"", :string, opts)
colorized = Inspect.Algebra.color_doc("\"" <> source <> "\"", :string, opts)
Inspect.Algebra.concat(["Version.parse_requirement!(", colorized, ")"])
end

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