Compare commits

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

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

* Fix non-deterministic Enum tests

* Fix :only option when deriving Inspect

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

* Fix non-deterministic Registry doctest

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

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

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

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

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

* Update lib/elixir/lib/kernel.ex

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

---------

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

Demo:

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

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

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

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

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

This reverts commit f97d8585e8.
2023-11-10 12:36:38 +01:00
Cameron Duley 56e6494b36 Use in/2 in String.replace_invalid/2 guards (#13093) 2023-11-10 09:09:29 +01:00
Cameron Duley 033a177078 Fix String.replace_invalid/2 perf regressions (#13090) 2023-11-10 09:09:29 +01:00
Minh Daoandminhqdao b9fffa3b41 Fix typo in getting-started guide (#13085)
Co-authored-by: minhqdao <hello@minhdao.de>
2023-11-07 23:05:51 +01:00
Juan Barrios 7be85838fa Update float.ex description of ceil/2 and floor/2 (#13084) 2023-11-07 20:11:24 +01:00
Jacob Swanner 37f8832229 Fix Enum cheatsheet for drop/2 and take/2 with negative index (#13080) 2023-11-07 10:51:31 +01:00
José Valim 7ebf5c3032 Add :emit_warnings to Code.string_to_quoted 2023-11-06 16:31:05 +01:00
José Valim 7d4d42097d Restore code paths in archive.install/escript.install
Closes #13079.
2023-11-06 15:56:05 +01:00
Lucas Francisco da Matta Vegi 33e1b570bf Additional remarks for maintaining research history (#13078)
Similar to what we had already done with other anti-patterns that changed names
2023-11-06 15:56:05 +01:00
José Valim 8920cc1432 Fix case clause error on tokenizer 2023-11-06 13:29:19 +01:00
Erik André Jakobsen 9b480f985b [docs] Clean up sigils intro (#13077) 2023-11-06 13:29:19 +01:00
Łukasz Samson daab3f80a0 Fix crashes when :beam_lib.info(beam) returns error (#13075) 2023-11-05 20:15:33 +01:00
Tony Dang ec8782a486 Fix typo in "Getting Started - Enumerables and Streams" docs (#13073) 2023-11-04 11:14:48 +01:00
Ioannis Kyriazis 6745f775b2 is -> us (#13072) 2023-11-03 23:23:56 +01:00
Łukasz Samson 7923e4f383 Elixir 1.14.5 supports Erlang/OTP 26 (#13071) 2023-11-03 11:54:02 +01:00
Michał Łępicki d280846843 Fix typo: an dread -> and read (#13069) 2023-11-03 08:08:23 +01:00
Rich Morin 178b654a1b Fix typo (#13068) 2023-11-02 21:04:21 +01:00
José Valim 3a516a3f2b Docs to new options and functions 2023-11-02 19:32:25 +01:00
Cameron Duley d5316d558f Add String.replace_invalid/2 (#13067) 2023-11-02 19:32:25 +01:00
Jonatan Kłosko a7216decad Add offset option to File.stream! (#13063) 2023-11-02 17:58:22 +01:00
Michał Łępicki 8d545610d2 Fix Path.absname/2 spec (#13065) 2023-11-02 13:08:52 +01:00
José Valim 23c27b629d Warn if both :applications and :extra_applications are used 2023-11-02 10:19:11 +01:00
Panagiotis Nezis 0220d81513 Support --sparse in archive.install and escript.install (#13059) 2023-11-02 08:57:51 +01:00
José Valim aabe5dad76 Do not use Erlang/OTP 26.1 on CI (#13062)
It has a bug when looking up mismatched module names.
2023-11-02 08:30:01 +01:00
Łukasz Samson 741757f2a7 Lazily evaluate File.cwd! in Path.expand and Path.absname (#13061)
do not crash with File.Error with already absolute paths if File.cwd returns error or nil
2023-11-02 08:11:44 +01:00
José Valim f5e543576e Use explicit/implicit vs manual/automatic 2023-11-01 16:33:17 +01:00
José Valim e6b641a075 Fix typo on docs 2023-11-01 16:11:26 +01:00
rktjmp 3822609621 Restore GenServer introduction mermaid graph (#13058)
Restores graph removed in f5a61d1, with correct request -> reply arrow
ordering.
2023-11-01 16:11:26 +01:00
Panagiotis Nezis 0f65cb065a Additional remarks for application config anti-pattern for Mix tasks (#13057) 2023-11-01 12:42:56 +01:00
José Valim ae19236e08 Bring behaviour section from website 2023-11-01 11:17:59 +01:00
José Valim f422b77aaa Update docs 2023-11-01 10:34:59 +01:00
José Valim 43b3e94506 Add more examples to unrelated clauses 2023-10-31 20:57:30 +01:00
José Valim 2d86cb0027 Handle warnings from unquote functions 2023-10-31 12:00:04 +01:00
José Valim 792d4cc631 Release v1.16.0-rc.0 2023-10-31 09:16:09 +01:00
José Valim f8016bca48 Describe them as potential anti-patterns 2023-10-31 08:52:14 +01:00
José Valim 82818f7126 Improve complex extraction example 2023-10-31 07:49:48 +01:00
José Valim 5c8f9aac64 Fix getting started links 2023-10-30 23:40:15 +01:00
José Valim 1a8bad1bf2 Streamline unrelated introduction 2023-10-30 20:10:53 +01:00
José Valim 4b795871b6 Improve examples and docs 2023-10-30 15:50:53 +01:00
José Valim 3036401c7c Clarify best practices and update anti-patterns list 2023-10-30 15:12:41 +01:00
José Valim 8bbba572de Describe pattern matching as simpler 2023-10-30 09:18:25 +01:00
José Valim 968b51319e Clarify scope of anti-patterns 2023-10-30 08:11:25 +01:00
José Valim 900f25f832 Branch out v1.16 2023-10-29 12:30:32 +01:00
678 changed files with 26794 additions and 73091 deletions
+2 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
[
inputs: [
"lib/*/{lib,scripts,unicode,test}/**/*.{ex,exs}",
@@ -21,5 +17,6 @@
# Float tests
float_assert: 1
]
],
normalize_bitstring_modifiers: false
]
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
*.ex diff=elixir
*.exs diff=elixir
-3
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
---
blank_issues_enabled: true
-3
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
---
name: Report an issue
description:
-3
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
version: 2
updates:
- package-ecosystem: "github-actions"
+134
View File
@@ -0,0 +1,134 @@
name: builds.hex.pm
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
concurrency: builds_txt
jobs:
release_pre_built:
strategy:
fail-fast: true
max-parallel: 1
matrix:
include:
- otp: 24
otp_version: '24.3'
upload_generic_version: upload_generic_version
- otp: 25
otp_version: '25.3'
- otp: 26
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get tags
run: git fetch --tags origin
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Utils.sh
run: |
cat << 'EOF' > utils.sh
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
EOF
chmod +x utils.sh
- name: Upload Docs to S3
if: ${{ matrix.build_docs }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
for f in doc/*; do
if [ -d "$f" ]; then
app=`echo $f | sed s/"doc\/"//`
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge "${surrogate_key}"
fi
done
- name: Upload Precompiled to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}
fi
- name: Update builds txt
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
ref_name=${{ github.ref_name }}
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
touch builds.txt
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
purge builds/elixir/txt
+6 -13
View File
@@ -1,22 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: CI for Markdown content
on:
push:
branches:
- "main"
- 'main'
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
pull_request:
paths:
- "lib/**/*.md"
- 'lib/**/*.md'
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
name: Lint Markdown content
@@ -24,17 +18,16 @@ jobs:
strategy:
fail-fast: false
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
uses: DavidAnson/markdownlint-cli2-action@v13.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+20 -73
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: CI
on:
@@ -14,6 +11,7 @@ on:
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
@@ -21,44 +19,29 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "28.1"
deterministic: true
- otp_version: "28.1"
erlc_opts: "warnings_as_errors"
coverage: true
- otp_version: "28.1"
otp_latest: true
erlc_opts: "warnings_as_errors"
- otp_version: "27.3"
erlc_opts: "warnings_as_errors"
- otp_version: "27.0"
erlc_opts: "warnings_as_errors"
- otp_version: "26.0"
otp_latest: true
- otp_version: "25.3"
- otp_version: "25.0"
- otp_version: "24.3"
- otp_version: "24.0"
- otp_version: master
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
# Earlier Erlang/OTP versions ignored compiler directives
# when using warnings_as_errors. So we only set ERLC_OPTS
# from Erlang/OTP 27+.
env:
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- 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
@@ -73,11 +56,6 @@ jobs:
- name: Elixir test suite
run: make test_elixir
continue-on-error: ${{ matrix.development }}
env:
COVER: "${{ matrix.coverage }}"
- name: "Calculate Coverage"
run: make cover | tee "$GITHUB_STEP_SUMMARY"
if: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main)
if: ${{ matrix.otp_latest }}
run: |
@@ -86,35 +64,28 @@ jobs:
cd ex_doc
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
cd ../elixir/
git fetch --tags
DOCS_OPTIONS="--warnings-as-errors" make docs
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
- name: "Upload Coverage Artifact"
if: "${{ matrix.coverage }}"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: TestCoverage
path: cover/*
if: ${{ matrix.otp_latest }}
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["26.2", "27.3", "28.1"]
runs-on: windows-2022
otp_version: ["24", "25", "26.0"]
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir
@@ -126,17 +97,17 @@ jobs:
- name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite
run: make test_erlang
run: make --keep-going test_erlang
- name: Elixir test suite
run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir
make --keep-going test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Install Shellcheck
@@ -148,27 +119,3 @@ jobs:
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
license_compliance:
name: Check Licence Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
-4
View File
@@ -1,8 +1,4 @@
# #!/usr/bin/env elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
[tag] = System.argv()
Mix.install([
+5 -8
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Notify
on:
@@ -13,16 +10,16 @@ permissions:
jobs:
notify:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
- uses: erlef/setup-beam@v1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
-109
View File
@@ -1,109 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: "Run OSS Review Toolkit"
description: "Runs OSS Review Toolkit & generates SBoMs"
inputs:
report-formats:
description: "ORT Report Formats"
required: true
fail-on-violation:
description: "Whether to fail on violation."
required: false
default: false
upload-reports:
description: "Whether to upload all reports"
required: false
default: false
version:
description: "Elixir Version (Tag / SHA)"
required: true
outputs:
results-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-path }}"
results-sbom-cyclonedx-xml-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
results-sbom-cyclonedx-json-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
results-sbom-spdx-yml-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
results-sbom-spdx-json-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
runs:
using: "composite"
steps:
- name: Fetch Default ORT Config
id: fetch-default-ort-config
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: oss-review-toolkit/ort-config
ref: "main"
path: ".ort-config"
- name: Setup ORT Config
id: setup-ort-config
shell: bash
run: |
mkdir -p "/$HOME/.ort/"
# Move Fetched Default Config into Place
mv .ort-config "$HOME/.ort/config"
# Append Global ORT Config
cat .ort/config/config.yml >> "$HOME/.ort/config/config.yml"
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
sed -i "s|https://github.com/elixir-lang/elixir.git|${ELIXIR_REPO}@${ELIXIR_VERSION}|" project.spdx.yml
env:
ELIXIR_VERSION: "${{ inputs.version }}"
ELIXIR_REPO: "${{ github.server_url }}/${{ github.repository }}.git"
- name: "Cache ScanCode"
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
with:
path: "~/.cache/scancode-tk"
key: ${{ runner.os }}-scancode
- name: Run OSS Review Toolkit
id: ort
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
with:
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
run: >-
labels,
cache-dependencies,
cache-scan-results,
analyzer,
scanner,
advisor,
evaluator,
reporter,
${{ inputs.upload-reports == 'true' && 'upload-results' || '' }}
fail-on: "${{ inputs.fail-on-violation == 'true' && 'violations,issues' || '' }}"
report-formats: "${{ inputs.report-formats }}"
ort-cli-report-args: >-
-O CycloneDX=output.file.formats=json,xml
-O SpdxDocument=outputFileFormats=JSON,YAML
sw-version: "${{ inputs.version }}"
+23 -384
View File
@@ -1,33 +1,25 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
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: read
contents: write
jobs:
create_draft_release:
runs-on: ubuntu-22.04
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
@@ -35,396 +27,43 @@ jobs:
--notes '' \
--draft \
${{ github.ref_name }}
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
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:
name: "Build Elixir"
release_pre_built:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
include:
- otp: 24
otp_version: '24.3'
- otp: 25
otp_version: '25.3'
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
- otp: 28
otp_version: "28.0"
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: matrix.build_docs
with:
name: Docs
path: Docs.zip*
sign:
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [26, 27, 28]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
steps:
- name: "Download build"
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
sbom:
name: Generate SBoM
needs: [build, sign]
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
path: /tmp/build-artifacts/
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
report-formats: "CycloneDx,SpdxDocument"
version: "${{ github.ref_type == 'tag' && github.ref_name || github.sha }}"
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
sbom-path: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Copy SBoM provenance"
id: sbom-provenance
shell: bash
run: |
mkdir attestations
for FILE in /tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}; do
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}").sigstore"
cp "$ATTESTATION" "attestations/$(basename "${{ steps.ort.outputs.results-sbom-spdx-json-path }}").sigstore"
env:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: "SBoM"
path: |
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: "Attestations"
path: "attestations/*.sigstore"
upload-release:
needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
- name: Upload Pre-built
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
else
tag="${{ github.ref_name }}"
fi
gh release upload \
--repo ${{ github.repository }} \
--clobber \
"$tag" \
elixir-otp-*.zip \
elixir-otp-*.zip.sha{1,256}sum \
elixir-otp-*.zip.sigstore \
elixir-otp-*.exe \
elixir-otp-*.exe.sha{1,256}sum \
elixir-otp-*.exe.sigstore \
Docs.zip \
Docs.zip.sha{1,256}sum \
Docs.zip.sigstore \
bom.*
upload-builds-hex-pm:
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
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 }}
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
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-${oldest_otp}.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 }}
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
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-${oldest_otp}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
done
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
--cache-control "public,max-age=3600" \
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
echo 'builds/elixir/txt' >> purge_keys.txt
- name: Flush cache
if: github.repository == 'elixir-lang/elixir'
run: |
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
for key in $(cat purge_keys.txt); do
purge "${key}"
done
gh release upload --clobber "${{ github.ref_name }}" \
elixir-otp-${{ matrix.otp }}.zip \
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
elixir-otp-${{ matrix.otp }}.exe \
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
- name: Upload Docs to GitHub
if: ${{ matrix.build_docs }}
env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: "Release pre built"
description: "Builds elixir release, ExDoc and generates docs"
inputs:
@@ -13,7 +10,7 @@ inputs:
runs:
using: "composite"
steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ inputs.otp_version }}
version-type: strict
@@ -22,6 +19,8 @@ runs:
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
@@ -35,6 +34,8 @@ runs:
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
- name: Get ExDoc ref
if: ${{ inputs.build_docs }}
shell: bash
@@ -45,7 +46,7 @@ runs:
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
fi
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@v3
if: ${{ inputs.build_docs }}
with:
repository: elixir-lang/ex_doc
@@ -63,5 +64,6 @@ runs:
if: ${{ inputs.build_docs }}
shell: bash
run: |
git fetch --tags
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
-5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
/doc/
/lib/*/ebin/
/lib/*/_build/
@@ -15,4 +11,3 @@
/.eunit
.elixir.plt
erl_crash.dump
/cover/
-8
View File
@@ -1,6 +1,3 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
@@ -20,19 +17,14 @@
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
-118
View File
@@ -1,118 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
excludes:
paths:
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: ".github/**/*"
reason: "BUILD_TOOL_OF"
comment: "Documentation"
- pattern: ".ort/**/*"
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-21
View File
@@ -1,21 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
includeFilesWithoutFindings: true
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
-88
View File
@@ -1,88 +0,0 @@
/*
* Copyright (C) 2019 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
* Copyright (c) 2021 The Elixir Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
// Docs: https://oss-review-toolkit.org/ort/docs/configuration/evaluator-rules
val whitelistedLicenses = listOf(
// License for Elixir & Imported Erlang Projects
"Apache-2.0",
// License for the Elixir Logo
"LicenseRef-elixir-trademark-policy",
"LicenseRef-scancode-elixir-trademark-policy",
// License for included Unicode Files
"LicenseRef-scancode-unicode",
// DCO for committers
"LicenseRef-scancode-dco-1.1"
).map { SpdxSingleLicenseExpression.parse(it) }.toSet()
fun PackageRule.howToFixDefault() = """
* Check if this license violation is intended
* Adjust evaluation rules in `.ort/config/evaluator.rules.kts`
""".trimIndent()
fun PackageRule.LicenseRule.isHandled() =
object : RuleMatcher {
override val description = "isHandled($license)"
override fun matches() = license in whitelistedLicenses
}
fun RuleSet.unhandledLicenseRule() = packageRule("UNHANDLED_LICENSE") {
// Do not trigger this rule on packages that have been excluded in the .ort.yml.
require {
-isExcluded()
}
// Define a rule that is executed for each license of the package.
licenseRule("UNHANDLED_LICENSE", LicenseView.CONCLUDED_OR_DECLARED_AND_DETECTED) {
require {
-isExcluded()
-isHandled()
}
// Throw an error message including guidance how to fix the issue.
error(
"The license $license is currently not covered by policy rules. " +
"The license was ${licenseSource.name.lowercase()} in package " +
"${pkg.metadata.id.toCoordinates()}.",
howToFixDefault()
)
}
}
fun RuleSet.unmappedDeclaredLicenseRule() = packageRule("UNMAPPED_DECLARED_LICENSE") {
require {
-isExcluded()
}
resolvedLicenseInfo.licenseInfo.declaredLicenseInfo.processed.unmapped.forEach { unmappedLicense ->
warning(
"The declared license '$unmappedLicense' could not be mapped to a valid license or parsed as an SPDX " +
"expression. The license was found in package ${pkg.metadata.id.toCoordinates()}.",
howToFixDefault()
)
}
}
val ruleSet = ruleSet(ortResult, licenseInfoResolver, resolutionProvider) {
unhandledLicenseRule()
unmappedDeclaredLicenseRule()
}
ruleViolations += ruleSet.violations
-15
View File
@@ -1,15 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-60
View File
@@ -1,60 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-18
View File
@@ -1,18 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-8
View File
@@ -1,8 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
-15
View File
@@ -1,15 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
+187 -439
View File
@@ -1,514 +1,262 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.16
# Changelog for Elixir v1.19
## Code snippets in diagnostics
## Type system improvements
Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings).
### Type checking of protocol dispatch and implementations
This release also adds type checking when dispatching and implementing protocols.
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
```elixir
defmodule Example do
def my_code(first..last//step = range) do
"hello #{range}"
end
end
```
the above emits the following warnings:
With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened:
```
warning: incompatible value given to string interpolation:
data
it has type:
%Range{first: term(), last: term(), step: term()}
but expected a type that implements the String.Chars protocol, it must be one of:
dynamic(
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
%Version.Requirement{}
) or atom() or binary() or float() or integer() or list(term())
```
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
```elixir
defmodule Example do
def my_code(%Date{} = date) do
for(x <- date, do: x)
end
end
```
will emit:
```
warning: incompatible value given to for-comprehension:
x <- date
it has type:
%Date{year: term(), month: term(), day: term(), calendar: term()}
but expected a type that implements the Enumerable protocol, it must be one of:
dynamic(
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
) or fun() or list(term()) or non_struct_map()
```
### Type checking and inference of anonymous functions
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
```elixir
defmodule Example do
def run do
fun = fn %{} -> :map end
fun.("hello")
end
end
```
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
```
warning: incompatible types given on function application:
fun.("hello")
given types:
binary()
but function has type:
(dynamic(map()) -> :map)
typing violation found at:
** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17:
error: syntax error before: '*'
│
6 │ fun.("hello")
│ ~
1 │ [1, 2, 3, 4, 5, *]
│ ^
│
└─ mod.exs:6:8: Example.run/0
└─ lib/my_app.ex:1:17
```
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
For mismatched delimiters, it now shows both delimiters:
### Acknowledgements
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/).
## Faster compile times in large projects
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
### Code loading bottlenecks
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing parallelization, especially on large projects.
This release makes it so modules are loaded lazily. This reduces the pressure on the code server and the amount of work during compilation, with reports of more than two times faster compilation for large projects. The benefits depend on the codebase size and the number of CPU cores available.
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
There are two potential regressions with this approach. The first one happens if you spawn processes during compilation which invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
list = [...]
Task.async_stream(list, fn item ->
MyLib.SomeOtherModule.do_something(item)
end)
end
```
** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18:
error: unexpected token: )
│
1 │ [1, 2, 3, 4, 5, 6)
│ │ └ mismatched closing delimiter (expected "]")
│ └ unclosed delimiter
│
└─ lib/my_app.ex:1:18
```
Because the spawned process is not visible to the compiler, it won't be able to load `MyLib.SomeOtherModule`. You have two options, either use `Kernel.ParallelCompiler.pmap/2` or explicitly call `Code.ensure_compiled!(MyLib.SomeOtherModule)` before spawning the process that uses said module.
For unclosed delimiters, it now shows where the unclosed delimiter starts:
The second one is related to `@on_load` callbacks (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invoke other modules defined within the same project. For example:
```elixir
defmodule MyLib.SomeModule do
@on_load :init
def init do
MyLib.AnotherModule.do_something()
end
def something_else do
...
end
end
MyLib.SomeModule.something_else()
```
** (TokenMissingError) token missing on lib/my_app:8:23:
error: missing terminator: )
│
1 │ my_numbers = (1, 2, 3, 4, 5, 6
│ └ unclosed delimiter
...
8 │ IO.inspect(my_numbers)
│ └ missing closing delimiter (expected ")")
│
└─ lib/my_app:8:23
```
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables:
Both snippets above could actually lead to non-deterministic compilation failures in the past, and as a result of these changes, compiling these cases are now deterministic.
### Parallel compilation of dependencies
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, as outlined in the previous section, there were pathological cases where performance gains would be left on the table, such as when compiling dependencies with native code or dependencies where one or two large files would take most of the compilation time.
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
## Improved pretty printing algorithm
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
```elixir
[
[1, 2, 3],
[1, 2, ...],
[1, ...],
[...],
...
]
```
error: undefined variable "unknown_var"
│
5 │ a - unknown_var
│ ^^^^^^^^^^^
│
└─ lib/sample.ex:5:9: Sample.foo/1
```
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
Otherwise the whole line is underlined:
```elixir
[
[1, 2, 3],
...
]
```
error: function names should start with lowercase characters or underscore, invalid name CamelCase
│
3 │ def CamelCase do
│ ^^^^^^^^^^^^^^^^
│
└─ lib/sample.ex:3
```
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
A huge thank you to Vinícius Müller for working on the new diagnostics.
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
## Revamped documentation
## Erlang/OTP 28 support
Elixir's Getting Started guides have been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references.
Elixir v1.19 officially supports Erlang/OTP 28.1+ and later. In order to support the new Erlang/OTP 28 representation for regular expressions, structs can now control how they are escaped into abstract syntax trees by defining a `__escape__/1` callback.
We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers examples of potential anti-patterns, with context and examples on how to improve their codebases.
On the other hand, the new representation for regular expressions implies they can no longer be used as default values for struct fields. Instead of this:
Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue.
```elixir
defmodule Foo do
defstruct regex: ~r/foo/
end
```
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
You must do this:
```elixir
defmodule Foo do
defstruct [:regex]
def new do
%Foo{regex: ~r/foo/}
end
end
```
## OpenChain certification
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
* Each release is attested along with the Source SBoM.
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
This work was performed by [Jonatan Männchen](https://maennchen.dev) and sponsored by the [Erlang Ecosystem Foundation](https://erlef.org).
## v1.19.6 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.19.5 (2026-01-09)
### 1. Enhancements
#### Elixir
* [Protocol] Optimize protocol consolidation to no longer load structs
### 2. Bug fixes
#### Elixir
* [Kernel] Fix unnecessary recompilation when `dbg_callback` is modified at runtime
* [Kernel] Fix parser crash on missing parentheses on expression following operator `not in`
* [Kernel] Support fetching abstract code for modules compiled with Elixir v1.14 and earlier
* [Protocol] Ensure protocol consolidation no longer stores outdated struct types. As a consequence, protocols types only track struct names at the moment
* [Stream] Revert optimization which caused nested streams in `Stream.flat_map/2` to crash
#### IEx
* [IEx] Fix usage of `#iex:break` as part of multi-line prompts
#### Logger
* [Logger.Backends] Do not crash on invalid metadata
## v1.19.4 (2025-11-27)
### 1. Enhancements
#### Mix
* [mix xref] Add `--min-cycle-label` to help projects adapt to the more precise `mix xref graph` reports in Elixir v1.19. In previous versions, Elixir would break a large compilation cycle into several smaller ones, and therefore developers would check for `--min-cycle-size` on CI. However, the issue is not the size of the cycle (it has no implication in the amount of compiled files), but how many compile-time dependencies (aka compile labels) in a cycle. The new option allows developers to filter on the label parameter
### 2. Bug fixes
#### Elixir
* [File] Ensure `File.cp_r/3` reports non-existing destination properly (instead of source)
#### ExUnit
* [ExUnit] Fix formatter crash when diffing takes too long
* [ExUnit] Ensure parallel matches in `assert` propagate type information
#### Logger
* [Logger] Fix regression where formatter would crash when given chardata (the crash would happen when logging non-ASCII characters)
#### Mix
* [mix help] Ensure `app:APP` works when the project or its dependencies were not yet compiled
* [mix escript.build] Ensure the `hex` application can be included in escripts
## v1.19.3 (2025-11-13)
### 1. Enhancements
#### Elixir
* [Kernel] Support /E modifier for regular expressions in config files
#### Mix
* [mix compile] Allow forcing specific compilers, such as `--force-elixir`, `--force-app`, etc
* [mix help app:APP] Support showing helps for apps in Elixir and Erlang standard libraries
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
#### ExUnit
* [ExUnit.Case] Fix crash when formatting errors caused by a linked/trapped exit during `setup_all`
#### Mix
* [mix compile.app] Ensure functions in the format `&Mod.fun/arity` can be written to .app files
* [mix compile.app] Ensure strings with Unicode characters can be written to .app files
## v1.19.2 (2025-11-02)
### 1. Enhancements
#### Elixir
* [Kernel] Measure and optimize writing of `.beam` files in the compiler
* [Kernel] Optimize rare scenarios where type checking took too long
#### Mix
* [mix compile] Add flag `--no-check-cwd` to skip compiler check to aid debugging
### 2. Bug fixes
#### Elixir
* [IO] Fix dialyzer warning on `IO.inspect :label`
* [Kernel] Ensure we warn on deprecated `~~~` unary operator
#### Logger
* [Logger] Reset ansi escapes before newlines in Logger
#### Mix
* [mix compile] Warn if `elixirc_paths` is not a list of string paths
* [mix compile] Address regression where umbrella children were compiled too early and without respecting compilation flags
* [mix deps.compile] Improve reliability of `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` across `mix escript.install`, `mix archive.install`, and others
## v1.19.1 (2025-10-20)
## v1.16.3 (2024-05-21)
### 1. Bug fixes
#### EEx
* [EEx] Address Dialyzer warnings when invoking `EEx.compile_string`
#### Elixir
* [Kernel] Optimize how types are computed for pretty printing
* [Kernel] Optimize how differences are computed in the type system
* [Macro] Do not escape options given to `dbg/2`
* [Protocol] Improve protocol violation warnings
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
* [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when`
* [Kernel] Only infer size in pinned variable in binary strings when needed
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
#### Logger
* [Logger] Ensure translators are persisted across logger restarts
#### Mix
* [mix compile] Do not attempt to touch deleted files when compilation fails and then resumed with missing files
* [mix deps.compile] Do not spawn partitions when all dependencies are local and already compiled
* [mix compile] Ensure compile paths are accessible during compilation
## v1.19.0 (2025-10-16)
## v1.16.2 (2024-03-10)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
* [Code.Fragment] Add `Code.Fragment.lines/1`
* [Enum] Provide more information on `Enum.OutOfBoundsError`
* [Inspect] Allow `optional: :all` when deriving Inspect
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
* [Kernel] Raise when U+2028 and U+2029 characters are present in comments and strings to avoid line spoofing attacks
* [Kernel] Include the line for the previous clause in errors/warnings related to conflicts between defaults on function definitions
* [Kernel] Support `min/2` and `max/2` as guards
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
* [Macro] Add `__escape__/1` callback so structs can escape references and other runtime data types in `Macro.escape/1`
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
* [OptionParser] Support the `:regex` type
* [OptionParser] Enhance parsing error to display available options
* [Protocol] Type checking of protocols dispatch and implementations
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
* [Regex] Raise error message when regexes are used as default values in struct fields for compatibility with Erlang/OTP 28
* [Registry] Add key-based partitioning of duplicate registries
* [String] Add `String.count/2` to count occurrences of a pattern
* [String] Update to Unicode 17.0.0
#### ExUnit
* [ExUnit] Set a process label for each test
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
* [ExUnit.Case] Add `:test_group` to the test context
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
#### IEx
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
#### Logger
* [Logger] Accept any enumerable in `Logger.metadata/1`
* [Code] Emit `:defmodule` tracing event on module definition
#### Mix
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun`, `mix help Mod.fun/arity`, and `mix help app:package`
* [mix format] Add options to mix format to allow excluding of files
* [mix test] Add `--name-pattern` option to `mix test`
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
* [mix xref graph] Add support for `--format json`
* [mix xref graph] Emit a warning if `--source` is part of a cycle
* [Mix] Support the `:compilers` option
* [Mix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
* [Mix] Add `Mix.install_project_dir/0`
* [Mix] Add environment variable for reusing `Mix.install/2` installation
* [Mix.SCM] Add `Mix.SCM.delete/1`
### 2. Bug fixes
#### Elixir
* [Code] Return error on invalid unicode sequences in `Code.string_to_quoted/2` instead of raising
* [Code] Properly handle column annotation for `in` in `not in` expressions
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
* [Enum] Fix infinite loop on `Enum.take/2` with negative index on empty enumerable
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
* [Inspect] Inspect ill-formed structs as maps
* [Kernel] Properly increment metadata newline when `?` is followed by a literal newline character
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
* [Protocol] `defstruct/1` and `defexception/1` are now disabled inside `defprotocol` as to not allow defining structs/exceptions alongside a protocol
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
#### ExUnit
* [ExUnit.Assertions] Fix order of pinned variables in failure reports
* [ExUnit.Assertions] Raise if attempting to raise an assertion error with invalid message (not a binary)
* [ExUnit.Case] Do not crash on empty test unit groups
* [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character
* [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input
#### IEx
* [IEx] Abort pipelines when there is an error in any step along the way
* [IEx.Helpers] Discard mermaid fenced blocks from ansi docs
#### ExUnit
* [ExUnit] Properly compared pinned values when building diffs
## v1.16.1 (2024-01-31)
### 1. Bug fixes
#### Elixir
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
* [String] Fix `String.capitalize/1` with a single codepoint
#### IEx
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
* [IEx] Do not assume `$HOME` is set
#### Mix
* [mix cmd] Preserve argument quoting in subcommands by no longer performing shell expansion. To revert to the previous behaviour, pass `--shell` before the command name
* [mix compile] Fix bug where reverting changes to an external resource (such as HEEx template) after a compilation error would make it so the source module would not be compiled
* [mix compile] Avoid failures when locking compilation across different users
* [mix compile] Fix race condition when renaming files used by the compilation lock
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
* [mix test] Ensure modules are preloaded in `mix test --slowest-modules=N`
* [mix xref graph] Provide more consistent output by considering strong connected components when computing graphs. This means that, if you were using `mix xref graph --format cycles`, you will now get fewer but larger cycles, as cycle between `A` and `B` and another between `A` and `C` is now considered a single cycle between `A`, `B`, and `C`. Note the cycles themselves are not problematic, unless they have compile-time dependencies in them
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
* [mix test] Properly resolve relative paths when running tests from individual files
* [mix test] Properly resolve Windows paths when running tests from individual files
## v1.16.0 (2023-12-22)
### 1. Enhancements
#### EEx
* [EEx] Include relative file information in diagnostics
#### Elixir
* [Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
* [Code] Automatically include columns in parsing options
* [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
* [Code.Fragment] Handle anonymous calls in fragments
* [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
* [File] Add `:offset` option to `File.stream!/2`
* [Kernel] Auto infer size of matched variable in bitstrings
* [Kernel] Preserve column information when translating typespecs
* [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
* [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
* [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
* [Macro] Add `Macro.compile_apply/4`
* [Module] Add support for `@nifs` annotation from Erlang/OTP 25
* [Module] Add support for missing `@dialyzer` configuration
* [String] Update to Unicode 15.1.0
* [String] Add `String.replace_invalid/2`
* [Task] Add `:limit` option to `Task.yield_many/2`
#### Logger
* [Logger] Add `Logger.levels/0`
#### Mix
* [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
* [mix archive.install] Support `--sparse` option
* [mix compile.app] Warn if both `:applications` and `:extra_applications` are used
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
* [mix escript.install] Support `--sparse` option
* [mix release] Include `include/` directory in releases
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
### 2. Bug fixes
#### Elixir
* [Code] Keep quotes for atom keys in formatter
* [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
* [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
* [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
* [Kernel] Ensure `dbg` module is a compile-time dependency
* [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
* [Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
* [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
* [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
* [Macro] Address exception on `Macro.to_string/1` for certain ASTs
* [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
* [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`
#### ExUnit
* [ExUnit] Raise on incorrectly dedented doctests
#### IEx
* [IEx.Pry] Fix prying functions with only literals in their body
#### Mix
* [mix archive.install] Restore code paths after `mix archive.install`
* [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
* [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
* [mix escript.install] Restore code paths after `mix escript.install`
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
* [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
* [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
* [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`
#### Mix
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and the task itself has no effect
* [mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`
### 4. Hard deprecations
#### Elixir
* [Code] Warn if line-break characters outside of `\r` and `\r\n` are found in strings according to UX#55. This warning will be fast-tracked into an error for security reasons in Elixir v1.20, following a similar rule to bidirectional control characters. They will already raise if found in comments
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}`, now requires the given variable (or expression) to explicitly pattern match on the struct before it can be updated. This is because, thanks to the type system, pattern matching on structs can find more errors, more reliably, and we want to promote its usage. Once pattern matching is added, you may optionally convert the struct update syntax into the map update syntax `%{uri | path: "/foo/bar"}` with no less of typing guarantees
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
* [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
* [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
* [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
* [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead
#### Logger
#### ExUnit
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
* [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead
#### Mix
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
* [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
* [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler
## v1.18
## v1.15
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md).
-6
View File
@@ -1,9 +1,3 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Code of Conduct
Contact: elixir-lang-conduct@googlegroups.com
-197
View File
@@ -1,197 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Contributing to Elixir
We invite contributions to Elixir. To contribute, there are a few
things you need to know about the code. First, Elixir code is divided
by each application inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
Some test files need their `test_helper.exs` to be explicitly required
before, such as:
```sh
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile all (including Erlang modules):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
you can rebuild the language from scratch with:
```sh
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
We encourage contributors to write tests that capture both existing and newly
introduced behavior, especially for bug fixes and major changes:
* **Bug Fixes:** If you are fixing a bug, please try to include a test that
*fails* before your change and *passes* afterward. This makes it easier to
confirm that the fix addresses the underlying issue and helps prevent
regressions in the future.
* **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add tests that cover the
major parts of that functionality. Aim to have the best code coverage possible.
With tests running and passing, you are ready to contribute to Elixir and
[send a pull request](https://help.github.com/articles/using-pull-requests/).
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
guidelines on licensing and compliance. Below is a summary of the key points
affecting **all external contributors**:
- Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
- SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
```elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
```
- No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
- Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
- Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
- Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the
best of my knowledge, is covered under an appropriate open
source license and I have the right under that license to
submit that work with modifications, whether created in whole
or in part by me, under the same open source license (unless
I am permitted to submit under a different license), as
Indicated in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including
all personal information I submit with it, including my
sign-off) is maintained indefinitely and may be redistributed
consistent with this project or the open source license(s)
involved.
```
See http://developercertificate.org/ for a copy of the Developer Certificate
of Origin license.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
Now go back to Elixir's root directory and run:
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
-73
View File
@@ -1,73 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,98 +0,0 @@
ELIXIR TEAM TRADEMARKS POLICY
This document outlines the policy for allowed usage of the “Elixir” word and the
Elixir logo by other parties.
“Elixir” and the Elixir logo are registered trademarks of the Elixir Team. The
Elixir Team believes in a decentralized approach to growing the community and
the ecosystem, independent of the Elixir project and the Elixir Team.
Anyone can use the Elixir trademarks if that use of the trademark is nominative.
The trademarks must not be used to disparage the project and its community, nor
be used in any way to imply ownership, endorsement, or association with the
Elixir project and the Elixir Team.
You must not visually combine the Elixir logo with any other images, or change
the logo in any way other than ways required by printing restrictions. If you
want to create your own visual identity in relation to Elixir, you might use the
shape of an unrelated “water drop” as part of your design, as seen in many
community projects and initiatives. You must not combine or modify the Elixir
logo.
The Elixir logo is available in our repository in both vertical and horizontal
versions.
Nominative use
The “nominative use” (or “nominative fair use”) is a legal doctrine that
authorizes everyone (even commercial companies) to use or refer to the trademark
of another if:
The product or service in question must be one not readily identifiable without
use of the trademark.
Only so much of the mark or marks may be used as is reasonably necessary to
identify the product or service.
The organization using the mark must do nothing that would, in conjunction with
the mark, suggest sponsorship or endorsement by the trademark holder.
Our trademarks must be used to refer to the Elixir programming language.
Examples of permitted use
All examples listed next must strictly adhere to the terms outlined in the
previous sections:
Usage of the Elixir logo to say a technology is “powered by Elixir” under
nominative use. Linking back to the Elixir website, if possible, is appreciated.
Usage of the Elixir logo to display it as a supported technology in a service or
platform. For instance, you may say “we support Elixir” and use the Elixir logo,
but you may not refer to yourself as “the Elixir platform” nor imply any form of
endorsement or association with Elixir.
Usage of the Elixir logo in non-commercial community meetups, in presentations,
and in courses when referring to the language and its ecosystem under nominative
use.
Usage of the Elixir logo in non-commercial swag (stickers, t-shirts, mugs, etc)
to promote the Elixir programming language. The Elixir marks must be the only
marks featured in the product. You need permission to make swag that include
Elixir and other third party marks in them.
Inclusion of the Elixir logo in non-commercial icon sets. Use of the Elixir
icons must still adhere to Elixir’s trademark policies.
Usage of the “Elixir” word in book titles, meetups, conferences, and podcasts.
You must not use the word to imply uniqueness or endorsement from the Elixir
team. “The Elixir book” and “The Elixir podcast” are not permitted.
“Elixir in Action”, “Thinking Elixir”, and “Kraków Elixir User Group” are valid
examples already in use today.
Usage of the “Elixir” word in the names of freely distributed software and
hardware products is allowed when referring to use with or suitability for the
Elixir programming language, such as wxElixir, Elixirsense, etc. If the product
includes the Elixir programming language itself, then you must also respect its
license.
Examples of not permitted use
Here is a non-exhaustive list of non permitted uses of the marks:
Usage of the Elixir logo in book covers, conferences, and podcasts.
Usage of the Elixir logo as the mark of third party projects, even in combination
with other marks.
Naming any company or product after Elixir, such as “The Elixir Hosting”,
“The Elixir Consultants”, etc.
Examples that require permission
Here are some examples that may be granted permission upon request:
Selling merchandise (stickers, t-shirts, mugs, etc).
You can request permission by emailing trademarks@elixir-lang.org.
Important note
Nothing in this page shall be interpreted to allow any third party to claim any
association with the Elixir project and the Elixir Team, or to imply any
approval or support by the Elixir project and the Elixir Team for any third
party products, services, or events.
-58
View File
@@ -1,58 +0,0 @@
UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
Unicode Data Files include all data files under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
http://www.unicode.org/cldr/data/ . Unicode Software includes any source
code published in the Unicode Standard or under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
http://www.unicode.org/cldr/data/.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA
FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY
ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS
AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE
OR USE THE DATA FILES OR SOFTWARE.
COPYRIGHT AND PERMISSION NOTICE
Copyright © Unicode, Inc. All rights reserved. Distributed under
the Terms of Use in http://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Unicode data files and any associated documentation (the
"Data Files") or Unicode software and any associated documentation (the
"Software") to deal in the Data Files or Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, and/or sell copies of the Data Files or Software,
and to permit persons to whom the Data Files or Software are furnished
to do so, provided that
(a) the above copyright notice(s) and this permission notice appear with
all copies of the Data Files or Software,
(b) both the above copyright notice(s) and this permission notice appear
in associated documentation, and
(c) there is clear notice in each modified Data File or in the Software
as well as in the documentation associated with the Data File(s) or
Software that the data or software has been modified.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR
ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or
other dealings in these Data Files or Software without prior written
authorization of the copyright holder.
Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be
registered in some jurisdictions. All other trademarks and registered
trademarks mentioned herein are the property of their respective owners.
+41 -56
View File
@@ -1,16 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -22,19 +17,19 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: cover install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.NOTPARALLEL:
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 26)])' -s erlang halt | grep -q '^true'; \
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 26.0 is required to build Elixir"; \
echo "At least Erlang/OTP 24.0 is required to build Elixir"; \
exit 1; \
fi
endef
@@ -53,10 +48,6 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
test_$(1): test_formatted $(1)
@ echo "==> $(1) (ex_unit)"
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
cover/ex_unit_$(1).coverdata:
$(Q) COVER="1" $(MAKE) test_$(1)
cover/combined.coverdata: cover/ex_unit_$(1).coverdata
endef
define WRITE_SOURCE_DATE_EPOCH
@@ -74,21 +65,17 @@ endef
#==> Compilation tasks
APP := lib/elixir/ebin/elixir.app
EEX := lib/eex/ebin/Elixir.EEx.beam
ELIXIR := lib/elixir/ebin/elixir.beam
PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile
compile: erlang elixir
compile: erlang $(APP) elixir
erlang: $(ELIXIR)
$(ELIXIR): $(PARSER) lib/elixir/src/*
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(Q) $(GENERATE_APP) $(VERSION)
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -96,27 +83,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then Mix, and then compile EEx fully
elixir: stdlib $(EEX) mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
"$(MAKE)" unicode; \
fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
$(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_MIN_SIG) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(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;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -155,7 +144,6 @@ check_reproducible: compile
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
$(Q) rm -rf lib/*/ebin
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
$(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
@@ -166,10 +154,16 @@ check_reproducible: compile
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean: clean_man
clean:
rm -rf ebin
rm -rf lib/*/ebin
rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
@@ -179,15 +173,13 @@ clean: clean_man
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
rm -rf cover
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
$(Q) "$(MAKE)" clean_man
#==> Documentation tasks
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
@@ -229,19 +221,19 @@ docs_logger: compile ../ex_doc/bin/ex_doc
$(call DOCS_CONFIG,logger)
../ex_doc/bin/ex_doc:
@ echo "ex_doc is not found in ../ex_doc as expected. See CONTRIBUTING.md for more information."
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@ false
#==> Zip tasks
Docs.zip: docs
rm -f Docs.zip
zip -9 -r Docs.zip CHANGELOG.md doc LICENSE README.md
zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
@ echo "Docs file created $(CURDIR)/Docs.zip"
Precompiled.zip: build_man compile
rm -f Precompiled.zip
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man README.md VERSION
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
#==> Test tasks
@@ -292,15 +284,6 @@ test_stdlib: compile
cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
fi
cover/ex_unit_elixir.coverdata:
$(Q) COVER="1" $(MAKE) test_stdlib
cover/combined.coverdata: cover/ex_unit_elixir.coverdata
cover/combined.coverdata:
bin/elixir ./lib/elixir/scripts/cover.exs
cover: cover/combined.coverdata
#==> Dialyzer tasks
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs
@@ -323,15 +306,17 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1
define BUILD_MANPAGES
man/$(APP).1:
$(Q) cp man/$(APP).1.in man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
$(Q) rm -f man/$(APP).1.bak
endef
man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
clean_man:
rm -f man/elixir.1
+37
View File
@@ -0,0 +1,37 @@
LEGAL NOTICE INFORMATION
------------------------
All the files in this distribution are copyright to the terms below.
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
Copyright Ericsson AB 1996-2015
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
== All other files
Copyright 2012 Plataformatec
Copyright 2021 The Elixir Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-168
View File
@@ -1,168 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Open Source Policy
## 1. Introduction
This Open Source Policy outlines the licensing, contribution, and compliance
requirements for all code released under the Elixir project. By adhering to
these guidelines, we ensure that our community, maintainers, and contributors
uphold both legal and ethical standards while fostering a collaborative,
transparent environment.
This policy exists to support and protect the Elixir community. It aims to
balance openness, collaboration, and respect for all contributors’ rights,
ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope
This policy applies to the Elixir Programming language, located at
https://github.com/elixir-lang/elixir. It covers every file, and contribution
made, including documentation and any associated assets.
## 3. Licensing
All code released by the Elixir team is licensed under the
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
licenses are recognized as permissible in this project:
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
These licenses are considered acceptable for any files or code that form part of
an Elixir repository. If a contribution requires a different license, it must
either be rejected or prompt an update to this policy.
## 4. Contributing to Elixir Projects
Any code contributed to Elixir repositories must fall under one of the accepted
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
other license will be rejected unless this policy is formally revised to include
that license. All files except those specifically exempted (e.g., certain test
fixture files) must contain SPDX license and copyright headers
(`SPDX-License-Identifier` and `SPDX-FileCopyrightText`). If a file qualifies
for an exception, this must be configured in the ORT (Open Source Review Toolkit)
configuration and undergo review.
Contributions must not introduce executable binary files into the codebase.
Every Elixir project within the organization will have an automated GitHub
Action to enforce these rules. This mechanism aids in detecting non-compliant
licenses or files early in the review process.
## 5. Preservation of Copyright and License Information
Any third-party code incorporated into Elixir projects must retain original
copyright and license headers. If no such headers exist in the source, they must
be added. This practice ensures that original authors receive proper credit and
that the licensing lineage is preserved.
## 6. Objectives
The Elixir project aims to promote a culture of responsible open source usage.
Specifically, our objectives include:
### 6.1 Clearly Define and Communicate Licensing & Compliance Policies
We will identify and document all third-party dependencies, ensure that license
information is communicated clearly, and maintain a project-wide license policy
or compliance handbook.
### 6.2 Implement Clear Processes for Reviewing Contributions
We will provide well-defined contribution guidelines. We implement the
Developer Certificate of Origin (DCO) for additional clarity regarding
contributor rights and obligations.
### 6.3 Track and Audit Third-Party Code Usage
All projects will implement a Software Bill of Materials (SBoM) strategy and
regularly verify license compliance for direct and transitive dependencies.
### 6.4 Monitor and Continuously Improve Open Source Compliance
We will conduct periodic internal audits, integrate compliance checks into
continuous integration (CI/CD) pipelines, and regularly review and refine these
objectives to align with best practices.
## 7. Roles and Responsibilities
### 7.1 Core Team Member
Core Team Members are responsible for being familiar with this policy and
ensuring it is consistently enforced. They must demonstrate sufficient
competencies to understand the policy requirements and must reject or request
changes to any pull requests that violate these standards.
### 7.2 Contributor
Contributors are expected to follow this policy when submitting code. If a
contributor submits a pull request that does not comply with the policy
(e.g., introduces a disallowed license), Core Team Members have the authority to
reject it or request changes. No special competencies are required for
contributors beyond awareness and adherence to the policy.
### 7.3 EEF CISO
The CISO designated by the Erlang Ecosystem Foundation (EEF) provides oversight
on queries and guidance regarding open source compliance or legal matters for
Elixir. The CISO is responsible for checking ongoing compliance with the policy,
escalating potential violations to the Core Team, and involving legal counsel if
necessary. This role does not require legal expertise but does involve
initiating legal or community discussions when needed.
## 8. Implications of Failing to Follow the Program Requirements
If a violation of this policy is identified, the Elixir Core Team will undertake
the following actions:
## 8.1 Review the Codebase for Additional Violations
We will investigate the codebase thoroughly to detect any similar instances of
non-compliance.
## 8.2 Review and Update the Process or Policy
In collaboration with the EEF CISO, the Elixir Core Team will assess the policy
and our internal workflows, making any necessary clarifications or amendments to
reduce the likelihood of recurrence.
## 8.3 Notify and Train Core Team Members
We will ensure that all active Core Team Members are informed about any policy
changes and understand how to apply them in everyday development.
## 8.4 Remove or Replace the Offending Code
If required, we will remove or replace the non-compliant code.
## 9. Contact
The project maintains a private mailing list at
[policy@elixir-lang.org](mailto:policy@elixir-lang.org) for handling licensing
and policy-related queries. Email is the preferred communication channel, and
the EEF CISO will be included on this list to provide assistance and ensure
timely responses. While solutions may take longer to implement, the project
commits to acknowledging all queries within five business days.
## 10. External Contributions of Core Team Members
When Core Team Members contribute to repositories outside Elixir, they do so in
a personal capacity or via their employer. They will not act as official
representatives of the Elixir team in those external contexts.
## 11. Policy Review and Amendments
This policy will be revisited annually to address new concerns, accommodate
changes in community standards, or adjust to emerging legal or technical
requirements. Proposed amendments must be reviewed by the Core Team and, if
necessary, by the EEF CISO. Any significant changes will be communicated to
contributors and made publicly available.
*Effective Date: 2025-02-20*
*Last Reviewed: 2025-02-20*
+153 -64
View File
@@ -1,18 +1,7 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture>
</h1>
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
@@ -23,7 +12,7 @@ For more about Elixir, installation and documentation,
## Policies
New releases are announced in the [announcement mailing list][8].
You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com>
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
and replying to the confirmation email.
All security releases [will be tagged with `[security]`][10]. For more
@@ -32,64 +21,61 @@ information, please read our [Security Policy][9].
All interactions in our official communication channels follow our
[Code of Conduct][1].
All contributions are required to conform to our [Open Source Policy][11].
## Bug reports
For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities
privately [in our Security page](https://github.com/elixir-lang/elixir/security)**.
privately at elixir-security@googlegroups.com**.
All currently open bugs related to Elixir are listed in the issues tracker.
The Elixir team uses the issues tracker to focus on *actionable items*,
including planned enhancements in the short and medium term. We also do
our best to label entries for clarity and to ease collaboration.
## Issues tracker management
All currently open bugs related to the Elixir repository are listed
in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short- and
medium-term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as:
* Proposing new features as well as requests for support, help, and
* Proposing new features as well as request for support, help, and
guidance must be done in their own spaces, detailed next.
* Issues we have identified to be outside of Elixir's scope,
such as an upstream bug, will be closed (and requested to be moved
* Issues where we have identified to be outside of Elixir scope,
such as a bug upstream, will be closed (and requested to be moved
elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. If you believe we got something wrong, drop a
comment and we can always reopen the issue.
issues tracker tidy. However, we may get things wrong from time to
time, so we are glad to revisit issues and reopen if necessary.
By keeping the overall issues tracker tidy and organized, the community
can easily peak at what is coming in new releases and also get involved
by commenting on existing issues and submitting pull requests. Please
remember to keep the tone positive and be kind! For more information,
see the [Code of Conduct][1].
Keep the tone positive and be kind! For more information, see the
[Code of Conduct][1].
## Discussions, support, and help
### Proposing new features
For general discussions, support, and help, please use the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
For proposing new features, please start a discussion in the
[Elixir Core mailing list][3]. The [language development history and
its focus are described on our website](https://elixir-lang.org/development.html).
## Proposing new features
We encourage you to first propose new features in the community spaces
listed above. These discussions help refine ideas and gather feedback before
submission. Our website also includes [a general outline of the language
history and its current development focus](https://elixir-lang.org/development.html).
Once you are ready, you can submit your proposal to the [Elixir Core
mailing list][3], either through the web interface or by subscribing to
it at <elixir-lang-core+subscribe@googlegroups.com>. Remember to include
a clear problem description, compare the proposed solution to existing
alternatives in the Elixir ecosystem (and in other languages if possible),
and consider the potential impact your changes will have on the codebase and
community.
Keep in mind that it is your responsibility to argue and explain
why a feature is useful and how it will impact the codebase and
the community. A good proposal includes the problem description
and how the proposed solution compares with existing alternatives
in the Elixir ecosystem (as well as in other languages). To iron
out a proposal before submission, consider using and gathering
feedback from the community spaces [listed on the sidebar of the
Elixir website](https://elixir-lang.org/).
Once a proposal is accepted, it will be added to [the issue tracker][2].
Features and bug fixes that have already been merged and will be included
in the next release are then "closed" and added to the [changelog][7]
before release.
in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use many of the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
## Compiling from source
@@ -113,18 +99,122 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
When updating the repository, you may want to run `make clean` before
recompiling. For deterministic builds, you should set the environment
variable `ERL_COMPILER_OPTIONS=deterministic`.
Additionally, you may choose to run the test suite with `make clean test`.
## Contributing
Contributions to Elixir are always welcome! Before you get started, please check
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed
guidelines on how to set up your environment, run the test suite, format your
code, and submit pull requests. We also include information on our review
process, licensing requirements, and helpful tips to ensure a smooth
contribution experience.
We welcome everyone to contribute to Elixir. To do so, there are a few
things you need to know about the code. First, Elixir code is divided
in applications inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are changing just one file, you can choose to compile and run tests only
for that particular file for fast development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_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):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted and then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
you can rebuild the language from scratch with:
```sh
make clean_elixir compile
```
Similarly, if you can't get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
With tests running and passing, you are ready to contribute to Elixir and
[send a pull request](https://help.github.com/articles/using-pull-requests/).
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If any changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
to be installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
Now go back to Elixir's root directory and run:
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Development links
@@ -147,7 +237,6 @@ contribution experience.
[8]: https://groups.google.com/group/elixir-lang-ann
[9]: SECURITY.md
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
[11]: OPEN_SOURCE_POLICY.md
## License
@@ -155,4 +244,4 @@ contribution experience.
Elixir source code is released under Apache License 2.0.
Check [LICENSE](LICENSE) file for more information.
Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.
+9 -17
View File
@@ -1,34 +1,26 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Release process
## Shipping a new version
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
3. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION" and push it
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
5. Once GitHub actions completes, generate a new tag, and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
6. Wait until GitHub Actions publish artifacts to the draft release
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (after first rc)
### In the new branch
1. Comment out `CANONICAL := main/` in /Makefile
1. Comment out `CANONICAL=` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
@@ -36,7 +28,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
2. Start new /CHANGELOG.md
@@ -50,6 +42,6 @@
2. Update `otp_release` checks in `/Makefile` and `/lib/elixir/src/elixir.erl`
3. Update relevant CI workflows in `/.github/workflows/*.yml` - for release workflows, outdated/recently added Erlang/OTP versions must run conditionally
3. Update relevant CI workflows in `/.github/workflows/*.yml`
4. Remove `otp_release` version checks that are no longer needed
+4 -10
View File
@@ -1,9 +1,3 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Security Policy
## Supported versions
@@ -12,11 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.19 | Bug fixes and security patches
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.16 | Bug fixes and security patches
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
1.12 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.19.6
1.16.3
+12 -10
View File
@@ -1,12 +1,7 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e
ELIXIR_VERSION=1.19.6
ELIXIR_VERSION=1.16.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -23,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.
@@ -117,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="iex"
;;
-v|--no-halt|--color|--no-color)
-v|--no-halt)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -187,6 +182,10 @@ while [ $I -le $LENGTH ]; do
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi
;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
;;
*)
while [ $I -le $LENGTH ]; do
I=$((I + 1))
@@ -219,14 +218,17 @@ SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
fi
# One MAY change ERTS_BIN= but you MUST NOT change
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
ERTS_BIN=
ERTS_BIN="$ERTS_BIN"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"
if [ -n "$RUN_ERL_PIPE" ]; then
ESCAPED=""
+70 -32
View File
@@ -1,11 +1,8 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.19.6
set ELIXIR_VERSION=1.16.3
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
@@ -28,13 +25,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
@@ -67,11 +64,13 @@ echo ** Standalone options can't be combined with other options.
goto end
:shortversion
echo %ELIXIR_VERSION%
echo !ELIXIR_VERSION!
goto end
:parseopts
setlocal enabledelayedexpansion
rem Parameters for Elixir
set parsElixir=
rem Parameters for Erlang
set parsErlang=
@@ -98,25 +97,48 @@ if "!par!"=="" (
)
shift
set par="!par:"=\"!"
if !endLoop! == 1 (
set parsElixir=!parsElixir! !par!
goto startloop
)
rem ******* EXECUTION OPTIONS **********************
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set useIEx=1 && goto startloop)
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && goto startloop)
rem ******* EVAL PARAMETERS ************************
if ""==!par:-e=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--eval=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--rpc-eval=! (
set "VAR=%~2"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
shift
shift
goto startloop
)
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-e=! (shift && goto startloop)
if ""==!par:--eval=! (shift && goto startloop)
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
if ""==!par:-r=! (shift && goto startloop)
if ""==!par:-pr=! (shift && goto startloop)
if ""==!par:-pa=! (shift && goto startloop)
if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %~1" && shift && goto startloop)
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %~1" && shift && goto startloop)
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %~1" && shift && goto startloop)
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %~1" && shift && goto startloop)
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && goto startloop)
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
@@ -130,19 +152,35 @@ if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1""
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
set endLoop=1
set parsElixir=!parsElixir! !par!
goto startloop
:run
setlocal disabledelayedexpansion
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
)
if defined useIEx (
set beforeExtra=-s elixir start_iex !beforeExtra!
) else (
set beforeExtra=-s elixir start_cli !beforeExtra!
)
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra!
if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
) else (
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
)
exit /B %ERRORLEVEL%
:end
-5
View File
@@ -1,9 +1,4 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
+1 -6
View File
@@ -1,9 +1,4 @@
@echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
-5
View File
@@ -1,9 +1,4 @@
#!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
+5 -7
View File
@@ -1,9 +1,4 @@
@echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
@@ -21,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
goto end
:run
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
-5
View File
@@ -1,7 +1,2 @@
#!/usr/bin/env elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Mix.CLI.main()
+1 -6
View File
@@ -1,7 +1,2 @@
@echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
Executable → Regular
+1 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
# Store path to mix.bat as a FileInfo object
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
$newArgs = @()
@@ -24,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
}
# Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs
& $mixBatPath $newArgs
+8 -28
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.SyntaxError do
defexception [:file, :line, :column, :snippet, message: "syntax error"]
@@ -118,19 +114,6 @@ defmodule EEx do
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata}
@type tokenize_opt ::
{:file, binary()}
| {:line, line}
| {:column, column}
| {:indentation, non_neg_integer}
| {:trim, boolean()}
@type compile_opt ::
tokenize_opt
| {:engine, module()}
| {:parser_options, Code.parser_opts()}
| {atom(), term()}
@doc """
Generates a function definition from the given string.
@@ -141,7 +124,6 @@ defmodule EEx do
template.
The supported `options` are described [in the module docs](#module-options).
Additional options are passed to the underlying engine.
## Examples
@@ -234,11 +216,9 @@ defmodule EEx do
"3"
"""
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
@spec compile_string(String.t(), keyword) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
case tokenize(source, tokenize_opts) do
case tokenize(source, options) do
{:ok, tokens} ->
EEx.Compiler.compile(tokens, source, options)
@@ -275,7 +255,7 @@ defmodule EEx do
#=> "3"
"""
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
@spec compile_file(Path.t(), keyword) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options)
@@ -293,7 +273,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t(), keyword, [compile_opt]) :: String.t()
@spec eval_string(String.t(), keyword, keyword) :: String.t()
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options)
@@ -315,7 +295,7 @@ defmodule EEx do
#=> "foo baz"
"""
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename)
@@ -337,8 +317,8 @@ defmodule EEx do
## Examples
iex> EEx.tokenize(~c"foo", line: 1, column: 1)
{:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
iex> EEx.tokenize('foo', line: 1, column: 1)
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
## Result
@@ -355,7 +335,7 @@ defmodule EEx do
Note new tokens may be added in the future.
"""
@doc since: "1.14.0"
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
@spec tokenize([char()] | String.t(), opts :: keyword) ::
{:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts)
+15 -32
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.Compiler do
@moduledoc false
@@ -52,14 +48,8 @@ defmodule EEx.Compiler do
end
end
# TODO: Remove me on Elixir v2.0
# TODO: Deprecate this on Elixir v1.18
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
line: line,
column: column,
file: state.file
)
case expr(t, line, column + 3, state, []) do
{:error, message} ->
{:error, message, %{line: line, column: column}}
@@ -81,10 +71,10 @@ defmodule EEx.Compiler do
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} ->
{:ok, _line, _column, _warnings, tokens} ->
# We ignore warnings because the code will be tokenized
# again later with the right line+column info
token_key(rev_tokens, expr)
token_key(tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
@@ -174,8 +164,8 @@ defmodule EEx.Compiler do
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
defp token_key(tokens, expr) do
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
@@ -304,19 +294,11 @@ defmodule EEx.Compiler do
source: source,
line: line,
quoted: [],
parser_options: [indentation: indentation] ++ parser_options,
parser_options: parser_options,
indentation: indentation
}
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
@@ -334,7 +316,8 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
@@ -357,6 +340,13 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -369,13 +359,6 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
-8
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.Engine do
@moduledoc ~S"""
Basic EEx engine that ships with Elixir.
@@ -17,10 +13,6 @@ defmodule EEx.Engine do
@doc """
Called at the beginning of every template.
It receives the options during compilation, including the
ones managed by EEx, such as `:line` and `:file`, as well
as custom engine options.
It must return the initial state.
"""
@callback init(opts :: keyword) :: state
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.SmartEngine do
@moduledoc """
The default engine used by EEx.
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.MixProject do
use Mix.Project
+9 -4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__)
defmodule EEx.SmartEngineTest do
@@ -47,6 +43,15 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
+54 -16
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__)
defmodule EEx.TokenizerTest do
@@ -130,21 +126,51 @@ defmodule EEx.TokenizerTest do
end
test "EEx comments" do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end)
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end
test "EEx comments with do-end" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 31, line: 1}},
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
{:text, ~c"bar", %{column: 46, line: 1}},
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
{:ok, exprs}
end
test "EEx multi-line comments" do
@@ -295,6 +321,15 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, ~c"\n123", %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do
exprs = [
{:comment, ~c" comment ", %{column: 3, line: 1}},
@@ -349,6 +384,9 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
{:error, message, %{column: 5, line: 1}}
assert EEx.tokenize(~c"<%# true ", @opts) ==
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
message = """
expected closing '--%>' for EEx expression
|
+12 -76
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("test_helper.exs", __DIR__)
require EEx
@@ -202,6 +198,15 @@ defmodule EExTest do
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
end
test "embedded code with comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
assert_eval(
"foo\n\nbar\n",
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
)
end
test "embedded code with multi-line comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
@@ -502,59 +507,6 @@ defmodule EExTest do
end
end
test "from Elixir parser" do
line = __ENV__.line + 6
message =
assert_raise TokenMissingError, fn ->
EEx.compile_string(
"""
<li>
<strong>Some:</strong>
<%= true && @some[ %>
</li>
""",
file: __ENV__.file,
line: line,
indentation: 12
)
end
assert message |> Exception.message() |> strip_ansi() =~ """
│
514 │ true && @some[\s
│ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter
"""
end
test "from Elixir parser with line breaks" do
line = __ENV__.line + 6
message =
assert_raise TokenMissingError, fn ->
EEx.compile_string(
"""
<li>
<strong>Some:</strong>
<%= true &&
@some[ %>
</li>
""",
file: __ENV__.file,
line: line,
indentation: 12
)
end
assert message |> Exception.message() |> strip_ansi() =~ """
│
#{line + 3} │ @some[\s
│ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter
"""
end
test "honor line numbers" do
assert_raise EEx.SyntaxError,
"nofile:100:6: expected closing '%>' for EEx expression",
@@ -591,16 +543,6 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
@@ -917,13 +859,13 @@ defmodule EExTest do
file = to_charlist(Path.relative_to_cwd(__ENV__.file))
assert EExTest.Compiled.before_compile() ==
{11, {EExTest.Compiled, :before_compile, 0, [file: file, line: 11]}}
{7, {EExTest.Compiled, :before_compile, 0, [file: file, line: 7]}}
assert EExTest.Compiled.after_compile() ==
{25, {EExTest.Compiled, :after_compile, 0, [file: file, line: 25]}}
{21, {EExTest.Compiled, :after_compile, 0, [file: file, line: 21]}}
assert EExTest.Compiled.unknown() ==
{30, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 30]}}
{26, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 26]}}
end
end
@@ -1001,12 +943,6 @@ defmodule EExTest do
end
end
@strip_ansi [IO.ANSI.green(), IO.ANSI.red(), IO.ANSI.reset()]
defp strip_ansi(doc) do
String.replace(doc, @strip_ansi, "")
end
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
result = EEx.eval_string(actual, binding, opts)
-7
View File
@@ -1,13 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
{line_exclude, line_include} =
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__)
CoverageRecorder.maybe_record("eex")
ExUnit.start(
trace: !!System.get_env("TRACE"),
include: line_include,
+2 -6
View File
@@ -1,7 +1,3 @@
%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2021 The Elixir Team
%% SPDX-FileCopyrightText: 2012 Plataformatec
{'src/*', [
warn_unused_vars,
warn_export_all,
@@ -13,8 +9,8 @@
warn_deprecated_function,
warn_obsolete_guard,
warn_exported_vars,
%% Enable this when we require Erlang/OTP 27+
%% warnings_as_errors,
%% warn_missing_spec,
%% warn_untyped_record,
debug_info,
{outdir, "ebin/"}
]}.
+44 -227
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Access do
@moduledoc """
Key-based access to data structures.
@@ -39,74 +35,61 @@ defmodule Access do
iex> nil[:a]
nil
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
to allow values to be set in nested data structures:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
## Maps and structs
While the access syntax is allowed in maps via `map[key]`,
if your map is made of predefined atom keys, you should prefer
to access those atom keys with `map.key` instead of `map[key]`,
as `map.key` will raise if the key is missing (which is not
supposed to happen if the keys are predefined) or if `map` is
`nil`.
supposed to happen if the keys are predefined).
Similarly, since structs are maps and structs have predefined
keys, they only allow the `struct.key` syntax and they do not
allow the `struct[key]` access syntax.
allow the `struct[key]` access syntax. `Access.key/1` can also
be used to construct dynamic access to structs and maps.
In other words, the `map[key]` syntax is loose, returning `nil`
for missing keys, while the `map.key` syntax is strict, raising
for both nil values and missing keys.
In a nutshell, when using `put_in/2` and friends:
To bridge this gap, Elixir provides the `get_in/1` and `get_in/2`
functions, which are capable of traversing nested data structures,
even in the presence of `nil`s:
put_in(struct_or_map.key, :value)
put_in(keyword_or_map[:key], :value)
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users["john"].age)
27
iex> get_in(users["unknown"].age)
nil
When using `put_in/3` and friends:
Notice how, even if no user was found, `get_in/1` returned `nil`.
Outside of `get_in/1`, trying to access the field `.age` on `nil`
would raise.
put_in(struct_or_map, [Access.key!(:key)], :value)
put_in(keyword_or_map, [:key], :value)
The `get_in/2` function takes one step further by allowing
different accessors to be mixed in. For example, given a user
map with the `:name` and `:languages` keys, here is how to
access the name of all programming languages:
This covers the dual nature of maps in Elixir, as they can be
either for structured data or as a key-value store. See the `Map`
module for more information.
iex> languages = [
...> %{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}
...> ]
iex> user = %{name: "john", languages: languages}
iex> get_in(user, [:languages, Access.all(), :name])
["elixir", "c"]
## Nested data structures
This module provides convenience functions for traversing other
structures, like tuples and lists. As we will see next, they can
even be used to update nested data structures.
Both key-based access syntaxes can be used with the nested update
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3`.
If you want to learn more about the dual nature of maps in Elixir,
as they can be either for structured data or as a key-value store,
see the `Map` module.
## Updating nested data structures
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1`
macros to further manipulate values in nested data structures:
For example, to update a map inside another map:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"].age, 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
As shown in the previous section, you can also use the
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3` functions to provide nested
custom accessors. For instance, given a user map with the
`:name` and `:languages` keys, here is how to deeply traverse
the map and convert all language names to uppercase:
This module provides convenience functions for traversing other
structures, like tuples and lists. These functions can be used
in all the `Access`-related functions and macros in `Kernel`.
For instance, given a user map with the `:name` and `:languages`
keys, here is how to deeply traverse the map and convert all
language names to uppercase:
iex> languages = [
...> %{name: "elixir", type: :functional},
@@ -354,7 +337,7 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must access a list element by index, \
you can use Enum.at/2 or the functions in the List module\
you can Enum.at/1 or the functions in the List module\
"""
end
@@ -422,8 +405,8 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must modify a list element by index, \
you can use Access.at/1 or the functions in the List module\
as a whole. If you really must mostify a list element by index, \
you can Access.at/1 or the functions in the List module\
"""
end
@@ -517,8 +500,8 @@ defmodule Access do
An error is raised if the accessed structure is not a map or a struct:
iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got:
...
** (BadMapError) expected a map, got: []
"""
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
def key(key, default \\ nil) do
@@ -556,8 +539,7 @@ defmodule Access do
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
{"john", %{user: %{}}}
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in:
...
** (KeyError) key :unknown not found in: %{name: \"john\"}
The examples above could be partially written as:
@@ -570,7 +552,7 @@ defmodule Access do
{"john", %{user: %{name: "JOHN"}}}
However, it is not possible to remove fields using the dot notation,
as it is implied those fields must also be present. In any case,
as it is implified those fields must also be present. In any case,
`Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically.
@@ -812,7 +794,7 @@ defmodule Access do
iex> get_in([:a, :b, :c], [Access.at!(2)])
:c
iex> get_in([:a, :b, :c], [Access.at!(3)])
** (Enum.OutOfBoundsError) out of bounds error at position 3 when traversing enumerable [:a, :b, :c]
** (Enum.OutOfBoundsError) out of bounds error
"""
@doc since: "1.11.0"
@@ -824,14 +806,12 @@ defmodule Access do
defp at!(:get, data, index, next) when is_list(data) do
case Enum.fetch(data, index) do
{:ok, value} -> next.(value)
:error -> raise Enum.OutOfBoundsError, index: index, enumerable: data
:error -> raise Enum.OutOfBoundsError
end
end
defp at!(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [], fn ->
raise Enum.OutOfBoundsError, index: index, enumerable: data
end)
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
end
defp at!(_op, data, _index, _next) do
@@ -891,7 +871,7 @@ defmodule Access do
end
defp filter(:get, data, func, next) when is_list(data) do
for elem <- data, func.(elem), do: next.(elem)
data |> Enum.filter(func) |> Enum.map(next)
end
defp filter(:get_and_update, data, func, next) when is_list(data) do
@@ -999,93 +979,6 @@ defmodule Access do
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
end
@doc """
Returns a function that accesses all values in a map or a keyword list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users, [Access.values(), :age]) |> Enum.sort()
[23, 27]
iex> update_in(users, [Access.values(), :age], fn age -> age + 1 end)
%{"john" => %{age: 28}, "meg" => %{age: 24}}
iex> put_in(users, [Access.values(), :planet], "Earth")
%{"john" => %{age: 27, planet: "Earth"}, "meg" => %{age: 23, planet: "Earth"}}
Values in keyword lists can be accessed as well:
iex> users = [john: %{age: 27}, meg: %{age: 23}]
iex> get_and_update_in(users, [Access.values(), :age], fn age -> {age, age + 1} end)
{[27, 23], [john: %{age: 28}, meg: %{age: 24}]}
By returning `:pop` from an accessor function, you can remove the accessed key and value
from the map or keyword list:
iex> require Integer
iex> numbers = [one: 1, two: 2, three: 3, four: 4]
iex> get_and_update_in(numbers, [Access.values()], fn num ->
...> if Integer.is_even(num), do: :pop, else: {num, to_string(num)}
...> end)
{[1, 2, 3, 4], [one: "1", three: "3"]}
An error is raised if the accessed structure is not a map nor a keyword list:
iex> get_in([1, 2, 3], [Access.values()])
** (RuntimeError) Access.values/0 expected a map or a keyword list, got: [1, 2, 3]
"""
@doc since: "1.19.0"
@spec values() :: Access.access_fun(data :: map() | keyword(), current_value :: list())
def values do
&values/3
end
defp values(:get, data = %{}, next) do
Enum.map(data, fn {_key, value} -> next.(value) end)
end
defp values(:get_and_update, data = %{}, next) do
{reverse_gets, updated_data} =
Enum.reduce(data, {[], %{}}, fn {key, value}, {gets, data_acc} ->
case next.(value) do
{get, update} -> {[get | gets], Map.put(data_acc, key, update)}
:pop -> {[value | gets], data_acc}
end
end)
{Enum.reverse(reverse_gets), updated_data}
end
defp values(op, data = [], next) do
values_keyword(op, data, next)
end
defp values(op, data = [{key, _value} | _tail], next) when is_atom(key) do
values_keyword(op, data, next)
end
defp values(_op, data, _next) do
raise "Access.values/0 expected a map or a keyword list, got: #{inspect(data)}"
end
defp values_keyword(:get, data, next) do
Enum.map(data, fn {key, value} when is_atom(key) -> next.(value) end)
end
defp values_keyword(:get_and_update, data, next) do
{reverse_gets, reverse_updated_data} =
Enum.reduce(data, {[], []}, fn {key, value}, {gets, data_acc} when is_atom(key) ->
case next.(value) do
{get, update} -> {[get | gets], [{key, update} | data_acc]}
:pop -> {[value | gets], data_acc}
end
end)
{Enum.reverse(reverse_gets), Enum.reverse(reverse_updated_data)}
end
defp normalize_range(%Range{first: first, last: last, step: step}, list)
when first < 0 or last < 0 do
count = length(list)
@@ -1120,80 +1013,4 @@ defmodule Access do
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
@doc ~S"""
Returns a function that accesses the first element of a list that matches the provided predicate.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
"francine"
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
`find/1` can also be used to pop the first found element out of a list or
a key inside of a list:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
nil
iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
** (RuntimeError) Access.find/1 expected a list, got: %{}
"""
@doc since: "1.17.0"
@spec find((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: term)
def find(predicate) when is_function(predicate, 1) do
fn op, data, next -> find(op, data, predicate, next) end
end
defp find(:get, data, predicate, next) when is_list(data) do
data |> Enum.find(predicate) |> next.()
end
defp find(:get_and_update, data, predicate, next) when is_list(data) do
get_and_update_find(data, [], predicate, next)
end
defp find(_op, data, _predicate, _next) do
raise "Access.find/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_find([], updates, _predicate, _next) do
{nil, :lists.reverse(updates)}
end
defp get_and_update_find([head | rest], updates, predicate, next) do
if predicate.(head) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
else
get_and_update_find(rest, [head | updates], predicate, next)
end
end
end
+5 -9
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Agent do
@moduledoc """
Agents are a simple abstraction around state.
@@ -211,7 +207,7 @@ defmodule Agent do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -288,7 +284,7 @@ defmodule Agent do
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
"""
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options)
end
@@ -315,7 +311,7 @@ defmodule Agent do
See `start_link/4` for more information.
"""
@spec start(module, atom, [term], GenServer.options()) :: on_start
@spec start(module, atom, [any], GenServer.options()) :: on_start
def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options)
end
@@ -352,7 +348,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get(agent, module, atom, [term], timeout) :: term
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
end
@@ -393,7 +389,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: term
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Agent.Server do
@moduledoc false
+36 -44
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Application do
@moduledoc """
A module for working with applications and defining application callbacks.
@@ -17,7 +13,7 @@ defmodule Application do
Developers typically interact with the application environment and its
callback module. Therefore those will be the topics we will cover first
before jumping into details about the application resource file and life cycle.
before jumping into details about the application resource file and life-cycle.
## The application environment
@@ -58,12 +54,12 @@ defmodule Application do
You can also change the application environment dynamically by using functions
such as `put_env/3` and `delete_env/2`.
> #### Application environment in libraries {: .info}
> #### Environment in libraries {: .tip}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
> The config files `config/config.exs` and `config/runtime.exs`
> are rarely used by libraries. Libraries typically define their environment
> in the `application/0` function of their `mix.exs`. Configuration files
> are rather used by applications to configure their libraries.
> #### Reading the environment of other applications {: .warning}
>
@@ -247,7 +243,7 @@ defmodule Application do
invoked if it hasn't been done yet. Then, it checks if the dependencies listed
in the `applications` key of the resource file are already started. Having at
least one dependency not started is an error condition. Functions like
`ensure_all_started/1` take care of starting an application and all of its
`ensure_all_started/1` takes care of starting an application and all of its
dependencies for you.
If the application does not have a callback module configured, starting is
@@ -262,8 +258,8 @@ defmodule Application do
Application.stop(:ex_unit)
#=> :ok
Stopping an application without a callback module defined, is in practice a
no-op, except for some system tracing.
Stopping an application without a callback module is defined, but except for
some system tracing, it is in practice a no-op.
Stopping an application with a callback module has three steps:
@@ -281,7 +277,7 @@ defmodule Application do
invoked only after termination of the whole supervision tree.
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
will shut down every application in the reverse order they were started.
will shut down every application in the opposite order they had been started.
By default, a SIGTERM from the operating system will automatically translate to
`System.stop/0`. You can also have more explicit control over operating system
@@ -677,6 +673,13 @@ defmodule Application do
> You must use this function to read only your own application
> environment. Do not read the environment of other applications.
> #### Application environment in libraries {: .info}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read our [library guidelines](library-guidelines.md).
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
@@ -793,12 +796,6 @@ defmodule Application do
@doc """
Puts the `value` in `key` for the given `app`.
> #### Compile environment {: .warning}
>
> Do not use this function to change environment variables read
> via `Application.compile_env/2`. The compile environment must
> be exclusively set before compilation, in your config files.
## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
@@ -820,29 +817,16 @@ defmodule Application do
end
@doc """
Puts the environment for multiple applications at the same time.
Puts the environment for multiple apps at the same time.
The given config should not:
* have the same application listed more than once
* have the same key inside the same application listed more than once
If those conditions are not met, this function will raise.
This function receives the same options as `put_env/4`. Returns `:ok`.
## Examples
Application.put_all_env(
my_app: [
key: :value,
another_key: :another_value
],
another_app: [
key: :value
]
)
If those conditions are not met, it will raise.
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@doc since: "1.9.0"
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
@@ -866,10 +850,7 @@ defmodule Application do
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
defp maybe_warn_on_app_env_key(app, key) do
message = fn ->
"passing non-atom as application env key is deprecated, got: #{inspect(key)}"
end
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
end
@@ -908,11 +889,11 @@ defmodule Application do
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:type` - if the application should be started in `:permanent`,
`:temporary`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`).
or concurrently. This option requires Erlang/OTP 26+.
"""
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
@@ -933,7 +914,18 @@ defmodule Application do
def ensure_all_started(apps, opts) when is_list(apps) and is_list(opts) do
opts = Keyword.validate!(opts, type: :temporary, mode: :serial)
:application.ensure_all_started(apps, opts[:type], opts[:mode])
if function_exported?(:application, :ensure_all_started, 3) do
:application.ensure_all_started(apps, opts[:type], opts[:mode])
else
# TODO: Remove this clause when we require Erlang/OTP 26+
Enum.reduce_while(apps, {:ok, []}, fn app, {:ok, acc} ->
case :application.ensure_all_started(app, opts[:type]) do
{:ok, apps} -> {:cont, {:ok, apps ++ acc}}
{:error, e} -> {:halt, {:error, e}}
end
end)
end
end
@doc """
+1 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Atom do
@moduledoc """
Atoms are constants whose values are their own name.
@@ -71,7 +67,7 @@ defmodule Atom do
## Examples
iex> Atom.to_charlist(:"An atom")
~c"An atom"
'An atom'
"""
@spec to_charlist(atom) :: charlist
+150 -583
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Base do
import Bitwise
@@ -319,126 +315,58 @@ defmodule Base do
"Double check your string for unwanted characters or pad it accordingly"
end
@doc """
Checks if a string is a valid base 16 encoded string.
> #### When to use this {: .tip}
>
> Use this function when you just need to *validate* that a string is
> valid base 16 data, without actually producing a decoded output string.
> This function is both more performant and memory efficient than using
> `decode16/2`, checking that the result is `{:ok, ...}`, and then
> discarding the decoded binary.
## Options
Accepts the same options as `decode16/2`.
## Examples
iex> Base.valid16?("666F6F626172")
true
iex> Base.valid16?("666f6f626172", case: :lower)
true
iex> Base.valid16?("666f6F626172", case: :mixed)
true
iex> Base.valid16?("ff", case: :upper)
false
"""
@doc since: "1.19.0"
@spec valid16?(binary, case: decode_case) :: boolean
def valid16?(string, opts \\ [])
def valid16?(string, opts) when is_binary(string) and rem(byte_size(string), 2) == 0 do
case Keyword.get(opts, :case, :upper) do
:upper -> validate16upper?(string)
:lower -> validate16lower?(string)
:mixed -> validate16mixed?(string)
end
end
def valid16?(string, _opts) when is_binary(string) do
false
end
upper = Enum.with_index(b16_alphabet)
for {base, alphabet} <- [upper: upper, lower: to_lower_dec.(upper), mixed: to_mixed_dec.(upper)] do
decode_name = :"decode16#{base}!"
validate_name = :"validate16#{base}?"
valid_char_name = :"valid_char16#{base}?"
name = :"decode16#{base}!"
{min, decoded} = to_decode_list.(alphabet)
defp unquote(validate_name)(<<>>), do: true
defp unquote(validate_name)(<<c1, c2, rest::binary>>) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(validate_name)(rest)
end
defp unquote(validate_name)(<<_char, _rest::binary>>), do: false
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
cond do
index not in 0..unquote(length(decoded) - 1) -> bad_character!(char)
new_char = elem({unquote_splicing(decoded)}, index) -> new_char
true -> bad_character!(char)
defp unquote(name)(char) do
try do
elem({unquote_splicing(decoded)}, char - unquote(min))
rescue
_ -> bad_character!(char)
else
nil -> bad_character!(char)
char -> char
end
end
defp unquote(decode_name)(<<c1, c2, c3, c4, c5, c6, c7, c8, rest::binary>>, acc) do
unquote(decode_name)(
defp unquote(name)(<<c1, c2, c3, c4, c5, c6, c7, c8, rest::binary>>, acc) do
unquote(name)(
rest,
<<
acc::binary,
unquote(decode_name)(c1)::4,
unquote(decode_name)(c2)::4,
unquote(decode_name)(c3)::4,
unquote(decode_name)(c4)::4,
unquote(decode_name)(c5)::4,
unquote(decode_name)(c6)::4,
unquote(decode_name)(c7)::4,
unquote(decode_name)(c8)::4
unquote(name)(c1)::4,
unquote(name)(c2)::4,
unquote(name)(c3)::4,
unquote(name)(c4)::4,
unquote(name)(c5)::4,
unquote(name)(c6)::4,
unquote(name)(c7)::4,
unquote(name)(c8)::4
>>
)
end
defp unquote(decode_name)(<<c1, c2, c3, c4, rest::binary>>, acc) do
unquote(decode_name)(
defp unquote(name)(<<c1, c2, c3, c4, rest::binary>>, acc) do
unquote(name)(
rest,
<<
acc::binary,
unquote(decode_name)(c1)::4,
unquote(decode_name)(c2)::4,
unquote(decode_name)(c3)::4,
unquote(decode_name)(c4)::4
unquote(name)(c1)::4,
unquote(name)(c2)::4,
unquote(name)(c3)::4,
unquote(name)(c4)::4
>>
)
end
defp unquote(decode_name)(<<c1::8, c2::8, rest::binary>>, acc) do
unquote(decode_name)(
rest,
<<acc::binary, unquote(decode_name)(c1)::4, unquote(decode_name)(c2)::4>>
)
defp unquote(name)(<<c1::8, c2::8, rest::binary>>, acc) do
unquote(name)(rest, <<acc::binary, unquote(name)(c1)::4, unquote(name)(c2)::4>>)
end
defp unquote(decode_name)(<<>>, acc) do
defp unquote(name)(<<>>, acc) do
acc
end
end
@@ -627,40 +555,6 @@ defmodule Base do
string |> remove_ignored(opts[:ignore]) |> decode64base!(pad?)
end
@doc """
Validates a base 64 encoded string.
> #### When to use this {: .tip}
>
> Use this function when you just need to *validate* that a string is
> valid base 64 data, without actually producing a decoded output string.
> This function is both more performant and memory efficient than using
> `decode64/2`, checking that the result is `{:ok, ...}`, and then
> discarding the decoded binary.
## Options
Accepts the same options as `decode64/2`.
## Examples
iex> Base.valid64?("Zm9vYmFy")
true
iex> Base.valid64?("Zm9vYmFy\\n", ignore: :whitespace)
true
iex> Base.valid64?("Zm9vYg==")
true
"""
@doc since: "1.19.0"
@spec valid64?(binary, ignore: :whitespace, padding: boolean) :: boolean
def valid64?(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
string |> remove_ignored(opts[:ignore]) |> validate64base?(pad?)
end
@doc """
Decodes a base 64 encoded string with URL and filename safe alphabet
into a binary string.
@@ -721,266 +615,120 @@ defmodule Base do
string |> remove_ignored(opts[:ignore]) |> decode64url!(pad?)
end
@doc """
Validates a base 64 encoded string with URL and filename safe alphabet.
> #### When to use this {: .tip}
>
> Use this function when you just need to *validate* that a string is
> valid (URL-safe) base 64 data, without actually producing a decoded
> output string. This function is both more performant and memory efficient
> than using `url_decode64/2`, checking that the result is `{:ok, ...}`,
> and then discarding the decoded binary.
## Options
Accepts the same options as `url_decode64/2`.
## Examples
iex> Base.url_valid64?("_3_-_A==")
true
iex> Base.url_valid64?("_3_-_A==\\n", ignore: :whitespace)
true
iex> Base.url_valid64?("_3_-_A", padding: false)
true
"""
@doc since: "1.19.0"
@spec url_valid64?(binary, ignore: :whitespace, padding: boolean) :: boolean
def url_valid64?(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
string |> remove_ignored(opts[:ignore]) |> validate64url?(pad?)
end
for {base, alphabet} <- [base: b64_alphabet, url: b64url_alphabet] do
decode_name = :"decode64#{base}!"
validate_name = :"validate64#{base}?"
validate_main_name = :"validate_main64#{validate_name}?"
valid_char_name = :"valid_char64#{base}?"
name = :"decode64#{base}!"
{min, decoded} = alphabet |> Enum.with_index() |> to_decode_list.()
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_name)(<<>>, _pad?), do: true
defp unquote(validate_name)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(^segs)-binary-unit(64), rest::binary>> = string
main_valid? = unquote(validate_main_name)(main)
case rest do
_ when not main_valid? ->
false
<<c1::8, c2::8, ?=, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2)
<<c1::8, c2::8, c3::8, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3)
<<c1::8, c2::8, c3::8, c4::8>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, ?=, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8)
<<c1::8, c2::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2)
<<c1::8, c2::8, c3::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7)
_ ->
false
defp unquote(name)(char) do
try do
elem({unquote_splicing(decoded)}, char - unquote(min))
rescue
_ -> bad_character!(char)
else
nil -> bad_character!(char)
char -> char
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(name)(<<>>, _pad?), do: <<>>
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
cond do
index not in 0..unquote(length(decoded) - 1) -> bad_character!(char)
new_char = elem({unquote_splicing(decoded)}, index) -> new_char
true -> bad_character!(char)
end
end
defp unquote(decode_name)(<<>>, _pad?), do: <<>>
defp unquote(decode_name)(string, pad?) do
defp unquote(name)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(^segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
unquote(decode_name)(c6)::6,
unquote(decode_name)(c7)::6,
unquote(decode_name)(c8)::6
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
unquote(name)(c6)::6,
unquote(name)(c7)::6,
unquote(name)(c8)::6
>>
end
case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<main::bits, unquote(decode_name)(c1)::6, bsr(unquote(decode_name)(c2), 4)::2>>
<<main::bits, unquote(name)(c1)::6, bsr(unquote(name)(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<main::bits, unquote(decode_name)(c1)::6, unquote(decode_name)(c2)::6,
bsr(unquote(decode_name)(c3), 2)::4>>
<<main::bits, unquote(name)(c1)::6, unquote(name)(c2)::6, bsr(unquote(name)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, ?=, ?=>> ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
bsr(unquote(decode_name)(c6), 4)::2
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
bsr(unquote(name)(c6), 4)::2
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
unquote(decode_name)(c6)::6,
bsr(unquote(decode_name)(c7), 2)::4
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
unquote(name)(c6)::6,
bsr(unquote(name)(c7), 2)::4
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
unquote(decode_name)(c6)::6,
unquote(decode_name)(c7)::6,
unquote(decode_name)(c8)::6
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
unquote(name)(c6)::6,
unquote(name)(c7)::6,
unquote(name)(c8)::6
>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(decode_name)(c1)::6, bsr(unquote(decode_name)(c2), 4)::2>>
<<main::bits, unquote(name)(c1)::6, bsr(unquote(name)(c2), 4)::2>>
<<c1::8, c2::8, c3::8>> when not pad? ->
<<main::bits, unquote(decode_name)(c1)::6, unquote(decode_name)(c2)::6,
bsr(unquote(decode_name)(c3), 2)::4>>
<<main::bits, unquote(name)(c1)::6, unquote(name)(c2)::6, bsr(unquote(name)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> when not pad? ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
bsr(unquote(decode_name)(c6), 4)::2
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
bsr(unquote(name)(c6), 4)::2
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<
main::bits,
unquote(decode_name)(c1)::6,
unquote(decode_name)(c2)::6,
unquote(decode_name)(c3)::6,
unquote(decode_name)(c4)::6,
unquote(decode_name)(c5)::6,
unquote(decode_name)(c6)::6,
bsr(unquote(decode_name)(c7), 2)::4
unquote(name)(c1)::6,
unquote(name)(c2)::6,
unquote(name)(c3)::6,
unquote(name)(c4)::6,
unquote(name)(c5)::6,
unquote(name)(c6)::6,
bsr(unquote(name)(c7), 2)::4
>>
_ ->
@@ -1235,45 +983,6 @@ defmodule Base do
end
end
@doc """
Checks if a base 32 encoded string is valid.
> #### When to use this {: .tip}
>
> Use this function when you just need to *validate* that a string is
> valid base 32 data, without actually producing a decoded output string.
> This function is both more performant and memory efficient than using
> `decode32/2`, checking that the result is `{:ok, ...}`, and then
> discarding the decoded binary.
## Options
Accepts the same options as `decode32/2`.
## Examples
iex> Base.valid32?("MZXW6YTBOI======")
true
iex> Base.valid32?("mzxw6ytboi======", case: :lower)
true
iex> Base.valid32?("zzz")
false
"""
@doc since: "1.19.0"
@spec valid32?(binary, case: decode_case, padding: boolean) :: boolean()
def valid32?(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
case Keyword.get(opts, :case, :upper) do
:upper -> validate32upper?(string, pad?)
:lower -> validate32lower?(string, pad?)
:mixed -> validate32mixed?(string, pad?)
end
end
@doc """
Decodes a base 32 encoded string with extended hexadecimal alphabet
into a binary string.
@@ -1369,45 +1078,6 @@ defmodule Base do
end
end
@doc """
Checks if a base 32 encoded string with extended hexadecimal alphabet is valid.
> #### When to use this {: .tip}
>
> Use this function when you just need to *validate* that a string is
> valid (extended hexadecimal) base 32 data, without actually producing
> a decoded output string. This function is both more performant and
> memory efficient than using `hex_decode32/2`, checking that the result
> is `{:ok, ...}`, and then discarding the decoded binary.
## Options
Accepts the same options as `hex_decode32/2`.
## Examples
iex> Base.hex_valid32?("CPNMUOJ1E8======")
true
iex> Base.hex_valid32?("cpnmuoj1e8======", case: :lower)
true
iex> Base.hex_valid32?("zzz", padding: false)
false
"""
@doc since: "1.19.0"
@spec hex_valid32?(binary, case: decode_case, padding: boolean) :: boolean
def hex_valid32?(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
case Keyword.get(opts, :case, :upper) do
:upper -> validate32hexupper?(string, pad?)
:lower -> validate32hexlower?(string, pad?)
:mixed -> validate32hexmixed?(string, pad?)
end
end
upper = Enum.with_index(b32_alphabet)
hexupper = Enum.with_index(b32hex_alphabet)
@@ -1419,223 +1089,120 @@ defmodule Base do
hexlower: to_lower_dec.(hexupper),
hexmixed: to_mixed_dec.(hexupper)
] do
decode_name = :"decode32#{base}!"
validate_name = :"validate32#{base}?"
validate_main_name = :"validate_main32#{validate_name}?"
valid_char_name = :"valid_char32#{base}?"
name = :"decode32#{base}!"
{min, decoded} = to_decode_list.(alphabet)
defp unquote(validate_main_name)(<<>>), do: true
defp unquote(validate_main_name)(
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8, rest::binary>>
) do
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8) and
unquote(validate_main_name)(rest)
end
defp unquote(validate_name)(<<>>, _pad?), do: true
defp unquote(validate_name)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(^segs)-binary-unit(64), rest::binary>> = string
main_valid? = unquote(validate_main_name)(main)
case rest do
_ when not main_valid? ->
false
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2)
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4)
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7) and
unquote(valid_char_name)(c8)
<<c1::8, c2::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2)
<<c1::8, c2::8, c3::8, c4::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4)
<<c1::8, c2::8, c3::8, c4::8, c5::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5)
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
unquote(valid_char_name)(c1) and
unquote(valid_char_name)(c2) and
unquote(valid_char_name)(c3) and
unquote(valid_char_name)(c4) and
unquote(valid_char_name)(c5) and
unquote(valid_char_name)(c6) and
unquote(valid_char_name)(c7)
_ ->
false
defp unquote(name)(char) do
try do
elem({unquote_splicing(decoded)}, char - unquote(min))
rescue
_ -> bad_character!(char)
else
nil -> bad_character!(char)
char -> char
end
end
@compile {:inline, [{valid_char_name, 1}]}
defp unquote(valid_char_name)(char)
when elem({unquote_splicing(decoded)}, char - unquote(min)) != nil,
do: true
defp unquote(name)(<<>>, _), do: <<>>
defp unquote(valid_char_name)(_char), do: false
defp unquote(decode_name)(char) do
index = char - unquote(min)
cond do
index not in 0..unquote(length(decoded) - 1) -> bad_character!(char)
new_char = elem({unquote_splicing(decoded)}, index) -> new_char
true -> bad_character!(char)
end
end
defp unquote(decode_name)(<<>>, _), do: <<>>
defp unquote(decode_name)(string, pad?) do
defp unquote(name)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(^segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
unquote(decode_name)(c5)::5,
unquote(decode_name)(c6)::5,
unquote(decode_name)(c7)::5,
unquote(decode_name)(c8)::5
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
unquote(name)(c5)::5,
unquote(name)(c6)::5,
unquote(name)(c7)::5,
unquote(name)(c8)::5
>>
end
case rest do
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
<<main::bits, unquote(decode_name)(c1)::5, bsr(unquote(decode_name)(c2), 2)::3>>
<<main::bits, unquote(name)(c1)::5, bsr(unquote(name)(c2), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
bsr(unquote(decode_name)(c4), 4)::1
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
bsr(unquote(name)(c4), 4)::1
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
bsr(unquote(decode_name)(c5), 1)::4
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
bsr(unquote(name)(c5), 1)::4
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
unquote(decode_name)(c5)::5,
unquote(decode_name)(c6)::5,
bsr(unquote(decode_name)(c7), 3)::2
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
unquote(name)(c5)::5,
unquote(name)(c6)::5,
bsr(unquote(name)(c7), 3)::2
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
unquote(decode_name)(c5)::5,
unquote(decode_name)(c6)::5,
unquote(decode_name)(c7)::5,
unquote(decode_name)(c8)::5
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
unquote(name)(c5)::5,
unquote(name)(c6)::5,
unquote(name)(c7)::5,
unquote(name)(c8)::5
>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(decode_name)(c1)::5, bsr(unquote(decode_name)(c2), 2)::3>>
<<main::bits, unquote(name)(c1)::5, bsr(unquote(name)(c2), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8>> when not pad? ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
bsr(unquote(decode_name)(c4), 4)::1
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
bsr(unquote(name)(c4), 4)::1
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8>> when not pad? ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
bsr(unquote(decode_name)(c5), 1)::4
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
bsr(unquote(name)(c5), 1)::4
>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<
main::bits,
unquote(decode_name)(c1)::5,
unquote(decode_name)(c2)::5,
unquote(decode_name)(c3)::5,
unquote(decode_name)(c4)::5,
unquote(decode_name)(c5)::5,
unquote(decode_name)(c6)::5,
bsr(unquote(decode_name)(c7), 3)::2
unquote(name)(c1)::5,
unquote(name)(c2)::5,
unquote(name)(c3)::5,
unquote(name)(c4)::5,
unquote(name)(c5)::5,
unquote(name)(c6)::5,
bsr(unquote(name)(c7), 3)::2
>>
_ ->
+1 -6
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Behaviour do
@moduledoc """
Mechanism for handling behaviours.
@@ -13,8 +9,7 @@ defmodule Behaviour do
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
is documented in `Module`.
`MyModule.behaviour_info(:callbacks)` can be used.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
+1 -5
View File
@@ -1,12 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Bitwise do
@moduledoc """
A set of functions that perform calculations on bits.
All bitwise functions work only on integers, otherwise an
All bitwise functions work only on integers; otherwise an
`ArithmeticError` is raised. The functions `band/2`,
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
+24 -133
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do
@moduledoc """
This module defines the responsibilities for working with
@@ -58,20 +54,9 @@ defmodule Calendar do
@typedoc """
Microseconds with stored precision.
`value` always represents the total value in microseconds.
The `precision` represents the number of digits that must be used when
The precision represents the number of digits that must be used when
representing the microseconds to external format. If the precision is `0`,
it means microseconds must be skipped. If the precision is `6`, it means
that `value` represents exactly the number of microseconds to be used.
## Examples
* `{0, 0}` means no microseconds.
* `{1, 6}` means 1µs.
* `{1000, 6}` means 1000µs (which is 1ms but measured at the microsecond precision).
* `{1000, 3}` means 1ms (which is measured at the millisecond precision).
it means microseconds must be skipped.
"""
@type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer}
@@ -85,14 +70,14 @@ defmodule Calendar do
@type zone_abbr :: String.t()
@typedoc """
The time zone UTC offset in ISO seconds for standard time.
The time zone UTC offset in seconds for standard time.
See also `t:std_offset/0`.
"""
@type utc_offset :: integer
@typedoc """
The time zone standard offset in ISO seconds (typically not zero in summer times).
The time zone standard offset in seconds (typically not zero in summer times).
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
"""
@@ -162,22 +147,6 @@ defmodule Calendar do
"""
@type time_zone_database :: module()
@typedoc """
Options for formatting dates and times with `strftime/3`.
"""
@type strftime_opts :: [
preferred_datetime: String.t(),
preferred_date: String.t(),
preferred_time: String.t(),
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_month_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_day_of_week_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
]
@doc """
Returns how many days there are in the given month of the given year.
"""
@@ -203,15 +172,6 @@ defmodule Calendar do
`starting_on` represents the starting day of the week. All
calendars must support at least the `:default` value. They may
also support other values representing their days of the week.
The value of `day_of_week` is an ordinal number meaning that a
value of `1` is defined to mean "first day of the week". It is
specifically not defined to mean `1` is `Monday`.
It is a requirement that `first_day_of_week` is less than `last_day_of_week`
and that `day_of_week` must be within that range. Therefore it can be said
that `day_of_week in first_day_of_week..last_day_of_week//1` must be
`true` for all values of `day_of_week`.
"""
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
{day_of_week(), first_day_of_week :: non_neg_integer(),
@@ -378,34 +338,6 @@ defmodule Calendar do
@doc since: "1.15.0"
@callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """
Shifts date by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """
Shifts naive datetime by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Shifts time by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
# General Helpers
@doc """
@@ -492,30 +424,25 @@ defmodule Calendar do
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
if the option is not received
* `:am_pm_names` - a function that receives either `:am` or `:pm`
(and also the datetime if the function is arity/2) and returns
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
the name of the period of the day, if the option is not received it defaults
to a function that returns `"am"` and `"pm"`, respectively
* `:month_names` - a function that receives a number (and also the
datetime if the function is arity/2) and returns the name of
* `:month_names` - a function that receives a number and returns the name of
the corresponding month, if the option is not received it defaults to a
function that returns the month names in English
* `:abbreviated_month_names` - a function that receives a number (and also
the datetime if the function is arity/2) and returns the
* `:abbreviated_month_names` - a function that receives a number and returns the
abbreviated name of the corresponding month, if the option is not received it
defaults to a function that returns the abbreviated month names in English
* `:day_of_week_names` - a function that receives a number and (and also the
datetime if the function is arity/2) returns the name of
* `:day_of_week_names` - a function that receives a number and returns the name of
the corresponding day of week, if the option is not received it defaults to a
function that returns the day of week names in English
* `:abbreviated_day_of_week_names` - a function that receives a number (and also
the datetime if the function is arity/2) and returns the abbreviated name of
the corresponding day of week, if the option is not received it defaults to a
function that returns the abbreviated day of week names in English
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
the abbreviated name of the corresponding day of week, if the option is not received
it defaults to a function that returns the abbreviated day of week names in English
## Formatting syntax
@@ -549,7 +476,7 @@ defmodule Calendar do
B | Full month name | January
c | Preferred date+time representation | 2018-10-17 12:34:56
d | Day of the month | 01, 31
f | Microseconds (uses its precision for width and padding) | 000000, 999999, 0123
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
H | Hour using a 24-hour clock | 00, 23
I | Hour using a 12-hour clock | 01, 12
j | Day of the year | 001, 366
@@ -571,12 +498,6 @@ defmodule Calendar do
Any other character will be interpreted as an invalid format and raise an error.
### `%f` Microseconds
`%f` does not support width and padding modifiers. It will be formatted by truncating
the microseconds to the precision of the `microseconds` field of the struct, with a
minimum precision of 1.
## Examples
Without user options:
@@ -620,20 +541,9 @@ defmodule Calendar do
...>)
"серпень"
Microsecond formatting:
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.0"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.048"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048531Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.048531"
"""
@doc since: "1.11.0"
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
@spec strftime(map(), String.t(), keyword()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse(
@@ -686,12 +596,12 @@ defmodule Calendar do
format_modifiers(rest, width, pad, datetime, format_options, acc)
end
defp am_pm(hour, format_options, datetime) when hour > 11 do
apply_format(:pm, format_options.am_pm_names, datetime)
defp am_pm(hour, format_options) when hour > 11 do
format_options.am_pm_names.(:pm)
end
defp am_pm(hour, format_options, datetime) when hour <= 11 do
apply_format(:am, format_options.am_pm_names, datetime)
defp am_pm(hour, format_options) when hour <= 11 do
format_options.am_pm_names.(:am)
end
defp default_pad(format) when format in ~c"aAbBpPZ", do: ?\s
@@ -712,7 +622,7 @@ defmodule Calendar do
result =
datetime
|> Date.day_of_week()
|> apply_format(format_options.abbreviated_day_of_week_names, datetime)
|> format_options.abbreviated_day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
@@ -723,7 +633,7 @@ defmodule Calendar do
result =
datetime
|> Date.day_of_week()
|> apply_format(format_options.day_of_week_names, datetime)
|> format_options.day_of_week_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
@@ -733,7 +643,7 @@ defmodule Calendar do
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.month
|> apply_format(format_options.abbreviated_month_names, datetime)
|> format_options.abbreviated_month_names.()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
@@ -741,10 +651,7 @@ defmodule Calendar do
# Full month name
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.month
|> apply_format(format_options.month_names, datetime)
|> pad_leading(width, pad)
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -798,7 +705,7 @@ defmodule Calendar do
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -822,11 +729,7 @@ defmodule Calendar do
# "AM" or "PM" (noon is "PM", midnight as "AM")
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.hour
|> am_pm(format_options, datetime)
|> String.upcase()
|> pad_leading(width, pad)
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
@@ -835,7 +738,7 @@ defmodule Calendar do
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
result =
datetime.hour
|> am_pm(format_options, datetime)
|> am_pm(format_options)
|> String.downcase()
|> pad_leading(width, pad)
@@ -1001,18 +904,6 @@ defmodule Calendar do
defp do_pad_leading(count, padding, acc),
do: do_pad_leading(count - 1, padding, [padding | acc])
defp apply_format(term, formatter, _datetime) when is_function(formatter, 1) do
formatter.(term)
end
defp apply_format(term, formatter, datetime) when is_function(formatter, 2) do
formatter.(term, datetime)
end
defp apply_format(_term, formatter, _datetime) do
raise ArgumentError, "formatter functions must be of arity 1 or 2, got: #{inspect(formatter)}"
end
defp options(user_options) do
default_options = %{
preferred_date: "%Y-%m-%d",
+30 -127
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Date do
@moduledoc """
A Date struct and functions.
@@ -35,17 +31,16 @@ defmodule Date do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `Date` struct fields. For proper comparison between
dates, use the `compare/2`, `after?/2` and `before?/2` functions.
The existence of the `compare/2` function in this module also allows
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
maximum date of an `Enum`. For example:
dates, use the `compare/2` function. The existence of the `compare/2`
function in this module also allows using `Enum.min/2` and `Enum.max/2`
functions to get the minimum and maximum date of an `Enum`. For example:
iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date)
iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date)
~D[2017-03-31]
## Using epochs
The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates
The `add/2` and `diff/2` functions can be used for computing dates
or retrieving the number of days between instants. For example, if there
is an interest in computing the number of days from the Unix epoch
(1970-01-01):
@@ -56,9 +51,6 @@ defmodule Date do
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
~D[2010-04-17]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01).
"""
@@ -193,8 +185,9 @@ defmodule Date do
end
def utc_today(calendar) do
%{year: year, month: month, day: day} = DateTime.utc_now(calendar)
%Date{year: year, month: month, day: day, calendar: calendar}
calendar
|> DateTime.utc_now()
|> DateTime.to_date()
end
@doc """
@@ -321,7 +314,7 @@ defmodule Date do
@doc """
Converts the given date to a string according to its calendar.
## Examples
### Examples
iex> Date.to_string(~D[2000-02-28])
"2000-02-28"
@@ -399,7 +392,7 @@ defmodule Date do
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`.
## Examples
### Examples
iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28"
@@ -633,7 +626,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -667,7 +660,7 @@ defmodule Date do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
@@ -691,15 +684,8 @@ defmodule Date do
@doc """
Adds the number of days to the given `date`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
>
> `add/2` always considers a day to be measured according to the
> `Calendar.ISO`.
The days are counted as Gregorian days, independent of the underlying
calendar. The date is returned in the same calendar as it was given in.
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
## Examples
@@ -717,7 +703,12 @@ defmodule Date do
@spec add(Calendar.date(), integer()) :: t
def add(%{calendar: Calendar.ISO} = date, days) do
%{year: year, month: month, day: day} = date
{year, month, day} = Calendar.ISO.shift_days({year, month, day}, days)
{year, month, day} =
Calendar.ISO.date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> Calendar.ISO.date_from_iso_days()
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
@@ -766,81 +757,6 @@ defmodule Date do
end
end
@doc """
Shifts given `date` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months and then days:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
When shifting by month, days are rounded down to the nearest valid date.
Raises an `ArgumentError` when called with time scale units.
## Examples
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
iex> Date.shift(~D[2016-01-30], month: -1)
~D[2015-12-30]
iex> Date.shift(~D[2016-01-31], year: 4, day: 1)
~D[2020-02-01]
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
# leap years
iex> Date.shift(~D[2024-02-29], year: 1)
~D[2025-02-28]
iex> Date.shift(~D[2024-02-29], year: 4)
~D[2028-02-29]
# rounding down
iex> Date.shift(~D[2015-01-31], month: 1)
~D[2015-02-28]
"""
@doc since: "1.17.0"
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
def shift(%{calendar: calendar} = date, duration) do
%{year: year, month: month, day: day} = date
{year, month, day} = calendar.shift_date(year, month, day, __duration__!(duration))
%Date{calendar: calendar, year: year, month: month, day: day}
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({unit, _value})
when unit in [:hour, :minute, :second, :microsecond] do
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc false
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
@@ -861,7 +777,7 @@ defmodule Date do
end
@doc """
Calculates the ordinal day of the week of a given `date`.
Calculates the day of the week of a given `date`.
Returns the day of the week as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 7, where
@@ -870,19 +786,10 @@ defmodule Date do
An optional `starting_on` value may be supplied, which
configures the weekday the week starts on. The default value
for it is `:default`, which translates to `:monday` for the
built-in ISO 8601 calendar. Any other weekday may be used for
`starting_on`, in such cases, that weekday will be considered the first
day of the week, and therefore it will be assigned the ordinal number 1.
The other calendars, the value returned is an ordinal day of week.
For example, `1` may mean "first day of the week" and `7` is
defined to mean "seventh day of the week". Custom calendars may
also accept their own variations of the `starting_on` parameter
with their own meaning.
built-in ISO calendar. Any other weekday may be given to.
## Examples
# 2016-10-31 is a Monday and by default Monday is the first day of the week
iex> Date.day_of_week(~D[2016-10-31])
1
iex> Date.day_of_week(~D[2016-11-01])
@@ -892,7 +799,6 @@ defmodule Date do
iex> Date.day_of_week(~D[-0015-10-30])
3
# 2016-10-31 is a Monday but, as we start the week on Sunday, now it returns 2
iex> Date.day_of_week(~D[2016-10-31], :sunday)
2
iex> Date.day_of_week(~D[2016-11-01], :sunday)
@@ -1092,7 +998,13 @@ defmodule Date do
def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.year_of_era(year, month, day)
# TODO: Remove me on 1.17
# The behaviour implementation already warns on missing callback.
if function_exported?(calendar, :year_of_era, 3) do
calendar.year_of_era(year, month, day)
else
calendar.year_of_era(year)
end
end
@doc """
@@ -1171,17 +1083,8 @@ defmodule Date do
end
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar != Calendar.ISO or year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, #{inspect(calendar)})"
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+3 -11
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Date.Range do
@moduledoc """
Returns an inclusive range between dates.
@@ -95,7 +91,7 @@ defmodule Date.Range do
[date_from_iso_days(current, calendar)]
end
defp slice(current, step, remaining, calendar) when remaining > 1 do
defp slice(current, step, remaining, calendar) do
[
date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar)
@@ -171,12 +167,8 @@ defmodule Date.Range do
when step < 0 and first_days < last_days,
do: 0
defp size(%Date.Range{
first_in_iso_days: first_days,
last_in_iso_days: last_days,
step: step
}),
do: abs(div(last_days - first_days, step)) + 1
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}),
do: abs(div(last_days - first_days, step)) + 1
# TODO: Remove me on v2.0
defp size(
+77 -303
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule DateTime do
@moduledoc """
A datetime implementation with a time zone.
@@ -17,8 +13,8 @@ defmodule DateTime do
Remember, comparisons in Elixir using `==/2`, `>/2`, `</2` and friends
are structural and based on the DateTime struct fields. For proper
comparison between datetimes, use the `compare/2`, `after?/2` and `before?/2` functions.
The existence of the `compare/2` function in this module also allows
comparison between datetimes, use the `compare/2` function. The
existence of the `compare/2` function in this module also allows
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
maximum datetime of an `Enum`. For example:
@@ -32,12 +28,7 @@ defmodule DateTime do
## Time zone database
Many functions in this module require a time zone database.
A time zone database is a record of the UTC offsets that its locales have
used at various times in the past, are using, and are expected to use in the
future.
Because those plans can change, it needs to be periodically updated.
By default, `DateTime` uses the default time zone database returned by
By default, it uses the default time zone database returned by
`Calendar.get_time_zone_database/0`, which defaults to
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
datetimes and returns `{:error, :utc_only_time_zone_database}`
@@ -52,7 +43,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices
To use one of them, first make sure it is added as a dependency in `mix.exs`.
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
@@ -69,7 +60,7 @@ defmodule DateTime do
a date and time at a given time zone". To understand precisely
what we mean, let's see an example.
Imagine someone in Poland who wants to schedule a meeting with someone
Imagine someone in Poland wants to schedule a meeting with someone
in Brazil in the next year. The meeting will happen at 2:30 AM
in the Polish time zone. At what time will the meeting happen in
Brazil?
@@ -92,14 +83,14 @@ defmodule DateTime do
not a problem, because time zone rules do not change for past
events.
To make matters worse, it may be that 2:30 AM in Polish time
To make matters worse, it may be that the 2:30 AM in Polish time
does not actually even exist or it is ambiguous. If a certain
time zone observes "Daylight Saving Time", they will move their
clock forward once a year. When this happens, there is a whole
hour that does not exist. Then, when they move the clock back,
there is a certain hour that will happen twice. So if you want to
schedule a meeting when this shift back happens, you would need to
explicitly say which occurrence of 2:30 AM you mean: the one in
explicitly say which occurence of 2:30 AM you mean: the one in
"Summer Time", which occurs before the shift, or the one
in "Standard Time", which occurs after it. Applications that are
date and time sensitive need to take these scenarios into account
@@ -179,9 +170,6 @@ defmodule DateTime do
truncate the resulting datetime. This is available
since v1.15.0.
The default unit if none gets passed is `:native`,
which results on a default resolution of microseconds.
## Examples
iex> datetime = DateTime.utc_now()
@@ -383,12 +371,13 @@ defmodule DateTime do
@doc """
Converts the given Unix time to `DateTime`.
The integer can be given in different unit, according to `System.convert_time_unit/3`,
and it will be converted to microseconds internally, which is the maximum precision
supported by `DateTime`. In other words, any precision higher than microseconds will
lead to truncation.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
be converted to microseconds internally. Up to
253402300799 seconds is supported.
Unix times are always in UTC. Therefore the DateTime will be returned in UTC.
Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
## Examples
@@ -884,10 +873,8 @@ defmodule DateTime do
The `datetime` is expected to be using the ISO calendar
with a year greater than or equal to 0.
It will return the integer with the given unit, according
to `System.convert_time_unit/3`. If the given unit is different
than microseconds, the returned value will be either truncated
or padded accordingly.
It will return the integer with the given unit,
according to `System.convert_time_unit/3`.
## Examples
@@ -907,7 +894,7 @@ defmodule DateTime do
-17412508655
"""
@spec to_unix(Calendar.datetime(), :native | System.time_unit()) :: integer
@spec to_unix(Calendar.datetime(), System.time_unit()) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
@@ -1035,18 +1022,15 @@ 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
format.
## Examples
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1096,21 +1080,8 @@ defmodule DateTime do
@spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t()
def to_iso8601(datetime, format \\ :extended, offset \\ nil)
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset)
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, nil)
when format in [:extended, :basic] do
datetime
|> to_iso8601_iodata(format, offset)
|> IO.iodata_to_binary()
end
def to_iso8601(%{calendar: _} = datetime, format, offset)
when format in [:extended, :basic] do
datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format, offset)
end
defp to_iso8601_iodata(datetime, format, nil) do
%{
year: year,
month: month,
@@ -1124,51 +1095,35 @@ defmodule DateTime do
std_offset: std_offset
} = datetime
[
datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format),
Calendar.ISO.offset_to_iodata(utc_offset, std_offset, time_zone, format)
]
datetime_to_string(year, month, day, hour, minute, second, microsecond, format) <>
Calendar.ISO.offset_to_string(utc_offset, std_offset, time_zone, format)
end
defp to_iso8601_iodata(
%{microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime,
format,
0
) do
def to_iso8601(
%{calendar: Calendar.ISO, microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime,
format,
0
)
when format in [:extended, :basic] do
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, 0)
[
datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
{microsecond, precision},
format
),
?Z
]
datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
"Z"
end
defp to_iso8601_iodata(datetime, format, offset) do
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset)
when format in [:extended, :basic] do
{_, precision} = datetime.microsecond
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, offset)
[
datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
{microsecond, precision},
format
),
Calendar.ISO.offset_to_iodata(offset, 0, nil, format)
]
datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
Calendar.ISO.offset_to_string(offset, 0, nil, format)
end
def to_iso8601(%{calendar: _} = datetime, format, offset) when format in [:extended, :basic] do
datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format, offset)
end
defp shift_by_offset(%{calendar: calendar} = datetime, offset) do
@@ -1181,12 +1136,10 @@ defmodule DateTime do
|> calendar.naive_datetime_from_iso_days()
end
defp datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format) do
[
Calendar.ISO.date_to_iodata(year, month, day, format),
?T,
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
]
defp datetime_to_string(year, month, day, hour, minute, second, microsecond, format) do
Calendar.ISO.date_to_string(year, month, day, format) <>
"T" <>
Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
end
@doc """
@@ -1385,12 +1338,7 @@ 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
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1546,11 +1494,6 @@ defmodule DateTime do
## Examples
iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z])
10
This function also considers timezone offsets:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
@@ -1599,56 +1542,42 @@ defmodule DateTime do
end
naive_diff =
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond))
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
offset_diff = utc_offset2 + std_offset2 - (utc_offset1 + std_offset1)
System.convert_time_unit(naive_diff, :microsecond, unit) +
System.convert_time_unit(offset_diff, :second, unit)
naive_diff + System.convert_time_unit(offset_diff, :second, unit)
end
@doc """
Adds a specified amount of time to a `DateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always considers the unit to be computed according
to the `Calendar.ISO`.
This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there
are summer time/daylight saving time changes, it will also change the
time forward or backward by one hour, so the elapsed time is precisely
24 hours. Similarly, adding just a few seconds to a datetime just before
"spring forward" can cause wall time to increase by more than an hour.
ignoring the wall time and timezone changes. For example, if you add
one day when there are summer time/daylight saving time changes,
it will also change the time forward or backward by one hour,
so the elapsed time is precisely 24 hours. Similarly, adding just
a few seconds to a datetime just before "spring forward" can cause
wall time to increase by more than an hour.
While this means this function is precise in terms of elapsed time,
its result may be confusing in certain use cases. For example, if a
its result may be misleading in certain use cases. For example, if a
user requests a meeting to happen every day at 15:00 and you use this
function to compute all future meetings by adding day after day, this
function may change the meeting time to 14:00 or 16:00 if there are
changes to the current timezone.
changes to the current timezone. Computing of recurring datetimes is
not currently supported in Elixir's standard library but it is available
by third-party libraries.
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can add to
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## Examples
### Examples
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1704,28 +1633,30 @@ defmodule DateTime do
add(datetime, amount_to_add * 60, :second, time_zone_database)
end
def add(%{calendar: calendar} = datetime, amount_to_add, unit, time_zone_database)
when is_integer(amount_to_add) do
def add(datetime, amount_to_add, unit, time_zone_database) when is_integer(amount_to_add) do
%{
microsecond: {_, precision},
time_zone: time_zone,
utc_offset: utc_offset,
std_offset: std_offset
std_offset: std_offset,
calendar: calendar,
microsecond: {_, precision}
} = datetime
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
total_offset = System.convert_time_unit(utc_offset + std_offset, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
result =
datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
# Subtract total offset in order to get UTC and add the integer for the addition
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add - total_offset, ppd)
|> shift_zone_for_iso_days_utc(calendar, precision, datetime.time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
@@ -1738,162 +1669,6 @@ defmodule DateTime do
end
end
@doc """
Shifts given `datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
Consequently, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen")
DateTime.shift(dt, hour: 1)
#=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles")
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## ISO calendar considerations
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2)
~U[2016-03-01 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4)
~U[2017-01-29 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25)
~U[2015-12-31 23:35:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4})
~U[2016-01-01 00:05:00.0005Z]
# leap years
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1)
~U[2025-02-28 00:00:00Z]
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4)
~U[2028-02-29 00:00:00Z]
# rounding down
iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1)
~U[2015-02-28 00:00:00Z]
"""
@doc since: "1.17.0"
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: std_offset,
utc_offset: utc_offset,
time_zone: time_zone
} = datetime
{year, month, day, hour, minute, second, {_, precision} = microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
result =
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
result_datetime
{:error, error} ->
raise ArgumentError,
"cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <>
"database #{inspect(time_zone_database)}), reason: #{inspect(error)}"
end
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given datetime with the microsecond field truncated to the given
precision (`:microsecond`, `:millisecond` or `:second`).
@@ -1942,7 +1717,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1971,7 +1746,7 @@ defmodule DateTime do
if Calendar.compatible_calendars?(dt_calendar, calendar) do
result_datetime =
datetime
|> to_iso_days()
|> to_iso_days
|> from_iso_days(datetime, calendar, precision)
{:ok, result_datetime}
@@ -1989,7 +1764,7 @@ defmodule DateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -2150,8 +1925,7 @@ defmodule DateTime do
)
case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
when calendar != Calendar.ISO or year in -9999..9999 ->
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
"~U[" <> formatted <> suffix(calendar) <> "]"
_ ->
-586
View File
@@ -1,586 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Duration do
@moduledoc """
Struct and functions for handling durations.
A `Duration` struct represents a collection of time scale units,
allowing for manipulation and calculation of durations.
Date and time scale units are represented as integers, allowing for
both positive and negative values.
Microseconds are represented using a tuple `{microsecond, precision}`.
This ensures compatibility with other calendar types implementing time,
such as `Time`, `DateTime`, and `NaiveDateTime`.
## Shifting
The most common use of durations in Elixir's standard library is to
"shift" the calendar types.
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
In the example above, `Date.shift/2` automatically converts the units
into a `Duration` struct, although one can also be given directly:
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
It is important to note that shifting is not an arithmetic operation.
For example, adding `date + 1 month + 1 month` does not yield the same
result as `date + 2 months`. Let's see an example:
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]
As you can see above, the results differ, which explains why operations
with durations are called "shift" rather than "add". This happens because,
once we add one month to `2016-01-31`, we get `2016-02-29`. Then adding
one extra month gives us `2016-03-29` instead of `2016-03-31`.
In particular, when applying durations to `Calendar.ISO` types:
* larger units (such as years and months) are applied before
smaller ones (such as weeks, hours, days, and so on)
* units are collapsed into months (`:year` and `:month`),
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
and microseconds (`:microsecond`) before they are applied
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
Therefore, 4 weeks _are not_ equivalent to 1 month
* in case of non-existing dates, the results are rounded down to the
nearest valid date
As the `shift/2` functions are calendar aware, they are guaranteed to return
valid date/times, considering leap years as well as DST in applicable time zones.
## Intervals
Durations in Elixir can be combined with stream operations to build intervals.
For example, to retrieve the next three Wednesdays starting from 17th April, 2024:
iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]
However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what
you to achieve. Compare the results of both examples below:
# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]
# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]
The second example consistently points to the last day of the month,
as it performs operations on the duration, rather than shifting date
after date.
## Comparing durations
In order to accurately compare durations, you need to either compare
only certain fields or use a reference time instant. This is because
some fields are relative to others. For example, you may say that
1 month is the same as 30 days, but if you add both of these durations
to `~D[2015-02-01]`, you would get different results, as that month
has only 28 days.
Therefore, if you wish to compare durations, one option is to use
`Date.shift/2` (or `DateTime.shift/2` or similar), and then compare
the dates:
iex> date = ~D[2015-02-01]
iex> Date.compare(Date.shift(date, month: 1), Date.shift(date, day: 30))
:lt
Or alternatively convert the durations to a fixed unit by using `to_timeout/1`,
which supports durations only up to weeks, raising if it has the month or year
fields set.
iex> to_timeout(hour: 24) == to_timeout(day: 1)
true
"""
@moduledoc since: "1.17.0"
@derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]}
defstruct year: 0,
month: 0,
week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
@typedoc """
The duration struct type.
"""
@type t :: %Duration{
year: integer,
month: integer,
week: integer,
day: integer,
hour: integer,
minute: integer,
second: integer,
microsecond: Calendar.microsecond()
}
@typedoc """
The unit pair type specifies a pair of a valid duration unit key and value.
"""
@type unit_pair ::
{:year, integer}
| {:month, integer}
| {:week, integer}
| {:day, integer}
| {:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, Calendar.microsecond()}
@typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
"""
@type duration :: t | [unit_pair]
@typedoc """
Options for `Duration.to_string/2`.
"""
@type to_string_opts :: [
units: [
year: String.t(),
month: String.t(),
week: String.t(),
day: String.t(),
hour: String.t(),
minute: String.t(),
second: String.t()
],
separator: String.t()
]
@microseconds_per_second 1_000_000
@doc """
Creates a new `Duration` struct from given `unit_pairs`.
Raises an `ArgumentError` when called with invalid unit pairs.
## Examples
iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
%Duration{year: 1, week: 3, hour: 4, second: 1}
iex> Duration.new!(second: 1, microsecond: {1000, 6})
%Duration{second: 1, microsecond: {1000, 6}}
iex> Duration.new!(month: 2)
%Duration{month: 2}
"""
@spec new!(duration()) :: t
def new!(%Duration{} = duration) do
duration
end
def new!(unit_pairs) do
Enum.each(unit_pairs, &validate_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_unit!({unit, _value})
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
end
defp validate_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}
"""
@spec add(t, t) :: t
def add(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year + d2.year,
month: d1.month + d2.month,
week: d1.week + d2.week,
day: d1.day + d2.day,
hour: d1.hour + d2.hour,
minute: d1.minute + d2.minute,
second: d1.second + d2.second,
microsecond: {m1 + m2, max(p1, p2)}
}
end
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}
"""
@spec subtract(t, t) :: t
def subtract(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year - d2.year,
month: d1.month - d2.month,
week: d1.week - d2.week,
day: d1.day - d2.day,
hour: d1.hour - d2.hour,
minute: d1.minute - d2.minute,
second: d1.second - d2.second,
microsecond: {m1 - m2, max(p1, p2)}
}
end
@doc """
Multiplies `duration` units by given `integer`.
## Examples
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}
"""
@spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%Duration{
year: duration.year * integer,
month: duration.month * integer,
week: duration.week * integer,
day: duration.day * integer,
hour: duration.hour * integer,
minute: duration.minute * integer,
second: duration.second * integer,
microsecond: {ms * integer, p}
}
end
@doc """
Negates `duration` units.
## Examples
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}
"""
@spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do
%Duration{
year: -duration.year,
month: -duration.month,
week: -duration.week,
day: -duration.day,
hour: -duration.hour,
minute: -duration.minute,
second: -duration.second,
microsecond: {-ms, p}
}
end
@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
def from_iso8601(string) when is_binary(string) do
case Calendar.ISO.parse_duration(string) do
{:ok, duration} ->
{:ok, new!(duration)}
error ->
error
end
end
@doc """
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(string) when is_binary(string) do
case from_iso8601(string) do
{:ok, duration} ->
duration
{:error, reason} ->
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
end
end
@doc """
Converts the given `duration` to a human readable representation.
## Options
* `:units` - the units to be used alongside each duration component.
The default units follow the ISO 80000-3 standard:
[
year: "a",
month: "mo",
week: "wk",
day: "d",
hour: "h",
minute: "min",
second: "s"
]
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
## Examples
iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"
By default, this function uses ISO 80000-3 units, which uses "a" for years.
But you can customize all units via the units option:
iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"
You may also choose the separator:
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"
A duration without components is rendered as "0s":
iex> Duration.to_string(Duration.new!([]))
"0s"
Microseconds are rendered as part of seconds with the appropriate precision:
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"
"""
@doc since: "1.18.0"
@spec to_string(t, to_string_opts) :: String.t()
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),
?.,
Calendar.ISO.microseconds_to_iodata(ms, p)
]
end
@compile {:inline, pair: 2}
defp pair(0, _key), do: []
defp pair(num, key), do: [Integer.to_string(num), key]
end
+66 -590
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar.ISO do
@moduledoc """
The default calendar implementation, a Gregorian calendar following ISO 8601.
@@ -22,8 +18,7 @@ defmodule Calendar.ISO do
The standard library supports a minimal set of possible ISO 8601 features.
Specifically, the parser only supports calendar dates and does not support
ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
ordinal and week formats.
By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times.
@@ -34,7 +29,7 @@ defmodule Calendar.ISO do
Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`).
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
#### Examples
@@ -235,9 +230,9 @@ defmodule Calendar.ISO do
]
end
defguardp is_year(year) when is_integer(year)
defguardp is_year_BCE(year) when year <= 0
defguardp is_year_CE(year) when year >= 1
defguardp is_year(year) when year in -9999..9999
defguardp is_year_BCE(year) when year in -9999..0
defguardp is_year_CE(year) when year in 1..9999
defguardp is_month(month) when month in 1..12
defguardp is_day(day) when day in 1..31
defguardp is_hour(hour) when hour in 0..23
@@ -668,87 +663,6 @@ defmodule Calendar.ISO do
end
end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms =
case string do
"-" <> _ ->
-ms
_ ->
ms
end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
end
_ ->
{:error, :invalid_time_component}
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
@doc """
Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -895,7 +809,7 @@ defmodule Calendar.ISO do
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
def date_from_iso_days(days) when days in -3_652_059..3_652_424 do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
@@ -1224,37 +1138,6 @@ defmodule Calendar.ISO do
:basic | :extended
) :: String.t()
def time_to_string(
hour,
minute,
second,
microsecond,
format \\ :extended
) do
time_to_iodata(hour, minute, second, microsecond, format)
|> IO.iodata_to_binary()
end
@doc """
Converts the given time into a iodata.
See `time_to_string/5` for more information.
## Examples
iex> data = Calendar.ISO.time_to_iodata(2, 2, 2, {2, 6})
iex> IO.iodata_to_binary(data)
"02:02:02.000002"
"""
@doc since: "1.19.0"
@spec time_to_iodata(
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond(),
:basic | :extended
) :: iodata
def time_to_iodata(
hour,
minute,
second,
@@ -1263,43 +1146,24 @@ defmodule Calendar.ISO do
)
when is_hour(hour) and is_minute(minute) and is_second(second) and
is_microsecond(ms_value, ms_precision) and format in [:basic, :extended] do
time_to_iodata_guarded(hour, minute, second, microsecond, format)
time_to_string_guarded(hour, minute, second, microsecond, format)
end
defp time_to_iodata_guarded(hour, minute, second, {_, 0}, format) do
time_to_iodata_format(hour, minute, second, format)
defp time_to_string_guarded(hour, minute, second, {_, 0}, format) do
time_to_string_format(hour, minute, second, format)
end
defp time_to_iodata_guarded(hour, minute, second, {microsecond, precision}, format) do
[
time_to_iodata_format(hour, minute, second, format),
?.
| microseconds_to_iodata(microsecond, precision)
]
defp time_to_string_guarded(hour, minute, second, {microsecond, precision}, format) do
time_to_string_format(hour, minute, second, format) <>
"." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
end
@doc false
def microseconds_to_iodata(_microsecond, 0), do: []
def microseconds_to_iodata(microsecond, 6), do: zero_pad(microsecond, 6)
def microseconds_to_iodata(microsecond, precision) do
num = div(microsecond, scale_factor(precision))
zero_pad(num, precision)
defp time_to_string_format(hour, minute, second, :extended) do
zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
end
defp scale_factor(1), do: 100_000
defp scale_factor(2), do: 10_000
defp scale_factor(3), do: 1_000
defp scale_factor(4), do: 100
defp scale_factor(5), do: 10
defp scale_factor(6), do: 1
defp time_to_iodata_format(hour, minute, second, :extended) do
[zero_pad(hour, 2), ?:, zero_pad(minute, 2), ?: | zero_pad(second, 2)]
end
defp time_to_iodata_format(hour, minute, second, :basic) do
[zero_pad(hour, 2), zero_pad(minute, 2) | zero_pad(second, 2)]
defp time_to_string_format(hour, minute, second, :basic) do
zero_pad(hour, 2) <> zero_pad(minute, 2) <> zero_pad(second, 2)
end
@doc """
@@ -1327,36 +1191,18 @@ defmodule Calendar.ISO do
@doc since: "1.4.0"
@spec date_to_string(year, month, day, :basic | :extended) :: String.t()
@impl true
def date_to_string(year, month, day, format \\ :extended) do
date_to_iodata(year, month, day, format)
|> IO.iodata_to_binary()
end
@doc """
Converts the given date into a iodata.
See `date_to_string/4` for more information.
## Examples
iex> data = Calendar.ISO.date_to_iodata(2015, 2, 28)
iex> IO.iodata_to_binary(data)
"2015-02-28"
"""
@doc since: "1.19.0"
@spec date_to_iodata(year, month, day, :basic | :extended) :: iodata
def date_to_iodata(year, month, day, format \\ :extended)
def date_to_string(year, month, day, format \\ :extended)
when is_integer(year) and is_integer(month) and is_integer(day) and
format in [:basic, :extended] do
date_to_iodata_guarded(year, month, day, format)
date_to_string_guarded(year, month, day, format)
end
defp date_to_iodata_guarded(year, month, day, :extended) do
[zero_pad(year, 4), ?-, zero_pad(month, 2), ?- | zero_pad(day, 2)]
defp date_to_string_guarded(year, month, day, :extended) do
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
end
defp date_to_iodata_guarded(year, month, day, :basic) do
[zero_pad(year, 4), zero_pad(month, 2) | zero_pad(day, 2)]
defp date_to_string_guarded(year, month, day, :basic) do
zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
end
@doc """
@@ -1399,61 +1245,8 @@ defmodule Calendar.ISO do
microsecond,
format \\ :extended
) do
naive_datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
format
)
|> IO.iodata_to_binary()
end
@doc """
Converts the given naive_datetime into a iodata.
See `naive_datetime_to_iodata/8` for more information.
## Examples
iex> data = Calendar.ISO.naive_datetime_to_iodata(2015, 2, 28, 1, 2, 3, {4, 6}, :basic)
iex> IO.iodata_to_binary(data)
"20150228 010203.000004"
iex> data = Calendar.ISO.naive_datetime_to_iodata(2015, 2, 28, 1, 2, 3, {4, 6}, :extended)
iex> IO.iodata_to_binary(data)
"2015-02-28 01:02:03.000004"
"""
@doc since: "1.19.0"
@spec naive_datetime_to_iodata(
year,
month,
day,
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond(),
:basic | :extended
) :: iodata
def naive_datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
format \\ :extended
) do
[
date_to_iodata(year, month, day, format),
?\s
| time_to_iodata(hour, minute, second, microsecond, format)
]
date_to_string(year, month, day, format) <>
" " <> time_to_string(hour, minute, second, microsecond, format)
end
@doc """
@@ -1519,89 +1312,20 @@ defmodule Calendar.ISO do
utc_offset,
std_offset,
format \\ :extended
) do
datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
time_zone,
zone_abbr,
utc_offset,
std_offset,
format
)
|> IO.iodata_to_binary()
end
@doc """
Converts the given datetime into a iodata.
See `datetime_to_iodata/12` for more information.
## Examples
iex> time_zone = "Etc/UTC"
iex> data = Calendar.ISO.datetime_to_iodata(2017, 8, 1, 1, 2, 3, {4, 5}, time_zone, "UTC", 0, 0)
iex> IO.iodata_to_binary(data)
"2017-08-01 01:02:03.00000Z"
"""
@doc since: "1.19.0"
@spec datetime_to_iodata(
year,
month,
day,
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond(),
Calendar.time_zone(),
Calendar.zone_abbr(),
Calendar.utc_offset(),
Calendar.std_offset(),
:basic | :extended
) :: iodata
def datetime_to_iodata(
year,
month,
day,
hour,
minute,
second,
microsecond,
time_zone,
zone_abbr,
utc_offset,
std_offset,
format \\ :extended
)
when is_time_zone(time_zone) and is_zone_abbr(zone_abbr) and is_utc_offset(utc_offset) and
is_std_offset(std_offset) do
[
date_to_iodata(year, month, day, format),
?\s,
time_to_iodata(hour, minute, second, microsecond, format),
offset_to_iodata(utc_offset, std_offset, time_zone, format),
zone_to_iodata(utc_offset, std_offset, zone_abbr, time_zone)
]
date_to_string(year, month, day, format) <>
" " <>
time_to_string(hour, minute, second, microsecond, format) <>
offset_to_string(utc_offset, std_offset, time_zone, format) <>
zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
end
@doc false
def offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"
def offset_to_string(utc, std, zone, format) do
offset_to_iodata(utc, std, zone, format)
|> IO.iodata_to_binary()
end
@doc false
def offset_to_iodata(0, 0, "Etc/UTC", _format), do: ?Z
def offset_to_iodata(utc, std, _zone, format) do
def offset_to_string(utc, std, _zone, format) do
total = utc + std
second = abs(total)
minute = second |> rem(3600) |> div(60)
@@ -1610,15 +1334,15 @@ defmodule Calendar.ISO do
end
defp format_offset(total, hour, minute, :extended) do
[sign(total), zero_pad(hour, 2), ?: | zero_pad(minute, 2)]
sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
end
defp format_offset(total, hour, minute, :basic) do
[sign(total), zero_pad(hour, 2) | zero_pad(minute, 2)]
sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
end
defp zone_to_iodata(_, _, _, "Etc/UTC"), do: []
defp zone_to_iodata(_, _, abbr, zone), do: [?\s, abbr, ?\s | zone]
defp zone_to_string(_, _, _, "Etc/UTC"), do: ""
defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone
@doc """
Determines if the date given is valid according to the proleptic Gregorian calendar.
@@ -1640,7 +1364,7 @@ defmodule Calendar.ISO do
@spec valid_date?(year, month, day) :: boolean
def valid_date?(year, month, day)
when is_integer(year) and is_integer(month) and is_integer(day) do
is_month(month) and day in 1..days_in_month(year, month)
is_year(year) and is_month(month) and day in 1..days_in_month(year, month)
end
@doc """
@@ -1679,24 +1403,16 @@ defmodule Calendar.ISO do
{0, 1}
end
defp sign(total) when total < 0, do: ?-
defp sign(_), do: ?+
defp sign(total) when total < 0, do: "-"
defp sign(_), do: "+"
defp zero_pad(val, count) when val >= 0 and count <= 6 do
defp zero_pad(val, count) when val >= 0 do
num = Integer.to_string(val)
case max(count - byte_size(num), 0) do
0 -> num
1 -> ["0" | num]
2 -> ["00" | num]
3 -> ["000" | num]
4 -> ["0000" | num]
5 -> ["00000" | num]
end
:binary.copy("0", max(count - byte_size(num), 0)) <> num
end
defp zero_pad(val, count) do
[?- | zero_pad(-val, count)]
"-" <> zero_pad(-val, count)
end
@doc """
@@ -1739,230 +1455,6 @@ defmodule Calendar.ISO do
{days, {@parts_per_day - 1, @parts_per_day}}
end
@doc """
Shifts Date by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2))
{2016, 3, 3}
iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1))
{2016, 3, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1))
{2016, 2, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1))
{2020, 2, 1}
"""
@impl true
@spec shift_date(year, month, day, Duration.t()) :: {year, month, day}
def shift_date(year, month, day, duration) do
shift_options = shift_date_options(duration)
Enum.reduce(shift_options, {year, month, day}, fn
{_, 0}, date ->
date
{:month, value}, date ->
shift_months(date, value)
{:day, value}, date ->
shift_days(date, value)
end)
end
@doc """
Shifts NaiveDateTime by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1))
{2016, 1, 3, 1, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30))
{2016, 1, 4, 6, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{2016, 1, 3, 0, 0, 0, {100, 6}}
"""
@impl true
@spec shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do
shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{_, 0}, naive_datetime ->
naive_datetime
{:month, value}, {year, month, day, hour, minute, second, microsecond} ->
{new_year, new_month, new_day} = shift_months({year, month, day}, value)
{new_year, new_month, new_day, hour, minute, second, microsecond}
{time_unit, value}, naive_datetime ->
shift_time_unit(naive_datetime, value, time_unit)
end)
end
@doc """
Shifts Time by Duration units according to its calendar.
## Examples
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2))
{15, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{13, 0, 0, {100, 6}}
"""
@impl true
@spec shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
def shift_time(hour, minute, second, microsecond, duration) do
shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{_, 0}, time ->
time
{time_unit, value}, time ->
shift_time_unit(time, value, time_unit)
end)
end
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
{year, month, day}
end
defp shift_months({year, month, day}, months) do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
new_month when new_month < 1 -> new_month + months_in_year
new_month -> new_month
end
new_day = min(day, days_in_month(new_year, new_month))
{new_year, new_month, new_day}
end
@doc false
def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{year, month, day, hour, minute, second, {ms_value, _}} =
naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> shift_time_unit(value, unit)
|> naive_datetime_from_iso_days()
{year, month, day, hour, minute, second, {ms_value, precision}}
end
def shift_time_unit({hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{_days, day_fraction} =
shift_time_unit({0, time_to_day_fraction(hour, minute, second, microsecond)}, value, unit)
{hour, minute, second, {microsecond, _}} = time_from_day_fraction(day_fraction)
{hour, minute, second, {microsecond, precision}}
end
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
defp shift_time_unit_values({0, _}, {_, original_precision}) do
{0, original_precision}
end
defp shift_time_unit_values({ms_value, ms_precision}, {_, _}) do
{ms_value, ms_precision}
end
defp shift_time_unit_values(value, {_, original_precision}) do
{value, original_precision}
end
defp shift_date_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _precision}
}) do
[
month: year * 12 + month,
day: week * 7 + day
]
end
defp shift_date_options(_duration) do
raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day"
end
defp shift_datetime_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(%Duration{
year: 0,
month: 0,
week: 0,
day: 0,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
second: hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(_duration) do
raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond"
end
## Helpers
@doc false
@@ -1993,13 +1485,16 @@ defmodule Calendar.ISO do
end
defp parse_microsecond("." <> rest) do
case parse_microsecond(rest, 0, []) do
{[], 0, _} ->
case parse_microsecond(rest, 0, "") do
{"", 0, _} ->
:error
{microsecond, precision, rest} ->
scale = scale_factor(precision)
{{:erlang.list_to_integer(microsecond) * scale, precision}, rest}
{microsecond, precision, rest} when precision in 1..6 ->
pad = String.duplicate("0", 6 - byte_size(microsecond))
{{String.to_integer(microsecond <> pad), precision}, rest}
{microsecond, _precision, rest} ->
{{String.to_integer(binary_part(microsecond, 0, 6)), 6}, rest}
end
end
@@ -2011,42 +1506,34 @@ defmodule Calendar.ISO do
{{0, 0}, rest}
end
defp parse_microsecond(<<head, tail::binary>>, 6, acc) when head in ?0..?9,
do: parse_microsecond(tail, 6, acc)
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
do: parse_microsecond(tail, precision + 1, [head | acc])
do: parse_microsecond(tail, precision + 1, <<acc::binary, head>>)
defp parse_microsecond(rest, precision, acc) do
{:lists.reverse(acc), precision, rest}
end
defp parse_microsecond(rest, precision, acc), do: {acc, precision, rest}
defp parse_offset(""), do: {nil, ""}
defp parse_offset("Z"), do: {0, ""}
defp parse_offset("-00:00"), do: :error
defp parse_offset(<<?+, h1, h2, ?:, m1, m2, rest::binary>>),
do: parse_offset(1, h1, h2, m1, m2, rest)
defp parse_offset(<<?+, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
do: parse_offset(1, hour, min, rest)
defp parse_offset(<<?-, h1, h2, ?:, m1, m2, rest::binary>>),
do: parse_offset(-1, h1, h2, m1, m2, rest)
defp parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
do: parse_offset(-1, hour, min, rest)
defp parse_offset(<<?+, h1, h2, m1, m2, rest::binary>>),
do: parse_offset(1, h1, h2, m1, m2, rest)
defp parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
do: parse_offset(1, hour, min, rest)
defp parse_offset(<<?-, h1, h2, m1, m2, rest::binary>>),
do: parse_offset(-1, h1, h2, m1, m2, rest)
defp parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
do: parse_offset(-1, hour, min, rest)
defp parse_offset(<<?+, h1, h2, rest::binary>>), do: parse_offset(1, h1, h2, ?0, ?0, rest)
defp parse_offset(<<?-, h1, h2, rest::binary>>), do: parse_offset(-1, h1, h2, ?0, ?0, rest)
defp parse_offset(<<?+, hour::2-bytes, rest::binary>>), do: parse_offset(1, hour, "00", rest)
defp parse_offset(<<?-, hour::2-bytes, rest::binary>>), do: parse_offset(-1, hour, "00", rest)
defp parse_offset(_), do: :error
defp parse_offset(sign, h1, h2, m1, m2, rest) do
with true <- h1 in ?0..?2 and h2 in ?0..?9,
true <- m1 in ?0..?5 and m2 in ?0..?9,
hour = (h1 - ?0) * 10 + h2 - ?0,
min = (m1 - ?0) * 10 + m2 - ?0,
true <- hour < 24 do
defp parse_offset(sign, hour, min, rest) do
with {hour, ""} when hour < 24 <- Integer.parse(hour),
{min, ""} when min < 60 <- Integer.parse(min) do
{(hour * 60 + min) * 60 * sign, rest}
else
_ -> :error
@@ -2155,22 +1642,11 @@ defmodule Calendar.ISO do
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
defp days_in_previous_years(year) do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
+57 -199
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule NaiveDateTime do
@moduledoc """
A NaiveDateTime struct (without a time zone) and functions.
@@ -40,10 +36,10 @@ defmodule NaiveDateTime do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `NaiveDateTime` struct fields. For proper comparison
between naive datetimes, use the `compare/2`, `after?/2` and `before?/2` functions.
The existence of the `compare/2` function in this module also allows
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
maximum naive datetime of an `Enum`. For example:
between naive datetimes, use the `compare/2` function. The existence of the
`compare/2` function in this module also allows using `Enum.min/2` and
`Enum.max/2` functions to get the minimum and maximum naive datetime of an
`Enum`. For example:
iex> Enum.min([~N[2020-01-01 23:00:07], ~N[2000-01-01 23:00:07]], NaiveDateTime)
~N[2000-01-01 23:00:07]
@@ -114,12 +110,30 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO)
end
def utc_now(calendar) do
utc_now(:native, calendar)
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
end
@doc """
@@ -144,20 +158,7 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
end
@doc """
@@ -251,7 +252,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -316,7 +317,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
@@ -391,20 +392,14 @@ defmodule NaiveDateTime do
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
## Examples
It uses seconds by default:
@@ -471,21 +466,23 @@ defmodule NaiveDateTime do
end
def add(
%{calendar: calendar, microsecond: {_, precision}} = naive_datetime,
%{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
amount_to_add,
unit
)
when is_integer(amount_to_add) do
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
if not is_integer(unit) and
unit not in ~w(second millisecond microsecond nanosecond)a do
raise ArgumentError,
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
ppd = System.convert_time_unit(86400, :second, unit)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add, ppd)
|> from_iso_days(calendar, precision)
end
@@ -574,83 +571,6 @@ defmodule NaiveDateTime do
units1 - units2
end
@doc """
Shifts given `naive_datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1)
~N[2016-02-29 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1)
~N[2020-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1)
~N[2014-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45)
~N[2016-01-31 00:00:45]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6})
~N[2016-01-31 00:00:00.000100]
# leap years
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1)
~N[2025-02-28 00:00:00]
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4)
~N[2028-02-29 00:00:00]
# rounding down
iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1)
~N[2015-02-28 00:00:00]
"""
@doc since: "1.17.0"
@spec shift(Calendar.naive_datetime(), Duration.duration()) :: t
def shift(%{calendar: calendar} = naive_datetime, duration) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = naive_datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given naive datetime with the microsecond field truncated to the
given precision (`:microsecond`, `:millisecond` or `:second`).
@@ -762,10 +682,7 @@ defmodule NaiveDateTime do
@doc """
Converts the given naive datetime to a string according to its calendar.
For readability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
## Examples
### Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13"
@@ -912,7 +829,7 @@ defmodule NaiveDateTime do
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
## Examples
### Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13"
@@ -938,19 +855,6 @@ defmodule NaiveDateTime do
def to_iso8601(%{calendar: Calendar.ISO} = naive_datetime, format)
when format in [:basic, :extended] do
naive_datetime
|> to_iso8601_iodata(format)
|> IO.iodata_to_binary()
end
def to_iso8601(%{calendar: _} = naive_datetime, format)
when format in [:basic, :extended] do
naive_datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
defp to_iso8601_iodata(naive_datetime, format) do
%{
year: year,
month: month,
@@ -961,11 +865,14 @@ defmodule NaiveDateTime do
microsecond: microsecond
} = naive_datetime
[
Calendar.ISO.date_to_iodata(year, month, day, format),
?T,
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
]
Calendar.ISO.date_to_string(year, month, day, format) <>
"T" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
end
def to_iso8601(%{calendar: _} = naive_datetime, format) when format in [:basic, :extended] do
naive_datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
@doc """
@@ -1009,8 +916,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
{:ok, ~N[2000-01-01 13:30:15.005000]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
@@ -1019,11 +924,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
"""
@spec from_erl(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1042,19 +943,13 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
~N[2000-01-01 13:30:15.005000]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
@@ -1081,28 +976,11 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(seconds, microsecond_precision \\ {0, 0}, calendar \\ Calendar.ISO)
def from_gregorian_seconds(seconds, {microsecond, precision}, Calendar.ISO)
when is_integer(seconds) do
{days, seconds} = div_rem(seconds, 24 * 60 * 60)
{hours, seconds} = div_rem(seconds, 60 * 60)
{minutes, seconds} = div_rem(seconds, 60)
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
%NaiveDateTime{
calendar: Calendar.ISO,
year: year,
month: month,
day: day,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microsecond, precision}
}
end
def from_gregorian_seconds(seconds, {microsecond, precision}, calendar)
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
@@ -1121,17 +999,6 @@ defmodule NaiveDateTime do
}
end
defp div_rem(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
if rem >= 0 do
{div, rem}
else
{div - 1, rem + int2}
end
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
@@ -1265,7 +1132,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1278,10 +1145,6 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert(
%{
calendar: calendar,
@@ -1313,7 +1176,7 @@ defmodule NaiveDateTime do
if Calendar.compatible_calendars?(ndt_calendar, calendar) do
result_naive_datetime =
naive_datetime
|> to_iso_days()
|> to_iso_days
|> from_iso_days(calendar, precision)
{:ok, result_naive_datetime}
@@ -1331,7 +1194,7 @@ defmodule NaiveDateTime do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1483,15 +1346,10 @@ defmodule NaiveDateTime do
calendar: calendar
} = naive_datetime
if calendar != Calendar.ISO or year in -9999..9999 do
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]"
else
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
end
"~N[" <> formatted <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+41 -172
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Time do
@moduledoc """
A Time struct and functions.
@@ -35,10 +31,9 @@ defmodule Time do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `Time` struct fields. For proper comparison between
times, use the `compare/2`, `after?/2` and `before?/2` functions.
The existence of the `compare/2` function in this module also allows
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
maximum time of an `Enum`. For example:
times, use the `compare/2` function. The existence of the `compare/2`
function in this module also allows using `Enum.min/2` and `Enum.max/2`
functions to get the minimum and maximum time of an `Enum`. For example:
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
~T[10:00:07.001]
@@ -55,57 +50,23 @@ defmodule Time do
calendar: Calendar.calendar()
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
Returns the current time in UTC.
You can pass a time unit to automatically truncate the resulting time.
The default unit if none gets passed is `:native` which results on a default resolution of microseconds.
## Examples
iex> time = Time.utc_now()
iex> time.hour >= 0
true
iex> time = Time.utc_now(:second)
iex> time.microsecond
{0, 0}
"""
@doc since: "1.4.0"
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO) do
case calendar_or_time_unit do
unit when unit in [:native, :microsecond, :millisecond, :second] ->
utc_now(unit, Calendar.ISO)
calendar ->
utc_now(:native, calendar)
end
end
@doc """
Returns the current time in UTC, supporting a precision and a specific calendar.
## Examples
iex> time = Time.utc_now(:microsecond, Calendar.ISO)
iex> time.hour >= 0
true
iex> time = Time.utc_now(:second, Calendar.ISO)
iex> time.microsecond
{0, 0}
"""
@doc since: "1.19.0"
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
@spec utc_now(Calendar.calendar()) :: t
def utc_now(calendar \\ Calendar.ISO) do
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(:os.system_time(), :native)
{hour, minute, second} = time
iso_time = %Time{
@@ -155,7 +116,7 @@ defmodule Time do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -225,7 +186,7 @@ defmodule Time do
@doc """
Converts the given `time` to a string.
## Examples
### Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
@@ -334,7 +295,7 @@ defmodule Time do
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
## Examples
### Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
@@ -395,21 +356,13 @@ defmodule Time do
## Examples
iex> Time.from_erl({23, 30, 15})
{:ok, ~T[23:30:15]}
iex> Time.from_erl({23, 30, 15}, 5000)
{:ok, ~T[23:30:15.005000]}
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -425,8 +378,6 @@ defmodule Time do
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, 5000)
~T[23:30:15.005000]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
@@ -505,18 +456,14 @@ defmodule Time do
@doc """
Adds the `amount_to_add` of `unit`s to the given `time`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` always considers the unit to be computed according to
> the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar.
@@ -568,26 +515,13 @@ defmodule Time do
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
when is_integer(amount_to_add) do
valid? =
if is_integer(unit),
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
if not valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond)
total = time_to_microseconds(time) + amount_to_add
parts = Integer.mod(total, @parts_per_day)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
{hour, minute, second, {microsecond, _precision}} =
Calendar.ISO.shift_time_unit(
{hour, minute, second, microsecond},
amount_to_add,
unit
)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
%Time{
hour: hour,
@@ -598,91 +532,19 @@ defmodule Time do
}
end
@doc """
Shifts given `time` by `duration` according to its calendar.
Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed to seconds and
microseconds before they are applied.
Raises an `ArgumentError` when called with date scale units.
## Examples
iex> Time.shift(~T[01:00:15], hour: 12)
~T[13:00:15]
iex> Time.shift(~T[01:35:00], hour: 6, minute: -15)
~T[07:20:00]
iex> Time.shift(~T[01:15:00], second: 125)
~T[01:17:05]
iex> Time.shift(~T[01:00:15], microsecond: {100, 6})
~T[01:00:15.000100]
iex> Time.shift(~T[01:15:00], Duration.new!(second: 65))
~T[01:16:05]
"""
@doc since: "1.17.0"
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
when unit_pair:
{:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
def shift(%{calendar: calendar} = time, duration) do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{hour, minute, second, microsecond} =
calendar.shift_time(hour, minute, second, microsecond, __duration__!(duration))
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
defp time_to_microseconds(%{
calendar: Calendar.ISO,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _}
}) do
0
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_duration_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
raise ArgumentError,
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({unit, _value})
when unit not in [:hour, :minute, :second, :microsecond] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
defp time_to_microseconds(time) do
iso_days = {0, to_day_fraction(time)}
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
end
@doc """
@@ -783,7 +645,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -839,7 +701,7 @@ defmodule Time do
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
@@ -849,8 +711,15 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
{:ok, value} = convert(time, calendar)
value
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end
@doc """
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar.TimeZoneDatabase do
@moduledoc """
This module defines a behaviour for providing time zone data.
+119 -311
View File
@@ -1,20 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code do
@moduledoc ~S"""
Utilities for managing code compilation, code evaluation, and code loading.
This module complements Erlang's [`:code` module](`:code`)
to add behavior which is specific to Elixir. For functions to
to add behaviour which is specific to Elixir. For functions to
manipulate Elixir's AST (rather than evaluating it), see the
`Macro` module.
## Working with files
This module contains three functions for compiling and evaluating files.
Here is a summary of them and their behavior:
Here is a summary of them and their behaviour:
* `require_file/2` - compiles a file and tracks its name. It does not
compile the file again if it has been previously required.
@@ -127,11 +123,6 @@ defmodule Code do
of the imported function/macro. A :remote_function/:remote_macro event
may still be emitted for the imported module/name/arity.
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
imported function or macro is processed inside a `quote/2`. `meta` is the
call AST metadata, `module` is the module the import is from, followed by
the `name` and a list of `arities` of the imported function/macro.
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
@@ -168,7 +159,7 @@ defmodule Code do
`{:ok, value}` or `:error`.
* `:defmodule` - (since v1.16.2) traced as soon as the definition of a module
starts. This is invoked early on in the module life cycle, `Module.open?/1`
starts. This is invoked early on in the module life-cycle, `Module.open?/1`
still returns `false` for such traces
* `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module
@@ -189,7 +180,7 @@ defmodule Code do
defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}")
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok
end
@@ -248,71 +239,19 @@ defmodule Code do
"""
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for code formatting functions.
"""
@type format_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:line_length, pos_integer()}
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
| {:migrate_unless, boolean()}
| {atom(), term()}
@typedoc """
Options for `quoted_to_algebra/2`.
"""
@type quoted_to_algebra_opt ::
{:line, pos_integer() | nil}
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
"""
@type parser_opts :: [
file: binary(),
line: pos_integer(),
column: pos_integer(),
indentation: non_neg_integer(),
columns: boolean(),
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
static_atoms_encoder: (atom() -> term()),
emit_warnings: boolean()
]
@typedoc """
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
"""
@type env_eval_opts :: [
file: binary(),
line: pos_integer(),
module: module(),
prune_binding: boolean()
]
@boolean_compiler_options [
:docs,
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:relative_paths
:relative_paths,
:warnings_as_errors
]
@list_compiler_options [:tracers, :parser_options]
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++
[:on_undefined_variable, :infer_signatures, :no_warn_undefined]
@list_compiler_options ++ [:on_undefined_variable]
@doc """
Lists all required files.
@@ -399,7 +338,7 @@ defmodule Code do
* `:cache` - (since v1.15.0) when true, the code path is cached
the first time it is traversed in order to reduce file system
operations.
operations. It requires Erlang/OTP 26, otherwise it is a no-op.
"""
@spec append_path(Path.t(), cache: boolean()) :: true | false
@@ -430,7 +369,7 @@ defmodule Code do
* `:cache` - (since v1.15.0) when true, the code path is cached
the first time it is traversed in order to reduce file system
operations.
operations. It requires Erlang/OTP 26, otherwise it is a no-op.
"""
@spec prepend_path(Path.t(), cache: boolean()) :: boolean()
@@ -459,7 +398,7 @@ defmodule Code do
* `:cache` - when true, the code path is cached the first time
it is traversed in order to reduce file system operations.
It requires Erlang/OTP 26, otherwise it is a no-op.
"""
@doc since: "1.15.0"
@spec prepend_paths([Path.t()], cache: boolean()) :: :ok
@@ -488,7 +427,7 @@ defmodule Code do
* `:cache` - when true, the code path is cached the first time
it is traversed in order to reduce file system operations.
It requires Erlang/OTP 26, otherwise it is a no-op.
"""
@doc since: "1.15.0"
@spec append_paths([Path.t()], cache: boolean()) :: :ok
@@ -568,9 +507,14 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
Options can be:
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
Additionally, you may also pass an environment as second argument,
so the evaluation happens within that environment.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -604,15 +548,14 @@ defmodule Code do
all imports, requires and aliases defined in the current environment
will be automatically carried over:
iex> require Integer
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
iex> result
3
iex> Enum.sort(binding)
[a: 1, b: 2]
"""
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
def eval_string(string, binding \\ [], opts \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do
@@ -667,8 +610,7 @@ defmodule Code do
"""
@doc since: "1.15.0"
@spec with_diagnostics([log: boolean()], (-> result)) ::
{result, [diagnostic(:warning | :error)]}
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
when result: term()
def with_diagnostics(opts \\ [], fun) do
value = :erlang.get(:elixir_code_diagnostics)
@@ -701,7 +643,7 @@ defmodule Code do
Defaults to `true`.
"""
@doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -717,17 +659,15 @@ defmodule Code do
## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error
reporting
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to `98`. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
the document. Defaults to 98. Note this value is used as
guideline but there are situations where it is not enforced.
See the "Line length" section below for more information
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
@@ -742,42 +682,28 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords.
Migration options (change the AST), see the "Migration formatting" section below:
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
becomes `<<foo::binary>>`, or adds parentheses for custom
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
* `:migrate_call_parens_on_pipe` (since v1.19.0) - when `true`,
formats calls on the right-hand side of the pipe operator to always include
parentheses, for example `foo |> bar` becomes `foo |> bar()` and
`foo |> mod.fun` becomes `foo |> mod.fun()`.
Parentheses are always added for qualified calls like `foo |> Bar.bar` even
when this option is `false`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_unless` (since v1.18.0) - when `true`,
rewrites `unless` expressions using `if` with a negated condition, for example
`unless foo, do:` becomes `if !foo, do:`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
## Design principles
The formatter was designed under three principles.
First, the formatter never changes the semantics of the code by default.
First, the formatter never changes the semantics of the code.
This means the input AST and the output AST are almost always equivalent.
The only cases where the formatter will change the AST is when the input AST
would cause *compiler warnings* and the output AST won't. The cases where
the formatter changes the AST can be disabled through formatting options
if desired.
The second principle is to provide as little configuration as possible.
This eases the formatter adoption by removing contention points while
@@ -788,8 +714,8 @@ defmodule Code do
specially because a function is named `defmodule`, `def`, or the like. This
principle mirrors Elixir's goal of being an extensible language where
developers can extend the language with new constructs as if they were
part of the language. When it is absolutely necessary to change behavior
based on the name, this behavior should be configurable, such as the
part of the language. When it is absolutely necessary to change behaviour
based on the name, this behaviour should be configurable, such as the
`:locals_without_parens` option.
## Running the formatter
@@ -872,10 +798,9 @@ defmodule Code do
## Line length
Another point about the formatter is that the `:line_length` configuration
indicates when an expression should be broken over multiple lines but it is
not guaranteed to do so. In many cases, it is not possible for the formatter
to break your code apart, which means it will go over the line length.
For example, if you have a long string:
is a guideline. In many cases, it is not possible for the formatter to break
your code apart, which means it will go over the line length. For example,
if you have a long string:
"this is a very long string that will go over the line length"
@@ -888,15 +813,15 @@ defmodule Code do
The string concatenation makes the code fit on a single line and also
gives more options to the formatter.
This may also appear in keywords such as do/end blocks and operators,
where the `do` keyword may go over the line length because there is no
opportunity for the formatter to introduce a line break in a readable way.
For example, if you do:
This may also appear in do/end blocks, where the `do` keyword (or `->`)
may go over the line length because there is no opportunity for the
formatter to introduce a line break in a readable way. For example,
if you do:
case very_long_expression() do
end
And only the `do` keyword is beyond the line length, Elixir **will not**
And only the `do` keyword is above the line length, Elixir **will not**
emit this:
case very_long_expression()
@@ -934,7 +859,7 @@ defmodule Code do
* Newlines before certain operators (such as the pipeline operators)
and before other operators (such as comparison operators)
The behaviors above are not guaranteed. We may remove or add new
The behaviours above are not guaranteed. We may remove or add new
rules in the future. The goal of documenting them is to provide better
understanding on what to expect from the formatter.
@@ -1011,8 +936,9 @@ defmodule Code do
## Code comments
The formatter handles code comments and guarantees a space is always added
between the beginning of the comment (#) and the next character.
The formatter also handles code comments in a way to guarantee a space
is always added between the beginning of the comment (#) and the next
character.
The formatter also extracts all trailing comments to their previous line.
For example, the code below
@@ -1024,25 +950,9 @@ defmodule Code do
# world
hello
While the formatter attempts to preserve comments in most situations,
that's not always possible, because code comments are handled apart from
the code representation (AST). While the formatter can preserve code
comments between expressions and function arguments, the formatter
cannot currently preserve them around operators. For example, the following
code:
foo() ||
# also check for bar
bar()
will move the code comments to before the operator usage:
# also check for bar
foo() ||
bar()
In some situations, code comments can be seen as ambiguous by the formatter.
For example, the comment in the anonymous function below
Because code comments are handled apart from the code representation (AST),
there are some situations where code comments are seen as ambiguous by the
code formatter. For example, the comment in the anonymous function below
fn
arg1 ->
@@ -1071,26 +981,11 @@ 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, [format_opt]) :: iodata
@spec format_string!(binary, keyword) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
line_length = Keyword.get(opts, :line_length, 98)
to_quoted_opts =
[
@@ -1113,7 +1008,7 @@ defmodule Code do
available options.
"""
@doc since: "1.6.0"
@spec format_file!(binary, [format_opt]) :: iodata
@spec format_file!(binary, keyword) :: iodata
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
string = File.read!(file)
formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1128,8 +1023,7 @@ defmodule Code do
Macro arguments are typically transformed by unquoting them into the
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types.
The options are described under `env_for_eval/1`.
See `eval_string/3` for a description of `binding` and `opts`.
## Examples
@@ -1151,7 +1045,7 @@ defmodule Code do
[a: 1, b: 2]
"""
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
{value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
@@ -1180,17 +1074,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
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """
@@ -1204,11 +1089,15 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`.
* `:prune_binding` - (since v1.14.2) prune binding to keep only
variables read or written by the evaluated code. Note that
variables used by modules are always pruned, even if later used
by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment.
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
@@ -1227,14 +1116,10 @@ defmodule Code do
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to `1`.
Defaults to 1.
* `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to `1`.
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
This is useful when the code parsed is embedded within another document.
Defaults to `0`.
Defaults to 1.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1243,7 +1128,7 @@ defmodule Code do
For example, `"null byte\\t\\x00"` will be kept as is instead of being
converted to a bitstring literal. Note if you set this option to false, the
resulting AST is no longer valid, but it can be useful to analyze/transform
source code, typically in combination with `quoted_to_algebra/2`.
source code, typically in in combination with `quoted_to_algebra/2`.
Defaults to `true`.
* `:existing_atoms_only` - when `true`, raises an error
@@ -1264,7 +1149,7 @@ defmodule Code do
* `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this
option overrides the `:existing_atoms_only` behavior for static
option overrides the `:existing_atoms_only` behaviour for static
atoms but `:existing_atoms_only` is still used for dynamic atoms,
such as atoms with interpolations.
@@ -1310,16 +1195,8 @@ defmodule Code do
* atoms used to represent single-letter sigils like `:sigil_X`
(but multi-letter sigils like `:sigil_XYZ` are encoded).
## Examples
iex> Code.string_to_quoted("1 + 3")
{:ok, {:+, [line: 1], [1, 3]}}
iex> Code.string_to_quoted("1 \ 3")
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
"""
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
@spec string_to_quoted(List.Chars.t(), keyword) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
@@ -1341,12 +1218,11 @@ defmodule Code do
It returns the AST if it succeeds,
raises an exception otherwise. The exception is a `TokenMissingError`
in case a token is missing (usually because the expression is incomplete),
`MismatchedDelimiterError` (in case of mismatched opening and closing delimiters) and
`SyntaxError` otherwise.
Check `string_to_quoted/2` for options information.
"""
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
@@ -1365,7 +1241,7 @@ defmodule Code do
Comments are maps with the following fields:
* `:line` - The line number of the source code
* `:line` - The line number the source code
* `:text` - The full text of the comment, including the leading `#`
@@ -1397,7 +1273,7 @@ defmodule Code do
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
charlist = to_charlist(string)
@@ -1427,7 +1303,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information.
"""
@doc since: "1.13.0"
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
def string_to_quoted_with_comments!(string, opts \\ []) do
charlist = to_charlist(string)
@@ -1436,11 +1312,13 @@ defmodule Code do
{forms, comments}
{:error, {location, error, token}} ->
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
input = {charlist, line, column, Keyword.get(opts, :indentation, 0)}
:elixir_errors.parse_error(location, file, error, token, input)
:elixir_errors.parse_error(
location,
Keyword.get(opts, :file, "nofile"),
error,
token,
{charlist, Keyword.get(opts, :line, 1), Keyword.get(opts, :column, 1)}
)
end
end
@@ -1450,7 +1328,7 @@ defmodule Code do
comment = %{
line: line,
column: column,
previous_eol_count: min(previous_eol_count(tokens), last_comment_distance(comments, line)),
previous_eol_count: previous_eol_count(tokens),
next_eol_count: next_eol_count(rest, 0),
text: List.to_string(comment)
}
@@ -1464,9 +1342,6 @@ defmodule Code do
defp next_eol_count([?\r, ?\n | rest], count), do: next_eol_count(rest, count + 1)
defp next_eol_count(_, count), do: count
defp last_comment_distance([%{line: last_line} | _], line), do: line - last_line
defp last_comment_distance([], _line), do: :infinity
defp previous_eol_count([{token, {_, _, count}} | _])
when token in [:eol, :",", :";"] and count > 0 do
count
@@ -1512,9 +1387,6 @@ defmodule Code do
## Options
This function accepts all options supported by `format_string!/2` for controlling
code formatting, plus these additional options:
* `:comments` - the list of comments associated with the quoted expression.
Defaults to `[]`. It is recommended that both `:token_metadata` and
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1525,13 +1397,17 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
The arity may be the atom `:*`, which implies all arities of
that name. The formatter already includes a list of functions
and this option augments this list.
* `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information.
"""
@doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
Inspect.Algebra.t()
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
def quoted_to_algebra(quoted, opts \\ []) do
quoted
|> Code.Normalizer.normalize(opts)
@@ -1650,8 +1526,8 @@ defmodule Code do
## Examples
Code.compiler_options(infer_signatures: false)
#=> %{infer_signatures: [:elixir]}
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
"""
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1680,12 +1556,6 @@ defmodule Code do
:elixir_config.get(key)
end
# TODO: Remove me in Elixir v2.0
def get_compiler_option(:warnings_as_errors) do
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
:ok
end
@doc """
Returns a list with all available compiler options.
@@ -1714,19 +1584,16 @@ defmodule Code do
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
This enables tooling to partially reconstruct the original source code,
for instance, to perform static analysis of code. Therefore, disabling
module. Defaults to `true`.
This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
`:debug_info` is not recommended as it removes the ability of the
Elixir compiler and other tools to provide feedback. If you want to
remove the `:debug_info` while deploying, tools like `mix release`
already do such by default.
Other environments, such as `mix test`, automatically disables this
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
Additionally, `mix test` disables it via the `:test_elixirc_options`
project configuration option.
This option can also be overridden per module using the `@compile` directive.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
@@ -1735,21 +1602,13 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - a list of applications of which modules
should be using during type inference. 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`, which is equivalent to setting it to `[:elixir]` only.
When setting this option, we recommend running `mix clean` so the current module
may be compiled from scratch. `mix test` automatically disables this option via
the `:test_elixirc_options` project configuration, as there is typically no need
to 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
@@ -1772,9 +1631,8 @@ defmodule Code do
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
This `:warn` behaviour only exists for compatibility reasons when working
with old dependencies.
It always returns `:ok`. Raises an error for invalid options.
@@ -1795,6 +1653,16 @@ defmodule Code do
: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, " <>
"got: #{inspect(value)}"
end
:elixir_config.put(:no_warn_undefined, value)
:ok
end
def put_compiler_option(key, value) when key in @list_compiler_options do
if not is_list(value) do
raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}"
@@ -1814,61 +1682,8 @@ defmodule Code do
:ok
end
def put_compiler_option(:infer_signatures, value) do
value =
cond do
value == false ->
false
value == true ->
[:elixir]
is_list(value) and Enum.all?(value, &is_atom/1) ->
value
true ->
raise "compiler option :infer_signatures should be a boolean or a list of applications, got: #{inspect(value)}"
end
:elixir_config.put(:infer_signatures, value)
: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, " <>
"got: #{inspect(value)}"
end
:elixir_config.put(:no_warn_undefined, value)
: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
# TODO: Remove me in Elixir v2.0
# TODO: Make this option have no effect on Elixir v2.0
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
if value == :warn do
IO.warn_once(
{__MODULE__, :on_undefined_variable},
fn ->
"setting :on_undefined_variable to :warn is deprecated. " <>
"The warning behaviour will be removed in future releases"
end,
3
)
end
:elixir_config.put(:on_undefined_variable, value)
:ok
end
@@ -1906,7 +1721,7 @@ defmodule Code do
Returns a list of tuples where the first element is the module name
and the second one is its bytecode (as a binary). A `file` can be
given as a second argument which will be used for reporting warnings
given as second argument which will be used for reporting warnings
and errors.
**Warning**: `string` can be any Elixir code and code can be executed with
@@ -2133,7 +1948,7 @@ defmodule Code do
case :code.ensure_loaded(module) do
{:error, :nofile} = error ->
if can_await_module_compilation?() do
case Kernel.ErrorHandler.ensure_compiled(module, :module, mode, nil) do
case Kernel.ErrorHandler.ensure_compiled(module, :module, mode) do
:found -> {:module, module}
:deadlock -> {:error, :unavailable}
:not_found -> {:error, :nofile}
@@ -2150,12 +1965,12 @@ defmodule Code do
@doc """
Returns `true` if the module is loaded.
This function doesn't attempt to load the module. For such behavior,
This function doesn't attempt to load the module. For such behaviour,
`ensure_loaded?/1` can be used.
## Examples
iex> Code.loaded?(String)
iex> Code.loaded?(Atom)
true
iex> Code.loaded?(NotYetLoaded)
@@ -2217,11 +2032,7 @@ defmodule Code do
@spec fetch_docs(module | String.t()) ::
{:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata,
docs :: [doc_element]}
| {:error,
:module_not_found
| :chunk_not_found
| {:invalid_chunk, binary}
| :invalid_beam}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
@@ -2292,9 +2103,6 @@ defmodule Code do
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
{:error, :beam_lib, _} ->
{:error, :invalid_beam}
end
end
+108 -271
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Formatter do
@moduledoc false
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
@@ -20,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 [:.., :"//"]
@@ -35,37 +31,30 @@ defmodule Code.Formatter do
@right_new_line_before_binary_operators [:|, :when]
# Operators that are logical cannot be mixed without parens
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
# Operators with next break fits
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
# Operators that always require parens even
# when they are their own parents as they are not semantically associative
@required_parens_even_when_parent [:--, :---]
# Operators that always require parens on operands
# when they are the parent of another operator with a difference precedence
# Most operators are listed, except comparison, arithmetic, and low precedence
# Operators that always require parens on operands when they are the parent
@required_parens_on_binary_operands [
:|>,
:<<<,
:>>>,
:|>,
:<~,
:~>,
:<<~,
:~>>,
:<~>,
:"<|>",
:in,
:"^^^",
:"//",
:++,
:--,
:+++,
:---,
:<>,
:..
:in,
:++,
:--,
:..,
:<>
]
@locals_without_parens [
@@ -158,7 +147,6 @@ defmodule Code.Formatter do
@doc """
Converts the quoted expression into an algebra document.
"""
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
def to_algebra(quoted, opts \\ []) do
comments = Keyword.get(opts, :comments, [])
@@ -194,11 +182,8 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils =
@@ -223,10 +208,8 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
}
end
@@ -283,7 +266,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -307,7 +290,6 @@ defmodule Code.Formatter do
end
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp quoted_to_algebra(
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
context,
@@ -384,11 +366,6 @@ defmodule Code.Formatter do
tuple_to_algebra(meta, [left, right], :flex_break, state)
end
# (left -> right)
defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
case meta[:delimiter] do
~s['''] ->
@@ -414,7 +391,7 @@ defmodule Code.Formatter do
{@double_heredoc
|> concat(string)
|> concat(@double_heredoc)
|> color_doc(:string, state.inspect_opts)
|> color(:string, state.inspect_opts)
|> force_unfit(), state}
else
string = escape_string(string, @double_quote)
@@ -422,7 +399,7 @@ defmodule Code.Formatter do
{@double_quote
|> concat(string)
|> concat(@double_quote)
|> color_doc(:string, state.inspect_opts), state}
|> color(:string, state.inspect_opts), state}
end
end
@@ -439,7 +416,6 @@ defmodule Code.Formatter do
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
end
# (unquote_splicing(...))
defp quoted_to_algebra(
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
context,
@@ -454,7 +430,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color_doc("nil", nil, state.inspect_opts), state}
{color("nil", nil, state.inspect_opts), state}
end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -471,7 +447,7 @@ defmodule Code.Formatter do
end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color_doc(:atom, state.inspect_opts), state}
|> color(:atom, state.inspect_opts), state}
end
# &1
@@ -494,62 +470,6 @@ defmodule Code.Formatter do
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
end
# disable migrate_call_parens_on_pipe within defmacro
defp quoted_to_algebra(
{atom, _, [{:|>, _, _}, _]} = ast,
context,
%{migrate_call_parens_on_pipe: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_call_parens_on_pipe: false})
end
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
@@ -559,13 +479,8 @@ defmodule Code.Formatter do
end
end
# ...
defp quoted_to_algebra({:..., _meta, []}, _context, state) do
{"...", state}
end
# 1..2//3
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
end
@@ -584,6 +499,11 @@ defmodule Code.Formatter do
remote_to_algebra(quoted, context, state)
end
# (left -> right)
defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
# [keyword: :list] (inner part)
# %{:foo => :bar} (inner part)
defp quoted_to_algebra(list, context, state) when is_list(list) do
@@ -597,12 +517,20 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do
{left, state} =
case left_arg do
{:__block__, _, [atom]} when is_atom(atom) ->
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
{:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), state}
{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}
{:__block__, _, [atom]} when is_atom(atom) ->
iodata =
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
[Atom.to_string(atom), ?:]
else
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
end
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -618,22 +546,16 @@ defmodule Code.Formatter do
end
doc =
concat(
group(left),
with_next_break_fits(next_break_fits?(right_arg, state), right, fn right ->
nest(glue(op, right), 2, :break)
end)
)
with_next_break_fits(next_break_fits?(right_arg, state), right, fn right ->
concat(group(left), group(nest(glue(op, group(right)), 2, :break)))
end)
{doc, state}
end
# #PID's and #Ref's may appear on regular AST
# Other foreign structures, such as maps and structs,
# may appear from Macro.to_string, so we stick a limit,
# although they won't be formatted accordingly.
defp quoted_to_algebra(unknown, _context, state) do
{inspect(unknown, printable_limit: :infinity), state}
{inspect(unknown), state}
end
## Blocks
@@ -703,12 +625,12 @@ defmodule Code.Formatter do
Atom.to_string(op)
end
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
with [left, right] <- args,
{_, _} <- augmented_binary_op(fun) do
{_, _} <- Code.Identifier.binary_op(fun) do
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
else
_ -> :error
@@ -735,7 +657,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
when op in @right_new_line_before_binary_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = op_string <> " "
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -772,7 +694,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
when op in @pipeline_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
max_line = line(meta)
@@ -786,7 +708,7 @@ defmodule Code.Formatter do
{{doc, @empty, 1}, state}
{{op, context}, arg}, _args, state ->
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = Atom.to_string(op) <> " "
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
{{concat(op_string, doc), @empty, 1}, state}
@@ -796,7 +718,7 @@ defmodule Code.Formatter do
end
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -806,13 +728,15 @@ defmodule Code.Formatter do
{right, state} =
binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0)
{op_string, right} =
doc =
cond do
op in @no_space_binary_operators ->
{op_string, group(right)}
op_doc = color(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators ->
{" " <> op_string <> " ", group(right)}
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true ->
eol? = eol?(meta, state)
@@ -820,15 +744,14 @@ defmodule Code.Formatter do
next_break_fits? =
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
{" " <> op_string,
with_next_break_fits(next_break_fits?, right, fn right ->
right = nest(concat(break(), right), nesting, :break)
if eol?, do: force_unfit(right), else: right
end)}
with_next_break_fits(next_break_fits?, right, fn right ->
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
right = nest(glue(op_doc, group(right)), nesting, :break)
right = if eol?, do: force_unfit(right), else: right
concat(group(left), group(right))
end)
end
op_doc = color_doc(op_string, :operator, state.inspect_opts)
doc = concat(concat(group(left), op_doc), group(right))
{doc, state}
end
@@ -850,54 +773,23 @@ defmodule Code.Formatter do
{wrap_in_parens(doc), state}
end
# |> var
# |> var()
defp binary_operand_to_algebra(
{var, meta, var_context},
context,
%{migrate_call_parens_on_pipe: true} = state,
:|>,
_parent_info,
:right,
_nesting
)
when is_atom(var) and is_atom(var_context) do
operand = {var, meta, []}
quoted_to_algebra(operand, context, state)
end
# |> var.fun
# |> var.fun()
defp binary_operand_to_algebra(
{{:., _, [_, fun]} = call, meta, []},
context,
%{migrate_call_parens_on_pipe: true} = state,
:|>,
_parent_info,
:right,
_nesting
)
when is_atom(fun) do
meta = Keyword.put_new_lazy(meta, :closing, fn -> [line: meta[:line]] end)
quoted_to_algebra({call, meta, []}, context, state)
end
defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do
{parent_assoc, parent_prec} = parent_info
with {op, meta, [left, right]} <- operand,
op_info = augmented_binary_op(op),
op_info = Code.Identifier.binary_op(op),
{_assoc, prec} <- op_info do
op_string = Atom.to_string(op)
cond do
# If we have the same operator and it is in the correct side,
# we don't add parens unless it is explicitly required.
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
# If the operator has the same precedence as the parent and is on
# the correct side, we respect the nesting rule to avoid multiple
# nestings. This only applies for left associativity or same operator.
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the operator requires parens (most of them do) or we are mixing logical operators
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
# If the parent requires parens or the precedence is inverted or
# it is in the wrong side, then we *need* parenthesis.
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
(op in @required_parens_logical_binary_operands and
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
@@ -1054,7 +946,7 @@ defmodule Code.Formatter do
)
when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -1101,9 +993,7 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state)
fun_doc =
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1171,7 +1061,7 @@ defmodule Code.Formatter do
fun
|> Atom.to_string()
|> string()
|> color_doc(:call, state.inspect_opts)
|> color(:call, state.inspect_opts)
|> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1265,7 +1155,7 @@ defmodule Code.Formatter do
args_doc =
if skip_parens? do
left_doc
|> concat(group(right_doc, :optimistic))
|> concat(next_break_fits(group(right_doc, :inherit), :enabled))
|> nest(:cursor, :break)
else
right_doc =
@@ -1273,7 +1163,8 @@ defmodule Code.Formatter do
|> nest(2, :break)
|> concat(break(""))
|> concat(")")
|> group(:optimistic)
|> group(:inherit)
|> next_break_fits(:enabled)
concat(nest(left_doc, 2, :break), right_doc)
end
@@ -1316,11 +1207,13 @@ defmodule Code.Formatter do
|> concat(args_doc)
|> nest(2)
|> concat(extra)
|> group()
skip_parens? ->
" "
|> concat(args_doc)
|> concat(extra)
|> group()
true ->
"("
@@ -1328,12 +1221,13 @@ defmodule Code.Formatter do
|> nest(2, :break)
|> concat(args_doc)
|> concat(extra)
|> group()
end
if next_break_fits? do
{group(doc, :pessimistic), state}
{next_break_fits(doc, :disabled), state}
else
{group(doc), state}
{doc, state}
end
end
@@ -1530,7 +1424,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} =
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1608,8 +1502,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts)
left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state}
end
@@ -1641,8 +1535,8 @@ defmodule Code.Formatter do
end
defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
end
defp tuple_to_algebra(meta, args, join, state) do
@@ -1652,8 +1546,8 @@ defmodule Code.Formatter do
{args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts)
left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color("}", :tuple, state.inspect_opts)
if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1663,11 +1557,16 @@ defmodule Code.Formatter do
end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
Atom.to_string(atom) |> color(:boolean, inspect_opts)
end
defp atom_to_algebra(nil, _, inspect_opts) do
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
Atom.to_string(nil) |> color(nil, inspect_opts)
end
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
defp atom_to_algebra(:"..//", _, inspect_opts) do
string(":\"..//\"") |> color(:atom, inspect_opts)
end
defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1680,7 +1579,7 @@ defmodule Code.Formatter do
_ -> ":\\\\"
end
string(string) |> color_doc(:atom, inspect_opts)
string(string) |> color(:atom, inspect_opts)
end
defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1693,7 +1592,7 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
@@ -1710,7 +1609,7 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color_doc(:number, inspect_otps)
|> color(:number, inspect_otps)
end
defp float_to_algebra(text, inspect_otps) do
@@ -1718,7 +1617,7 @@ defmodule Code.Formatter do
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_otps)
color(string, :number, inspect_otps)
end
defp insert_underscores("-" <> digits) do
@@ -1799,17 +1698,10 @@ defmodule Code.Formatter do
doc =
case args do
[_ | _] ->
concat_to_last_group(doc, ",")
[] when last_arg_mode == :force_comma ->
concat_to_last_group(doc, ",")
[] when last_arg_mode == :next_break_fits ->
doc |> ungroup_if_group() |> group(:optimistic)
[] when last_arg_mode == :none ->
doc
[_ | _] -> concat_to_last_group(doc, ",")
[] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",")
[] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled)
[] when last_arg_mode == :none -> doc
end
{{doc, @empty, 1}, state}
@@ -2304,15 +2196,10 @@ defmodule Code.Formatter do
unary_operator?(quoted) or binary_operator?(quoted)
end
# We convert ..// into two operators for simplicity,
# so we need to augment the binary table.
defp augmented_binary_op(:"//"), do: {:right, 190}
defp augmented_binary_op(op), do: Code.Identifier.binary_op(op)
defp binary_operator?(quoted) do
case quoted do
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
{op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
_ -> false
end
end
@@ -2327,14 +2214,11 @@ defmodule Code.Formatter do
defp with_next_break_fits(condition, doc, fun) do
if condition do
doc
|> group(:optimistic)
|> next_break_fits(:enabled)
|> fun.()
|> group(:pessimistic)
|> next_break_fits(:disabled)
else
doc
|> group()
|> fun.()
|> group()
fun.(doc)
end
end
@@ -2351,7 +2235,6 @@ defmodule Code.Formatter do
(not interpolated?(entries) and eol_or_comments?(meta, state))
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
meta[:delimiter] == ~s[''']
end
@@ -2493,24 +2376,20 @@ 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.
# It would be best if we had a mechanism to apply this.
defp concat_to_last_group([left | right], concat) do
[left | concat_to_last_group(right, concat)]
defp concat_to_last_group({:doc_cons, left, right}, concat) do
{:doc_cons, left, concat_to_last_group(right, concat)}
end
defp concat_to_last_group({:doc_group, group, mode}, concat) do
{:doc_group, concat(group, concat), mode}
{:doc_group, {:doc_cons, group, concat}, mode}
end
defp concat_to_last_group(other, concat) do
concat(other, concat)
{:doc_cons, other, concat}
end
defp ungroup_if_group({:doc_group, group, _mode}), do: group
@@ -2542,7 +2421,7 @@ defmodule Code.Formatter do
end
defp get_charlist_quotes(:heredoc, state) do
if state.migrate_charlists_as_sigils do
if state.normalize_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc}
else
{@single_heredoc, @single_heredoc}
@@ -2551,7 +2430,7 @@ defmodule Code.Formatter do
defp get_charlist_quotes({:regular, chunks}, state) do
cond do
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
true -> {@sigil_c_double, @double_quote}
end
@@ -2560,46 +2439,4 @@ defmodule Code.Formatter do
defp has_double_quote?(chunk) do
is_binary(chunk) and chunk =~ @double_quote
end
# Migration rewrites
@bool_operators [
:>,
:>=,
:<,
:<=,
:in
]
@guards [
:is_atom,
:is_boolean,
:is_nil,
:is_number,
:is_integer,
:is_float,
:is_binary,
:is_map,
:is_struct,
:is_non_struct_map,
:is_exception,
:is_list,
:is_tuple,
:is_function,
:is_reference,
:is_pid,
:is_port
]
defp negate_condition(condition) do
case condition do
{neg, _, [condition]} when neg in [:!, :not] -> condition
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
_ -> {:!, [], [condition]}
end
end
end
+31 -281
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Code.Fragment do
@moduledoc """
This module provides conveniences for analyzing fragments of
@@ -11,59 +8,6 @@ defmodule Code.Fragment do
@type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> term()),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like the retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples
iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
["foo\r\n", "bar\r\n", "baz"]
iex> Code.Fragment.lines("foo\nbar\nbaz")
["foo\n", "bar\n", "baz"]
iex> Code.Fragment.lines("")
[""]
"""
@doc since: "1.19.0"
def lines(string) do
lines(string, <<>>)
end
defp lines(<<?\n, rest::binary>>, acc),
do: [<<acc::binary, ?\n>> | lines(rest, <<>>)]
defp lines(<<char, rest::binary>>, acc),
do: lines(rest, <<acc::binary, char>>)
defp lines(<<>>, acc),
do: [acc]
@doc """
Receives a string and returns the cursor context.
@@ -87,7 +31,7 @@ defmodule Code.Fragment do
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
{:local_or_var, 'hello_wor'}
## Return values
@@ -99,9 +43,6 @@ defmodule Code.Fragment do
or `{:local_or_var, charlist}` and `charlist` is a static part
Examples are `__MODULE__.Submodule` or `@hello.Submodule`
* `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
catch, else, rescue) or a binary operator
* `{:dot, inside_dot, charlist}` - the context is a dot
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
@@ -192,10 +133,9 @@ defmodule Code.Fragment do
references, and more.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
@spec cursor_context(List.Chars.t(), keyword()) ::
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
| {:dot, inside_dot, charlist}
| {:dot_arity, inside_dot, charlist}
| {:dot_call, inside_dot, charlist}
@@ -245,15 +185,15 @@ defmodule Code.Fragment do
cursor_context(to_charlist(other), opts)
end
@operators ~c"\\<>+-*/:=|&~^%!$"
@starting_punctuation ~c",([{;"
@closing_punctuation ~c")]}\"'"
@operators ~c"\\<>+-*/:=|&~^%!"
@starter_punctuation ~c",([{;"
@non_starter_punctuation ~c")]}\"'.$"
@space ~c"\t\s"
@trailing_identifier ~c"?!"
@tilde_op_prefix ~c"<=~"
@non_identifier @trailing_identifier ++
@operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.]
@operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space
@textual_operators ~w(when not and or in)c
@keywords ~w(do end after else catch rescue fn true false nil)c
@@ -283,11 +223,11 @@ defmodule Code.Fragment do
# A local arity definition
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
# Starting a new expression
[h | _] when h in @starting_punctuation -> {:expr, 0}
# It is keyword, binary operator, a local or remote call without parens
rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces})
[h | _] when h in @starter_punctuation -> {:expr, 0}
# It is a local or remote call without parens
rest when spaces > 0 -> call_to_cursor_context({rest, spaces})
# It is an identifier
_ -> identifier_to_cursor_context(reverse, spaces, false)
_ -> identifier_to_cursor_context(reverse, 0, false)
end
end
@@ -302,8 +242,7 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{{:sigil, _}, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
{_, _} -> {:none, 0}
end
end
@@ -327,16 +266,8 @@ defmodule Code.Fragment do
end
end
defp closing_or_call_to_cursor_context({reverse, spaces}) do
if closing?(reverse) do
{{:block_keyword_or_binary_operator, ~c""}, 0}
else
call_to_cursor_context({reverse, spaces})
end
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
@@ -386,42 +317,12 @@ defmodule Code.Fragment do
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
dot(rest, count + 1, acc)
{rest, rest_count} ->
response =
if rest_count > count and closing?(rest),
do: :block_keyword_or_binary_operator,
else: :local_or_var
{{response, acc}, count}
_ ->
{{:local_or_var, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
# If it is a closing punctuation
defp closing?([h | _]) when h in @closing_punctuation, do: true
# Closing bitstring (but deal with operators)
defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
# Keywords
defp closing?(rest) do
case split_non_identifier(rest, []) do
{~c"nil", _} -> true
{~c"true", _} -> true
{~c"false", _} -> true
{[digit | _], _} when digit in ?0..?9 -> true
{[upper | _], _} when upper in ?A..?Z -> true
{[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
{_, _} -> false
end
end
defp split_non_identifier([h | t], acc) when h not in @non_identifier,
do: split_non_identifier(t, [h | acc])
defp split_non_identifier(rest, acc), do: {acc, rest}
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
defp identifier(rest, count), do: check_identifier(rest, count, [])
@@ -461,14 +362,6 @@ defmodule Code.Fragment do
:none
end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
@@ -588,10 +481,10 @@ defmodule Code.Fragment do
defp operator(rest, count, acc, _call_op?) do
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
{:ok, _, _, _, [{:atom, _, _}], []} ->
{:ok, _, _, _, [{:atom, _, _}]} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, _, _, [{_, _, op}], []} ->
{:ok, _, _, _, [{_, _, op}]} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
@@ -654,7 +547,7 @@ defmodule Code.Fragment do
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
## Differences to `cursor_context/2`
@@ -669,13 +562,13 @@ defmodule Code.Fragment do
`local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
* This function returns keywords as `{:keyword, ~c"do"}`
* This function returns keywords as `{:keyword, 'do'}`
* This function never returns `:expr`
@@ -683,7 +576,7 @@ defmodule Code.Fragment do
of examples and their return values.
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
@spec surround_context(List.Chars.t(), position(), keyword()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
@@ -697,9 +590,7 @@ defmodule Code.Fragment do
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
| {:keyword, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
@@ -749,16 +640,7 @@ defmodule Code.Fragment do
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
{rest, _} = strip_spaces(rest, 0)
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
@@ -774,9 +656,6 @@ defmodule Code.Fragment do
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
@@ -792,12 +671,6 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset)
@@ -810,9 +683,6 @@ defmodule Code.Fragment do
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
@@ -845,16 +715,6 @@ defmodule Code.Fragment do
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset)
@@ -1201,23 +1061,16 @@ 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.
Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed.
Defaults to `1`.
Defaults to 1.
* `:column` - the starting column of the string being parsed.
Defaults to `1`.
Defaults to 1.
* `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`.
@@ -1230,117 +1083,14 @@ defmodule Code.Fragment do
* `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts =
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end
+31 -66
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Identifier do
@moduledoc false
@@ -17,7 +13,7 @@ defmodule Code.Identifier do
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
def unary_op(op) do
cond do
op in [:&, :...] -> {:non_associative, 90}
op in [:&] -> {:non_associative, 90}
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
op in [:@] -> {:non_associative, 320}
true -> :error
@@ -48,6 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
op in [:in] -> {:left, 170}
op in [:"^^^"] -> {:left, 180}
op in [:"//"] -> {:right, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
@@ -75,11 +72,11 @@ defmodule Code.Identifier do
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: binary(), remaining :: binary()}
{escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do
escape(binary, char, limit, <<>>, fun)
escape(binary, char, limit, [], fun)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
@@ -87,96 +84,64 @@ defmodule Code.Identifier do
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
if value = fun.(h) do
value = IO.iodata_to_binary(value)
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
else
escape(t, char, decrement(count), escape_char(h, acc), fun)
end
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
defp escape_char(0), do: [?\\, ?0]
defp escape_char(char, acc)
# Some characters that are confusing (zero-width / alternative spaces) are displayed
# using their unicode representation:
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
@escaped_bom :binary.bin_to_list("\\uFEFF")
defp escape_char(65279), do: @escaped_bom
# BOM
when char == 0xFEFF
# Mathematical invisibles
when char in 0x2061..0x2064
# Bidirectional neutral
when char in [0x061C, 0x200E, 0x200F]
# Bidirectional general (source of vulnerabilities)
when char in 0x202A..0x202E
when char in 0x2066..0x2069
# Interlinear annotations
when char in 0xFFF9..0xFFFC
# Zero-width joiners and non-joiners
when char in [0x200C, 0x200D, 0x034F]
# Non-break space / zero-width space
when char in [0x00A0, 0x200B, 0x2060]
# Line/paragraph separators
when char in [0x2028, 0x2029]
# Spaces
when char in 0x2000..0x200A
when char == 0x205F do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
end
defp escape_char(char, acc)
defp escape_char(char)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<acc::binary, char::utf8>>
<<char::utf8>>
end
defp escape_char(char, acc) when char < 0x100 do
defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
[?\\, ?x, to_hex(a), to_hex(b)]
end
defp escape_char(char, acc) when char < 0x10000 do
defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end
defp escape_char(char, acc) when char < 0x1000000 do
defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
end
defp escape_map(?\a), do: "\\a"
defp escape_map(?\b), do: "\\b"
defp escape_map(?\d), do: "\\d"
defp escape_map(?\e), do: "\\e"
defp escape_map(?\f), do: "\\f"
defp escape_map(?\n), do: "\\n"
defp escape_map(?\r), do: "\\r"
defp escape_map(?\t), do: "\\t"
defp escape_map(?\v), do: "\\v"
defp escape_map(?\\), do: "\\\\"
defp escape_map(?\a), do: [?\\, ?a]
defp escape_map(?\b), do: [?\\, ?b]
defp escape_map(?\d), do: [?\\, ?d]
defp escape_map(?\e), do: [?\\, ?e]
defp escape_map(?\f), do: [?\\, ?f]
defp escape_map(?\n), do: [?\\, ?n]
defp escape_map(?\r), do: [?\\, ?r]
defp escape_map(?\t), do: [?\\, ?t]
defp escape_map(?\v), do: [?\\, ?v]
defp escape_map(?\\), do: [?\\, ?\\]
defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1}
+38 -48
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Code.Normalizer do
@moduledoc false
@@ -14,7 +11,6 @@ defmodule Code.Normalizer do
Wraps literals in the quoted expression to conform to the AST format expected
by the formatter.
"""
@spec normalize(Macro.t(), keyword()) :: Macro.t()
def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true)
@@ -62,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
@@ -100,7 +96,6 @@ defmodule Code.Normalizer do
end
# Charlists with interpolations
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
@@ -130,17 +125,22 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]}
end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped. However, it should be escaped if applicable.
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
# it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta})
right = maybe_escape_literal(right, state)
{:., meta, [left, right]}
end
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
normalize_args(quoted, state)
end
# left -> right
defp do_normalize({:->, meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
@@ -169,6 +169,9 @@ defmodule Code.Normalizer do
right = normalize_map_args(right, state)
[{:|, pipe_meta, [left, right]}]
[{_, _, _} = call] ->
[do_normalize(call, state)]
args ->
normalize_map_args(args, state)
end
@@ -178,17 +181,16 @@ defmodule Code.Normalizer do
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
with "sigil_" <> _ <- Atom.to_string(sigil),
true <- binary_interpolated?(args),
true <- List.ascii_printable?(modifiers) do
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
when is_list(args) and is_atom(sigil) do
case Atom.to_string(sigil) do
"sigil_" <> _ ->
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
else
_ ->
normalize_call(quoted, state)
end
@@ -293,18 +295,21 @@ defmodule Code.Normalizer do
# Lists
defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do
# It's a charlist, we normalize it as a ~C sigil
string =
# It's a charlist
list =
if state.escape do
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist)
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
else
List.to_string(list)
list
end
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
{:__block__, meta, [list]}
else
meta =
if line = state.parent_meta[:line] do
@@ -351,14 +356,14 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
@@ -368,20 +373,11 @@ defmodule Code.Normalizer do
last_args =
case last_arg do
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
cond do
key_meta[:format] == :keyword ->
[keyword]
block_keyword?(keyword) ->
[
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
{{:__block__, [format: :keyword] ++ meta, args}, value}
end)
]
true ->
[last_arg]
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
end
[] ->
@@ -395,12 +391,6 @@ defmodule Code.Normalizer do
end
end
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
do: block_keyword?(tail)
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false
defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
+11 -9
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Typespec do
@moduledoc false
@@ -31,6 +27,11 @@ defmodule Code.Typespec do
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -287,6 +288,7 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -315,6 +317,10 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
@@ -332,14 +338,10 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
+2 -18
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defprotocol Collectable do
@moduledoc """
A protocol to traverse data structures.
@@ -98,10 +94,10 @@ end
defimpl Collectable, for: List do
def into(list) do
# TODO: Change the behavior so the into always comes last on Elixir v2.0
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
if list != [] do
IO.warn(
"the Collectable protocol is deprecated for non-empty lists. The behavior of " <>
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
@@ -144,10 +140,6 @@ defimpl Collectable, for: BitString do
__acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a binary requires a bitstring, got: #{inspect(other)}"
end
{[binary], fun}
@@ -163,10 +155,6 @@ defimpl Collectable, for: BitString do
_acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a bitstring requires a bitstring, got: #{inspect(other)}"
end
{bitstring, fun}
@@ -184,10 +172,6 @@ defimpl Collectable, for: Map do
_map_acc, :halt ->
:ok
_map_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a map requires {key, value} tuples, got: #{inspect(other)}"
end
{map, fun}
+9 -45
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config do
@moduledoc ~S"""
A simple keyword-based configuration API.
@@ -45,8 +41,7 @@ defmodule Config do
> application environment is effectively a global storage. Also note that
> the `config/config.exs` of a library is not evaluated when the library is
> used as a dependency, as configuration is always meant to configure the
> current project. For more information, see ["Using application configuration for
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
> current project. For more information, read our [library guidelines](library-guidelines.md).
## Migrating from `use Mix.Config`
@@ -87,9 +82,9 @@ defmodule Config do
...
end
The only places where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which are always within
the `Mix.Tasks` namespace.
The only files where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which always within the
`Mix.Tasks` namespace.
## `config/runtime.exs`
@@ -98,12 +93,6 @@ defmodule Config do
(assembled with `mix release`).
"""
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports}
@@ -141,6 +130,7 @@ defmodule Config do
config :logger,
level: :warn,
backends: [:console]
config :logger,
level: :info,
@@ -148,12 +138,12 @@ defmodule Config do
will have a final configuration for `:logger` of:
[level: :info, truncate: 1024]
[level: :info, backends: [:console], truncate: 1024]
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
@@ -204,37 +194,11 @@ 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.
In Mix projects this function returns the environment this configuration
file is executed on.
In releases, returns the `MIX_ENV` specified when running `mix release`.
file is executed on. In releases, the environment when `mix release` ran.
This is most often used to execute conditional code:
@@ -312,7 +276,7 @@ defmodule Config do
end
@doc false
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target)
+1 -16
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config.Provider do
@moduledoc """
Specifies a provider API that loads configuration during boot.
@@ -111,16 +107,6 @@ defmodule Config.Provider do
"""
@type config_path :: {:system, binary(), binary()} | binary()
@typedoc """
Options for `init/3`.
"""
@type init_opts :: [
extra_config: config(),
prune_runtime_sys_config_after_boot: boolean(),
reboot_system_after_config: boolean(),
validate_compile_env: [{atom(), [atom()], term()}]
]
@doc """
Invoked when initializing a config provider.
@@ -206,7 +192,6 @@ defmodule Config.Provider do
@reboot_mode_key :config_provider_reboot_mode
@doc false
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
@@ -327,7 +312,7 @@ defmodule Config.Provider do
"""
the application #{inspect(app)} has a different value set #{path(key, path)} \
during runtime compared to compile time. Since this application environment entry was \
marked as compile time, this difference can lead to different behavior than expected:
marked as compile time, this difference can lead to different behaviour than expected:
* Compile time value #{return_to_text(compile_return)}
* Runtime value #{return_to_text(runtime_return)}
+3 -13
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config.Reader do
@moduledoc """
API for reading config files defined with `Config`.
@@ -46,12 +42,6 @@ defmodule Config.Reader do
@behaviour Config.Provider
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@impl true
def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path)
@@ -74,7 +64,7 @@ defmodule Config.Reader do
Accepts the same options as `read!/2`.
"""
@doc since: "1.11.0"
@spec eval!(Path.t(), binary, config_opts) :: keyword
@spec eval!(Path.t(), binary, keyword) :: keyword
def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -96,7 +86,7 @@ defmodule Config.Reader do
"""
@doc since: "1.9.0"
@spec read!(Path.t(), config_opts) :: keyword
@spec read!(Path.t(), keyword) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -110,7 +100,7 @@ defmodule Config.Reader do
option cannot be disabled in `read_imports!/2`.
"""
@doc since: "1.9.0"
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths"
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Dict do
@moduledoc ~S"""
Generic API for dictionaries.
+82 -43
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule DynamicSupervisor do
@moduledoc ~S"""
A supervisor optimized to only start children dynamically.
@@ -137,6 +133,67 @@ defmodule DynamicSupervisor do
A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`.
## Migrating from Supervisor's :simple_one_for_one
In case you were using the deprecated `:simple_one_for_one` strategy from
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
few steps.
Imagine the given "old" code:
defmodule MySupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
Supervisor.start_child(__MODULE__, [foo, bar, baz])
end
@impl true
def init(init_arg) do
children = [
# Or the deprecated: worker(MyWorker, [init_arg])
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
end
It can be upgraded to the DynamicSupervisor like this:
defmodule MySupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# If MyWorker is not using the new child specs, we need to pass a map:
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: [init_arg]
)
end
end
The difference is that the `DynamicSupervisor` expects the child specification
at the moment `start_child/2` is called, and no longer on the init callback.
If there are any initial arguments given on initialization, such as `[initial_arg]`,
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
"""
@behaviour GenServer
@@ -158,7 +215,10 @@ defmodule DynamicSupervisor do
extra_arguments: [term()]
}
@typedoc "Options given to `start_link/1` and `init/1` functions"
@typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@type init_option ::
{:strategy, strategy()}
| {:max_restarts, non_neg_integer()}
@@ -169,15 +229,7 @@ defmodule DynamicSupervisor do
@typedoc "Supported strategies"
@type strategy :: :one_for_one
@typedoc """
Return values of `start_child` functions.
Unlike `Supervisor`, this module ignores the child spec ids,
so `{:error, {:already_started, pid}}` is not returned for child specs
given with the same id. `{:error, {:already_started, pid}}` is returned
however if a duplicate name is used when using
[name registration](`m:GenServer#module-name-registration`).
"""
@typedoc "Return values of `start_child` functions"
@type on_start_child ::
{:ok, pid}
| {:ok, pid, info :: term}
@@ -225,7 +277,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -293,11 +345,9 @@ defmodule DynamicSupervisor do
specified in the child spec given to `start_child/2`. Defaults to
an empty list.
* Any of the standard [GenServer options](`t:GenServer.option/0`)
"""
@doc since: "1.6.0"
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
@spec start_link([option | init_option]) :: Supervisor.on_start()
def start_link(options) when is_list(options) do
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
{sup_opts, start_opts} = Keyword.split(options, keys)
@@ -331,15 +381,9 @@ defmodule DynamicSupervisor do
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
with `:normal` reason.
## Options
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
callback.
"""
@doc since: "1.6.0"
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
@spec start_link(module, term, [option]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end
@@ -347,17 +391,11 @@ defmodule DynamicSupervisor do
@doc """
Dynamically adds a child specification to `supervisor` and starts that child.
`child_spec` should be a valid [child specification](`m:Supervisor#module-child-specification`).
The child process will be started as defined in the child specification. Note that while
`child_spec` should be a valid child specification as detailed in the
"Child specification" section of the documentation for `Supervisor`. The child
process will be started as defined in the child specification. Note that while
the `:id` field is still required in the spec, the value is ignored and
therefore does not need to be unique. Unlike `Supervisor`, this module does not
return `{:error, {:already_started, pid}}` for child specs given with the same id.
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
used when using [name registration](`m:GenServer#module-name-registration`).
This function will block the `DynamicSupervisor` until the child initializes.
When starting too many processes dynamically, you may want to use a
`PartitionSupervisor` to split the work across multiple processes.
therefore does not need to be unique.
If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and
@@ -462,14 +500,6 @@ defmodule DynamicSupervisor do
@doc """
Terminates the given child identified by `pid`.
This function will block the `DynamicSupervisor` until the child
terminates, which may take an arbitrary amount of time if the child
is trapping exits and implements its own terminate callback.
For this reason, it is often better to ask the child process
itself to terminate, often by declaring in its child spec it has
a restart strategy of `:transient` (or `:temporary`) and then
sending it a message to stop with reason `:shutdown`.
If successful, this function returns `:ok`. If there is no process with
the given PID, this function returns `{:error, :not_found}`.
"""
@@ -1106,6 +1136,15 @@ defmodule DynamicSupervisor do
]
end
@impl true
def format_status(:terminate, [_pdict, state]) do
state
end
def format_status(_, [_pdict, %{mod: mod} = state]) do
[data: [{~c"State", state}], supervisor: [{~c"Callback", mod}]]
end
## Helpers
@compile {:inline, call: 2}
+179 -432
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+81 -519
View File
File diff suppressed because it is too large Load Diff
+13 -17
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
require Record
defmodule File.Stat do
@@ -64,19 +60,19 @@ defmodule File.Stat do
defstruct keys
@type t :: %__MODULE__{
size: non_neg_integer() | :undefined,
type: :device | :directory | :regular | :other | :symlink | :undefined,
access: :read | :write | :read_write | :none | :undefined,
atime: :calendar.datetime() | integer() | :undefined,
mtime: :calendar.datetime() | integer() | :undefined,
ctime: :calendar.datetime() | integer() | :undefined,
mode: non_neg_integer() | :undefined,
links: non_neg_integer() | :undefined,
major_device: non_neg_integer() | :undefined,
minor_device: non_neg_integer() | :undefined,
inode: non_neg_integer() | :undefined,
uid: non_neg_integer() | :undefined,
gid: non_neg_integer() | :undefined
size: non_neg_integer(),
type: :device | :directory | :regular | :other | :symlink,
access: :read | :write | :read_write | :none,
atime: :calendar.datetime() | integer(),
mtime: :calendar.datetime() | integer(),
ctime: :calendar.datetime() | integer(),
mode: non_neg_integer(),
links: non_neg_integer(),
major_device: non_neg_integer(),
minor_device: non_neg_integer(),
inode: non_neg_integer(),
uid: non_neg_integer(),
gid: non_neg_integer()
}
@doc """
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule File.Stream do
@moduledoc """
Defines a `File.Stream` struct returned by `File.stream!/3`.
+30 -44
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
import Kernel, except: [round: 1]
defmodule Float do
@@ -143,8 +139,6 @@ defmodule Float do
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
iex> Float.parse(".12")
:error
iex> Float.parse("pi")
:error
iex> Float.parse("1.7976931348623159e+308")
@@ -168,50 +162,40 @@ defmodule Float do
end
defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_unsigned(rest, false, false, [digit])
do: parse_unsigned(rest, false, false, <<digit>>)
defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_unsigned(rest, dot?, e?, [digit | acc])
do: parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
do: parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])
do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
# When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
# rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, true = _e?, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
:erlang.binary_to_float(add_dot(acc, dot?))
rescue
ArgumentError -> :error
else
float -> {float, rest}
end
defp parse_unsigned(rest, dot?, false = _e?, acc) do
float =
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
{float, rest}
end
defp parse_unsigned(rest, dot?, false = _e?, acc),
do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
defp add_dot(acc, false), do: acc <> ".0"
@doc """
Rounds a float to the largest float less than or equal to `number`.
@@ -226,7 +210,7 @@ defmodule Float do
## Known issues
The behavior of `floor/2` for floats can be surprising. For example:
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
@@ -234,7 +218,7 @@ defmodule Float do
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behavior above.
which explains the behaviour above.
## Examples
@@ -270,7 +254,7 @@ defmodule Float do
The operation is performed on the binary floating point, without a
conversion to decimal.
The behavior of `ceil/2` for floats can be surprising. For example:
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
@@ -278,7 +262,7 @@ defmodule Float do
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behavior above.
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -323,7 +307,7 @@ defmodule Float do
## Known issues
The behavior of `round/2` for floats can be surprising. For example:
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
@@ -331,8 +315,8 @@ defmodule Float do
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
@@ -591,23 +575,27 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples
iex> Float.to_charlist(7.0)
~c"7.0"
'7.0'
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) do
:erlang.float_to_list(float, [:short])
def to_charlist(float) when is_float(float) do
:io_lib_format.fwrite_g(float)
end
@doc """
@@ -626,8 +614,6 @@ defmodule Float do
For a configurable representation, use `:erlang.float_to_binary/2`.
Inlined by the compiler.
## Examples
iex> Float.to_string(7.0)
@@ -635,8 +621,8 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) do
:erlang.float_to_binary(float, [:short])
def to_string(float) when is_float(float) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
@doc false
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Function do
@moduledoc """
A set of functions for working with functions.
+3 -9
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule GenEvent do
# Functions from this module are deprecated in elixir_dispatch.
@@ -26,7 +22,7 @@ defmodule GenEvent do
This approach has some shortcomings (it provides no back-pressure for example)
but can still replace GenEvent for low-profile usages of it. [This blog post
by José
Valim](https://dashbit.co/blog/replacing-genevent-by-a-supervisor-plus-genserver)
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
has more detailed information on this approach.
### GenStage
@@ -50,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) ::
{:ok, state}
| {:ok, state, :hibernate}
| {:error, reason :: term}
when state: term
| {:error, reason :: any}
when state: any
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state}
@@ -511,8 +507,6 @@ defmodule GenEvent do
{:ok, states, [name, handlers, hib]}
end
# Keeping deprecated format_status/2 since the current implementation is not
# compatible with format_status/1 and GenEvent is deprecated anyway
@doc false
def format_status(opt, status_data) do
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
-8
View File
@@ -1,10 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule GenEvent.Stream do
@moduledoc false
@moduledoc deprecated: "This functionality is no longer supported"
defstruct manager: nil, timeout: :infinity
@type t :: %__MODULE__{manager: GenEvent.manager(), timeout: timeout}
@@ -51,9 +46,6 @@ defmodule GenEvent.Stream do
end
defimpl Enumerable, for: GenEvent.Stream do
@moduledoc false
@moduledoc deprecated: "This functionality is no longer supported"
def reduce(stream, acc, fun) do
start_fun = fn -> start(stream) end
next_fun = &next(stream, &1)
+28 -56
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule GenServer do
@moduledoc """
A behaviour module for implementing the server of a client-server relation.
@@ -207,16 +203,14 @@ defmodule GenServer do
The generated `child_spec/1` can be customized with the following options:
* `:id` - the child specification identifier, defaults to the current module
* [`:restart`](`m:Supervisor#module-restart-values-restart`) - when the
child should be restarted, defaults to `:permanent`
* [`:shutdown`](`m:Supervisor#module-shutdown-values-shutdown`) - how to
shut down the child, either immediately or by giving it time to shut down
* `:restart` - when the child should be restarted, defaults to `:permanent`
* `:shutdown` - how to shut down the child, either immediately or by giving it time to shut down
For example:
use GenServer, restart: :transient, shutdown: 10_000
See the ["Child specification"](`m:Supervisor#module-child_spec-1-function`) section in the `Supervisor` module for more
See the "Child specification" section in the `Supervisor` module for more
detailed information. The `@doc` annotation immediately preceding
`use GenServer` will be attached to the generated `child_spec/1` function.
@@ -271,14 +265,6 @@ defmodule GenServer do
generated atoms won't be garbage-collected. For such cases, you can
set up your own local registry by using the `Registry` module.
For example:
{:ok, _} = Registry.start_link(keys: :unique, name: :stacks)
name = {:via, Registry, {:stacks, "stack 1"}}
{:ok, _pid} = GenServer.start_link(Stack, "hello", name: name)
GenServer.whereis(name)
#=> #PID<0.150.0>
## Receiving "regular" messages
The goal of a `GenServer` is to abstract the "receive" loop for developers,
@@ -414,7 +400,7 @@ defmodule GenServer do
or is suspended, the parent PID, the debugger state, and the state of
the behaviour module, which includes the callback module state
(as returned by `:sys.get_state/2`). It's possible to change how this
status is represented by defining the optional `c:GenServer.format_status/1`
status is represented by defining the optional `c:GenServer.format_status/2`
callback.
* `:sys.trace/3` - prints all the system events to `:stdio`.
* `:sys.statistics/3` - manages collection of process statistics.
@@ -429,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)
@@ -537,8 +523,8 @@ defmodule GenServer do
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
| {:stop, reason :: any}
when state: any
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
@@ -773,39 +759,22 @@ defmodule GenServer do
when old_vsn: term | {:down, term}
@doc """
This function is called by a `GenServer` process in the following situations:
Invoked in some cases to retrieve a formatted version of the `GenServer` status:
* [`:sys.get_status/1,2`](`:sys.get_status/1`) is invoked to get the `GenServer` status.
* The `GenServer` process terminates abnormally and logs an error.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenServer`; in such cases, `reason` is `:normal`
This callback is used to limit the status of the process returned by
[`:sys.get_status/1,2`](`:sys.get_status/1`) or sent to logger.
* the `GenServer` terminates abnormally and logs an error; in such cases,
`reason` is `:terminate`
The callback gets a map `status` describing the current status and shall return
a map `new_status` with the same keys, but it may transform some values.
Two possible use cases for this callback is to remove sensitive information
from the state to prevent it from being printed in log files, or to compact
large irrelevant status items that would only clutter the logs.
## Example
@impl GenServer
def format_status(status) do
Map.new(status, fn
{:state, state} -> {:state, Map.delete(state, :private_key)}
{:message, {:password, _}} -> {:message, {:password, "redacted"}}
key_value -> key_value
end)
end
This callback can be useful to control the *appearance* of the status of the
`GenServer`. For example, it can be used to return a compact representation of
the `GenServer`'s state to avoid having large state terms printed.
`pdict_and_state` is a two-elements list `[pdict, state]` where `pdict` is a
list of `{key, value}` tuples representing the current process dictionary of
the `GenServer` and `state` is the current state of the `GenServer`.
"""
@doc since: "1.17.0"
@callback format_status(status :: :gen_server.format_status()) ::
new_status :: :gen_server.format_status()
# TODO: Remove this on v2.0
@doc deprecated: "Use format_status/1 callback instead"
@callback format_status(reason, pdict_and_state :: list) :: term
when reason: :normal | :terminate
@@ -814,7 +783,6 @@ defmodule GenServer do
handle_info: 2,
handle_cast: 2,
handle_call: 3,
format_status: 1,
format_status: 2,
handle_continue: 2
@@ -859,7 +827,7 @@ defmodule GenServer do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -959,7 +927,7 @@ defmodule GenServer do
end
defmacro __before_compile__(env) do
if not Module.defines?(env.module, {:init, 1}) do
unless Module.defines?(env.module, {:init, 1}) do
message = """
function init/1 required by behaviour GenServer is not implemented \
(in module #{inspect(env.module)}).
@@ -1032,7 +1000,7 @@ defmodule GenServer do
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, term, options) :: on_start
@spec start_link(module, any, options) :: on_start
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:link, module, init_arg, options)
end
@@ -1042,7 +1010,7 @@ defmodule GenServer do
See `start_link/3` for more information.
"""
@spec start(module, term, options) :: on_start
@spec start(module, any, options) :: on_start
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:nolink, module, init_arg, options)
end
@@ -1112,8 +1080,8 @@ defmodule GenServer do
arrives or a timeout occurs. `c:handle_call/3` will be called on the server
to handle the request.
`server` can be a PID or any of the other values described in the
"Name registration" section of the documentation for this module.
`server` can be any of the values described in the "Name registration"
section of the documentation for this module.
## Timeouts
@@ -1134,6 +1102,10 @@ defmodule GenServer do
nil ->
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
# TODO: remove this clause when we require Erlang/OTP 25+
pid when pid == self() ->
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
pid ->
try do
:gen.call(pid, :"$gen_call", request, timeout)
+5 -17
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule HashDict do
@moduledoc """
Tuple-based HashDict implementation.
@@ -260,18 +256,15 @@ defmodule HashDict do
end
defimpl Enumerable, for: HashDict do
@moduledoc false
@moduledoc deprecated: "Use Map instead"
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
@@ -281,7 +274,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, module.size(dict)}
end
@@ -291,12 +284,9 @@ defimpl Enumerable, for: HashDict do
end
defimpl Collectable, for: HashDict do
@moduledoc false
@moduledoc deprecated: "Use Map instead"
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -309,13 +299,11 @@ defimpl Collectable, for: HashDict do
end
defimpl Inspect, for: HashDict do
@moduledoc false
@moduledoc deprecated: "Use Map instead"
import Inspect.Algebra
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end
end
+5 -17
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule HashSet do
@moduledoc """
Tuple-based HashSet implementation.
@@ -274,24 +270,21 @@ defmodule HashSet do
end
defimpl Enumerable, for: HashSet do
@moduledoc false
@moduledoc deprecated: "Use MapSet instead"
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.size(set)}
end
@@ -301,12 +294,9 @@ defimpl Enumerable, for: HashSet do
end
defimpl Collectable, for: HashSet do
@moduledoc false
@moduledoc deprecated: "Use MapSet instead"
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
@@ -319,13 +309,11 @@ defimpl Collectable, for: HashSet do
end
defimpl Inspect, for: HashSet do
@moduledoc false
@moduledoc deprecated: "Use MapSet instead"
import Inspect.Algebra
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end
end
+184 -273
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
import Kernel, except: [inspect: 1]
import Inspect.Algebra
@@ -64,11 +60,9 @@ defprotocol Inspect do
* `:except` - remove the given fields when inspecting.
* `:optional` - (since v1.14.0) a list of fields that should not be
included when they match their default value. This can be used to
simplify the struct representation at the cost of hiding
information. Since v1.19.0, the `:all` atom can be passed to
mark all fields as optional.
* `:optional` - (since v1.14.0) do not include a field if it
matches its default value. This can be used to simplify the
struct representation at the cost of hiding information.
Whenever `:only` or `:except` are used to restrict fields,
the struct will be printed using the `#User<...>` notation,
@@ -84,25 +78,15 @@ defprotocol Inspect do
#=> #User<id: 1, name: "Jane", ...>
If you use only the `:optional` option, the struct will still be
printed as a valid struct.
defmodule Point do
@derive {Inspect, optional: [:z]}
defstruct [x: 0, y: 0, z: 0]
end
inspect(%Point{x: 1})
%Point{x: 1, y: 0}
printed as `%User{...}`.
## Custom implementation
You can also define your custom protocol implementation by
defining the `inspect/2` function. The function receives the
entity to be inspected followed by the inspecting options,
represented by the struct `Inspect.Opts` and it must return
an algebra document alongside the updated options (or, optionally,
just the algebra document). Building of the algebra document
is done with `Inspect.Algebra`.
represented by the struct `Inspect.Opts`. Building of the
algebra document is done with `Inspect.Algebra`.
Many times, inspecting a structure can be implemented in function
of existing entities. For example, here is `MapSet`'s `inspect/2`
@@ -112,19 +96,16 @@ defprotocol Inspect do
import Inspect.Algebra
def inspect(map_set, opts) do
{doc, opts} = to_doc_with_opts(MapSet.to_list(map_set), opts)
{concat(["MapSet.new(", doc, ")"]), opts}
concat(["MapSet.new(", Inspect.List.inspect(MapSet.to_list(map_set), opts), ")"])
end
end
First [`to_doc_with_opts/2`](`Inspect.Algebra.to_doc_with_opts/2`) is
used to convert another data structure into its algebra document and
then [`concat/1`](`Inspect.Algebra.concat/1`) concatenates algebra
documents together.
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from
`Inspect.Algebra` and it concatenates algebra documents together.
In the example above it is concatenating the string `"MapSet.new("`,
the document returned by `to_doc_with_opts/2`, and the final string `")"`.
Therefore, the MapSet with the numbers 1, 2, and 3 will be printed as:
the document returned by `Inspect.Algebra.to_doc/2`, and the final
string `")"`. Therefore, the MapSet with the numbers 1, 2, and 3
will be printed as:
iex> MapSet.new([1, 2, 3], fn x -> x * 2 end)
MapSet.new([2, 4, 6])
@@ -153,114 +134,11 @@ defprotocol Inspect do
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
Note that, from Elixir v1.19, the inspect protocol was augmented to
allow a two-element tuple with the document and the updated options
to be returned from the protocol.
"""
# 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, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
optional =
case Keyword.get(options, :optional, []) do
:all ->
fields
optional ->
:ok = validate_option(:optional, optional, fields, module)
optional
end
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_as_struct(
var!(struct),
var!(name),
var!(infos),
var!(opts)
)
end
end
end
end
defp validate_option(option, option_list, fields, module) do
if not is_list(option_list) do
raise ArgumentError,
"invalid value #{Kernel.inspect(option_list)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)} " <>
"(expected a list)"
end
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.
@@ -269,8 +147,7 @@ defprotocol Inspect do
`Inspect.Algebra.to_doc/2` should be preferred as it handles structs
and exceptions.
"""
@spec inspect(t, Inspect.Opts.t()) ::
Inspect.Algebra.t() | {Inspect.Algebra.t(), Inspect.Opts.t()}
@spec inspect(t, Inspect.Opts.t()) :: Inspect.Algebra.t()
def inspect(term, opts)
end
@@ -278,7 +155,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -298,7 +175,7 @@ defimpl Inspect, for: BitString do
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color_doc(IO.iodata_to_binary(inspected), :string, opts)
color(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
@@ -309,16 +186,14 @@ defimpl Inspect, for: BitString do
end
defp inspect_bitstring("", opts) do
color_doc("<<>>", :binary, opts)
color("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, %{limit: limit} = opts) do
left = color_doc("<<", :binary, opts)
right = color_doc(">>", :binary, opts)
inner = each_bit(bitstring, limit, opts)
doc = group(concat(concat(left, nest(inner, 2)), right))
new_limit = if limit == :infinity, do: limit, else: max(0, limit - byte_size(bitstring))
{doc, %{opts | limit: new_limit}}
defp inspect_bitstring(bitstring, opts) do
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
defp each_bit(_, 0, _) do
@@ -326,7 +201,7 @@ defimpl Inspect, for: BitString do
end
defp each_bit(<<>>, _counter, _opts) do
Inspect.Algebra.empty()
:doc_nil
end
defp each_bit(<<h::8>>, _counter, opts) do
@@ -353,7 +228,7 @@ end
defimpl Inspect, for: List do
def inspect([], opts) do
color_doc("[]", :list, opts)
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
@@ -384,56 +259,32 @@ defimpl Inspect, for: List do
lists
end
open = color_doc("[", :list, opts)
sep = color_doc(",", :list, opts)
close = color_doc("]", :list, opts)
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
color(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
container_doc_with_opts(open, term, close, opts, &keyword/2,
separator: sep,
break: :strict
)
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
true ->
container_doc_with_opts(open, term, close, opts, &to_doc_with_opts/2, separator: sep)
container_doc(open, term, close, opts, &to_doc/2, separator: sep)
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
{doc, opts} = to_doc_with_opts(value, opts)
{concat(key, concat(" ", doc)), opts}
key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@doc false
@@ -450,28 +301,16 @@ end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color_doc("{", :tuple, opts)
sep = color_doc(",", :tuple, opts)
close = color_doc("}", :tuple, opts)
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc_with_opts(
open,
Tuple.to_list(tuple),
close,
opts,
&to_doc_with_opts/2,
container_opts
)
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
end
defimpl Inspect, for: Map do
def inspect(map, opts) do
inspect_as_map(map, opts)
end
def inspect_as_map(map, opts) do
list =
if Keyword.get(opts.custom_options, :sort_maps) do
map |> Map.to_list() |> :lists.sort()
@@ -483,36 +322,34 @@ defimpl Inspect, for: Map do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color_doc(" => ", :map, opts)
sep = color(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
map_container_doc(list, "", opts, fun)
end
def inspect_as_struct(map, name, infos, opts) do
def inspect(map, name, infos, opts) do
fun = fn %{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts) end
map_container_doc(infos, name, opts, fun)
end
defp to_assoc({key, value}, opts, sep) do
{key_doc, opts} = to_doc_with_opts(key, opts)
{value_doc, opts} = to_doc_with_opts(value, opts)
{concat(concat(key_doc, sep), value_doc), opts}
concat(concat(to_doc(key, opts), sep), to_doc(value, opts))
end
defp map_container_doc(list, name, opts, fun) do
open = color_doc("%" <> name <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
container_doc_with_opts(open, list, close, opts, fun, separator: sep, break: :strict)
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
end
end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color_doc(inspected, :number, opts)
color(inspected, :number, opts)
end
defp base_to_value(base) do
@@ -550,48 +387,33 @@ defimpl Inspect, for: Float do
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
Float.to_charlist(float)
:io_lib_format.fwrite_g(float)
end
color_doc(IO.iodata_to_binary(formatted), :number, opts)
color(IO.iodata_to_binary(formatted), :number, opts)
end
end
defimpl Inspect, for: Regex do
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
case translate_options(regex_opts, []) do
:error ->
concat([
"Regex.compile!(",
to_doc(regex.source, opts),
", ",
to_doc(regex_opts, opts),
")"
])
translated_opts ->
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color_doc(source, :regex, opts)
end
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
end
defp translate_options([:dotall, {:newline, :anycrlf} | t], acc),
do: translate_options(t, [?s | acc])
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
defp translate_options([:unicode, :ucp | t], acc), do: translate_options(t, [?u | acc])
defp translate_options([:caseless | t], acc), do: translate_options(t, [?i | acc])
defp translate_options([:extended | t], acc), do: translate_options(t, [?x | acc])
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([:export | t], acc), do: translate_options(t, [?E | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, regex.opts])
color(source, :regex, opts)
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
@@ -627,7 +449,7 @@ defimpl Inspect, for: Function do
match?(@elixir_compiler ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__()}>"
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
@@ -662,6 +484,36 @@ defimpl Inspect, for: Function do
end
end
defimpl Inspect, for: Inspect.Error do
@impl true
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
message = Exception.message(inspect_error)
format_output(message, stacktrace)
end
defp format_output(message, [_ | _] = stacktrace) do
stacktrace = Exception.format_stacktrace(stacktrace)
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
Stacktrace:
#{stacktrace}
>\
"""
end
defp format_output(message, []) do
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
>\
"""
end
end
defimpl Inspect, for: PID do
def inspect(pid, _opts) do
"#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))
@@ -682,19 +534,97 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
def inspect(%module{} = struct, opts) do
info =
for %{field: field} = map <- module.__info__(:struct),
field != :__exception__,
do: map
defmacro __deriving__(module, struct, options) do
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
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
def inspect_as_struct(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc(">", :map, opts)
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)}
rescue
_ -> Inspect.Map.inspect(struct, opts)
else
{dunder, fields} ->
if Map.keys(dunder) == Map.keys(struct) do
infos =
for %{field: field} = info <- fields,
field not in [:__struct__, :__exception__],
do: info
Inspect.Map.inspect(struct, Macro.inspect_atom(:literal, module), infos, opts)
else
Inspect.Map.inspect(struct, opts)
end
end
end
def inspect(map, name, infos, opts) do
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
@@ -705,25 +635,6 @@ defimpl Inspect, for: Any do
end
end
defimpl Inspect, for: Range do
import Inspect.Algebra
import Kernel, except: [inspect: 2]
def inspect(first..last//1, opts) when last >= first do
concat([to_doc(first, opts), "..", to_doc(last, opts)])
end
def inspect(first..last//step, opts) do
concat([to_doc(first, opts), "..", to_doc(last, opts), "//", to_doc(step, opts)])
end
# TODO: Remove me on v2.0
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
step = if first <= last, do: 1, else: -1
inspect(Map.put(range, :step, step), opts)
end
end
require Protocol
Protocol.derive(
File diff suppressed because it is too large Load Diff
-90
View File
@@ -1,90 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Inspect.Error do
@moduledoc """
Raised when a struct cannot be inspected.
"""
@enforce_keys [:exception_module, :exception_message, :stacktrace, :inspected_struct]
defexception @enforce_keys
@impl true
def exception(arguments) when is_list(arguments) do
exception = Keyword.fetch!(arguments, :exception)
exception_module = exception.__struct__
exception_message = Exception.message(exception) |> String.trim_trailing("\n")
stacktrace = Keyword.fetch!(arguments, :stacktrace)
inspected_struct = Keyword.fetch!(arguments, :inspected_struct)
%Inspect.Error{
exception_module: exception_module,
exception_message: exception_message,
stacktrace: stacktrace,
inspected_struct: inspected_struct
}
end
@impl true
def message(%__MODULE__{
exception_module: exception_module,
exception_message: exception_message,
inspected_struct: inspected_struct
}) do
~s'''
got #{inspect(exception_module)} with message:
"""
#{pad(exception_message, 4)}
"""
while inspecting:
#{pad(inspected_struct, 4)}
'''
end
@doc false
def pad(message, padding_length)
when is_binary(message) and is_integer(padding_length) and padding_length >= 0 do
padding = String.duplicate(" ", padding_length)
message
|> String.split("\n")
|> Enum.map(fn
"" -> "\n"
line -> [padding, line, ?\n]
end)
|> IO.iodata_to_binary()
|> String.trim_trailing("\n")
end
end
defimpl Inspect, for: Inspect.Error do
@impl true
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
message = Exception.message(inspect_error)
format_output(message, stacktrace)
end
defp format_output(message, [_ | _] = stacktrace) do
stacktrace = Exception.format_stacktrace(stacktrace)
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
Stacktrace:
#{stacktrace}
>\
"""
end
defp format_output(message, []) do
"""
#Inspect.Error<
#{Inspect.Error.pad(message, 2)}
>\
"""
end
end
+17 -24
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Integer do
@moduledoc """
Functions for working with integers.
@@ -165,7 +161,7 @@ defmodule Integer do
@doc since: "1.4.0"
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
@@ -193,16 +189,13 @@ defmodule Integer do
@spec digits(integer, pos_integer) :: [integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
case integer do
0 -> [0]
_integer -> digits(integer, base, [])
end
do_digits(integer, base, [])
end
defp digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp digits(integer, base, acc),
do: digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
@doc """
Returns the integer represented by the ordered `digits`.
@@ -224,16 +217,16 @@ defmodule Integer do
"""
@spec undigits([integer], pos_integer) :: integer
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
undigits(digits, base, 0)
do_undigits(digits, base, 0)
end
defp undigits([], _base, acc), do: acc
defp do_undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
@doc """
Parses a text representation of an integer.
@@ -369,25 +362,25 @@ defmodule Integer do
## Examples
iex> Integer.to_charlist(123)
~c"123"
'123'
iex> Integer.to_charlist(+456)
~c"456"
'456'
iex> Integer.to_charlist(-789)
~c"-789"
'-789'
iex> Integer.to_charlist(0123)
~c"123"
'123'
iex> Integer.to_charlist(100, 16)
~c"64"
'64'
iex> Integer.to_charlist(-100, 16)
~c"-64"
'-64'
iex> Integer.to_charlist(882_681_651, 36)
~c"ELIXIR"
'ELIXIR'
"""
@spec to_charlist(integer, 2..36) :: charlist

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