Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0220bd267 | ||
|
|
6f5cc529d5 | ||
|
|
3d80256852 | ||
|
|
8731ef03d9 | ||
|
|
f8a2d05b57 | ||
|
|
1145dc0168 | ||
|
|
12f93df04f | ||
|
|
47a63a73b6 | ||
|
|
d0ebaa7cdd | ||
|
|
e9ea9dea7f | ||
|
|
1feef9cb34 | ||
|
|
5c5fa57234 | ||
|
|
e891b1a831 | ||
|
|
001a5eb2e8 | ||
|
|
6085ed0288 | ||
|
|
57be943f91 | ||
|
|
99cad598a8 | ||
|
|
ef4c926299 | ||
|
|
d1acad0155 | ||
|
|
4a69a00c71 | ||
|
|
a9ad1f1b2a | ||
|
|
53e3d8f55e | ||
|
|
f53b8c822d | ||
|
|
cdd312b0ca | ||
|
|
0cc53735a1 | ||
|
|
c88ecdec16 | ||
|
|
1b4536bf17 | ||
|
|
ab4d6817ab | ||
|
|
717490a1b0 | ||
|
|
8cb1361e1a | ||
|
|
967f4b412c | ||
|
|
8a876d6c78 | ||
|
|
831b12a116 | ||
|
|
d16497b966 | ||
|
|
7ef6dfc4e4 | ||
|
|
e1158a55a3 | ||
|
|
78cc8bea7a | ||
|
|
5914aba7e4 | ||
|
|
40301963e4 | ||
|
|
248d532ae4 | ||
|
|
74bc9506a9 | ||
|
|
41e218b160 | ||
|
|
94221f2894 | ||
|
|
6eaac2dc72 | ||
|
|
92650c84be | ||
|
|
b30cbb600b | ||
|
|
6092fb4618 | ||
|
|
07450832e7 | ||
|
|
34adbcb95a | ||
|
|
286d310ac3 | ||
|
|
2fc916088a | ||
|
|
9a428e9cdd | ||
|
|
090dddcf00 | ||
|
|
089c470e13 | ||
|
|
68481b7377 | ||
|
|
e2dc9ae72e | ||
|
|
51c95b6000 | ||
|
|
12917b00e5 | ||
|
|
ab9ecf2d7f | ||
|
|
eca809285e | ||
|
|
8e71e65aae | ||
|
|
c3a27be073 | ||
|
|
e10f9fb6fe | ||
|
|
f66ffd0733 | ||
|
|
696bad8de1 | ||
|
|
96c8e31410 | ||
|
|
70be440359 | ||
|
|
c6d6683225 | ||
|
|
db2504eca6 | ||
|
|
98c3f5c602 | ||
|
|
db4684b2f5 | ||
|
|
f3f65e4cd1 | ||
|
|
5331174934 | ||
|
|
f4f2be67d7 | ||
|
|
686daadaaa | ||
|
|
6e8563e930 | ||
|
|
65be24a8c2 | ||
|
|
5bd7a90864 | ||
|
|
831046fcb7 | ||
|
|
6c44feae2f | ||
|
|
35eef87590 | ||
|
|
fa091c6bd9 | ||
|
|
a561f81539 | ||
|
|
739c1b6292 | ||
|
|
3e1e6ddf8a | ||
|
|
ccb8c5a2e7 | ||
|
|
07e580cf17 | ||
|
|
1f9f713d9c | ||
|
|
9814a4cd61 | ||
|
|
3dfdd80dff | ||
|
|
0fdc7510b8 | ||
|
|
0e8873238d | ||
|
|
ba02eb90a0 | ||
|
|
fdc7716bf3 | ||
|
|
a050c00fbc | ||
|
|
d395c3e3cf | ||
|
|
c3e903f339 | ||
|
|
452ba40855 | ||
|
|
8854492cc5 | ||
|
|
105770cfa0 | ||
|
|
28b053a122 |
+120
-5
@@ -2,31 +2,146 @@ env:
|
||||
CIRRUS_CLONE_DEPTH: 50
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
ERLC_OPTS: "+warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
test_template: &DEFAULT_TEST_SETTINGS
|
||||
# don't cancel the task execution if it's master or a release branch
|
||||
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
|
||||
|
||||
test_linux_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
container:
|
||||
image: buildpack-deps:trusty
|
||||
cpu: 8
|
||||
memory: 1536Mi
|
||||
|
||||
env:
|
||||
PATH: "${CIRRUS_WORKING_DIR}/otp/bin:${PATH}"
|
||||
|
||||
matrix:
|
||||
- name: Linux, ${OTP_RELEASE}, Ubuntu 14.04
|
||||
alias: Linux Stable
|
||||
matrix:
|
||||
- env:
|
||||
CHECK_POSIX_COMPLIANT: true
|
||||
CHECK_REPRODUCIBLE: true
|
||||
OTP_RELEASE: OTP-23.0
|
||||
- env:
|
||||
OTP_RELEASE: OTP-22.3
|
||||
- env:
|
||||
OTP_RELEASE: OTP-22.0
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.3.8
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.0
|
||||
|
||||
- name: Linux, OTP-${OTP_RELEASE}, development, Ubuntu 14.04
|
||||
alias: Linux Development
|
||||
allow_failures: true
|
||||
skip_notifications: true
|
||||
depends_on:
|
||||
- Linux Stable
|
||||
- FreeBSD Stable
|
||||
matrix:
|
||||
- env:
|
||||
OTP_RELEASE: master
|
||||
- env:
|
||||
OTP_RELEASE: maint
|
||||
|
||||
install_script:
|
||||
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
|
||||
- mkdir -p otp
|
||||
- tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
- otp/Install -minimal ${CIRRUS_WORKING_DIR}/otp
|
||||
- rm -rf .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
test_erlang_script: make test_erlang
|
||||
|
||||
test_elixir_script: make test_elixir
|
||||
|
||||
check_posix_compliant_script: |
|
||||
if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
|
||||
apt update
|
||||
apt install -y shellcheck
|
||||
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"
|
||||
else
|
||||
echo "The format of the shell scripts is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
check_reproducible_script: |
|
||||
if [ -n "$CHECK_REPRODUCIBLE" ]; then
|
||||
make check_reproducible
|
||||
else
|
||||
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
|
||||
test_windows_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: Windows, OTP-${OTP_RELEASE}, Windows Server 2019
|
||||
alias: Windows Stable
|
||||
|
||||
matrix:
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 22.0
|
||||
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 21.0.1
|
||||
|
||||
windows_container:
|
||||
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
|
||||
os_version: ${OS_VERSION}
|
||||
cpu: 4
|
||||
memory: 6GB
|
||||
|
||||
install_script:
|
||||
- rmdir /s /q .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
test_erlang_script: make --keep-going test_erlang
|
||||
|
||||
test_elixir_script: make --keep-going test_elixir
|
||||
|
||||
|
||||
test_freebsd_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: FreeBSD 13.0
|
||||
name: FreeBSD 12.1
|
||||
alias: FreeBSD Stable
|
||||
|
||||
freebsd_instance:
|
||||
image_family: freebsd-13-0
|
||||
image_family: freebsd-12-1
|
||||
cpu: 8
|
||||
memory: 7424Mi
|
||||
|
||||
env:
|
||||
CHECK_REPRODUCIBLE: true
|
||||
LC_ALL: en_US.UTF-8
|
||||
PATH: $PATH:/usr/local/lib/erlang22/bin
|
||||
|
||||
install_script:
|
||||
- pkg install -y erlang-runtime22 git gmake
|
||||
- pkg install -y erlang git gmake
|
||||
- rm -rf .git
|
||||
- gmake compile
|
||||
|
||||
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
[
|
||||
inputs: [
|
||||
"lib/*/{lib,unicode,test}/**/*.{ex,exs}",
|
||||
"lib/*/*.exs",
|
||||
"lib/ex_unit/examples/*.exs",
|
||||
".formatter.exs"
|
||||
"lib/*/mix.exs"
|
||||
],
|
||||
|
||||
locals_without_parens: [
|
||||
# Formatter tests
|
||||
assert_format: 2,
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||
*.ex diff=elixir
|
||||
*.exs diff=elixir
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Linux, ${{ matrix.otp_release }}, Ubuntu 16.04
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
otp_release: ['OTP-24.0', 'OTP-23.3', 'OTP-23.0', 'OTP-22.3', 'OTP-22.0']
|
||||
development: [false]
|
||||
include:
|
||||
- otp_release: master
|
||||
development: true
|
||||
- otp_release: maint
|
||||
development: true
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Erlang/OTP
|
||||
run: |
|
||||
cd $RUNNER_TEMP
|
||||
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-16.04/${{ matrix.otp_release }}.tar.gz
|
||||
mkdir -p otp
|
||||
tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
otp/Install -minimal $(pwd)/otp
|
||||
echo "$(pwd)/otp/bin" >> $GITHUB_PATH
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
rm -rf .git
|
||||
make compile
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Dyalizer
|
||||
run: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
- name: Check reproducible builds
|
||||
run: taskset 1 make check_reproducible
|
||||
if: matrix.otp_release == 'OTP-24.0'
|
||||
|
||||
test_windows:
|
||||
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
|
||||
strategy:
|
||||
matrix:
|
||||
otp_release: ['22.0']
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Cache Erlang/OTP package
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang
|
||||
key: OTP-${{ matrix.otp_release }}-windows-2019
|
||||
- name: Install Erlang/OTP
|
||||
run: choco install -y erlang --version ${{ matrix.otp_release }}
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
remove-item '.git' -recurse -force
|
||||
make compile
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Erlang test suite
|
||||
run: make --keep-going test_erlang
|
||||
- name: Elixir test suite
|
||||
run: |
|
||||
del c:/Windows/System32/drivers/etc/hosts
|
||||
make --keep-going test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
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"
|
||||
@@ -0,0 +1,17 @@
|
||||
on: check_suite
|
||||
name: CI email
|
||||
jobs:
|
||||
sendEmail:
|
||||
name: Send email
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send email
|
||||
# Source: https://github.com/elixir-lang/elixir-ci
|
||||
uses: docker://fertapric/elixir-ci-email:latest
|
||||
env:
|
||||
APP_NAME: Cirrus CI
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIL_FROM: ci@elixir-lang.org
|
||||
MAIL_HOST: smtp.sendgrid.net
|
||||
MAIL_PASSWORD: ${{ secrets.CI_EMAIL_PASSWORD }}
|
||||
MAIL_USERNAME: ${{ secrets.CI_EMAIL_USERNAME }}
|
||||
+306
-197
@@ -1,282 +1,391 @@
|
||||
# Changelog for Elixir v1.12
|
||||
# Changelog for Elixir v1.10
|
||||
|
||||
Elixir v1.12 is out with improvements to scripting, tighter Erlang/OTP 24 integration, stepped ranges, and dozen of new functions across the standard library. Overall this is a small release, which continues our tradition of bringing Elixir developers quality of life improvements every 6 months.
|
||||
## Support for Erlang/OTP 21+
|
||||
|
||||
Elixir v1.12 requires Erlang/OTP 22+. We also recommend running `mix local.rebar` after installation to upgrade to the latest Rebar versions, which includes support for Erlang OTP/24+.
|
||||
Elixir v1.10 requires Erlang/OTP 21+, allowing Elixir to integrate with Erlang/OTP's new logger. Currently, this means that the logger level, logger metadata, as well as all log messages are now shared between Erlang and Elixir APIs.
|
||||
|
||||
## Scripting improvements: `Mix.install/2` and `System.trap_signal/3`
|
||||
We will continue improving the relationship between the logging systems in future releases. In particular, we plan to expose all log levels and runtime filtering functionalities available in Erlang directly into Elixir in the next Elixir version.
|
||||
|
||||
Elixir v1.12 brings new conveniences for those using Elixir for scripting (via `.exs` files). Elixir has been capable of managing dependencies for a quite long time, but it could only be done within Mix projects. In particular, the Elixir team is wary of global dependencies as any scripts that rely on system packages are brittle and hard to reproduce whenever your system changes.
|
||||
This release also adds two new guards, `is_struct/1` and `is_map_key/2`, thanks to the strict requirement on Erlang/OTP 21+.
|
||||
|
||||
`Mix.install/2` is meant to be a sweetspot between single-file scripts and full-blown Mix projects. With `Mix.install/2`, you can list your dependencies on top of your scripts. When you execute the script for the first time, Elixir will download, compile, and cache your dependencies before running your script. Future invocations of the script will simply read the compiled artefacts from the cache:
|
||||
## Releases improvements
|
||||
|
||||
Elixir v1.9 introduced releases as a mechanism to package self-contained applications. Elixir v1.10 further improves releases with bug fixes and new enhancements based on feedback we got from the community. The highlights are:
|
||||
|
||||
* Allow the dual boot system of releases to be disabled on environments that are boot-time sensitive, such as embedded devices
|
||||
|
||||
* Track and raise if compile-time configuration is set or changes at runtime (more in the next section)
|
||||
|
||||
* Support for easily adding extra files to releases via overlays
|
||||
|
||||
* Allow `RELEASE_DISTRIBUTION` to be set to `none` in order to fully disable it
|
||||
|
||||
* Add a built-in `:tar` step that automatically packages releases
|
||||
|
||||
See the full CHANGELOG for more improvements.
|
||||
|
||||
## Improvements to sort-based APIs in Enum
|
||||
|
||||
`Enum.sort/1` in Elixir by default sorts from lowest to highest:
|
||||
|
||||
```elixir
|
||||
Mix.install([:jason])
|
||||
IO.puts Jason.encode!(%{hello: :world})
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"])
|
||||
["apple", "banana", "pineapple"]
|
||||
```
|
||||
|
||||
`Mix.install/2` also performs protocol consolidation, which gives script developers an option to execute their code in the most performant format possible.
|
||||
If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code:
|
||||
|
||||
**Note:** `Mix.install/2` is currently experimental and it may change in future releases.
|
||||
|
||||
Another improvement to scripting is the ability to trap exit signals via `System.trap_signal/3`. All you need is the signal name and a callback that will be invoked when the signal triggers. For example, ExUnit leverages this functionality to print all currently running tests when you abort the test suite via SIGQUIT (`Ctrl+\\ `):
|
||||
|
||||
```
|
||||
$ mix test
|
||||
.......................................................................
|
||||
.....................^\
|
||||
|
||||
Aborting test suite, the following have not completed:
|
||||
|
||||
* test query building [test/ecto/query_test.exs:48]
|
||||
* test placeholders in Repo.insert_all [test/ecto/repo_test.exs:502]
|
||||
|
||||
Showing results so far...
|
||||
|
||||
78 doctests, 1042 tests, 0 failures
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], &>=/2)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
This is particularly useful when your tests get stuck and you want to know which one is the culprit.
|
||||
Furthermore, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result:
|
||||
|
||||
**Important**: Trapping signals may have strong implications on how a system shuts down and behave in production and therefore it is extremely discouraged for libraries to set their own traps. Instead, they should redirect users to configure them themselves. The only cases where it is acceptable for libraries to set their own traps is when using Elixir in script mode, such as in `.exs` files and via Mix tasks.
|
||||
|
||||
## Tighter Erlang/OTP 24 integration
|
||||
|
||||
Erlang/OTP 24 ships with JIT compilation support and Elixir developers don't have to do anything to reap its benefits. There are many other features in Erlang/OTP 24 to look forwards to and Elixir v1.12 provides integration with many of them: such as support for 16bit floats in bitstrings as well as performance improvements in the compiler and during code evaluation.
|
||||
|
||||
Another excellent feature in Erlang/OTP 24 is the implementation of [EEP 54](http://erlang.org/eeps/eep-0054.html), which provides extended error information for many functions in Erlang's stdlib. Elixir v1.12 fully leverages this feature to improve reporting for errors coming from Erlang. For example, in earlier OTP versions, inserting an invalid argument into a ETS table that no longer exists would simply error with `ArgumentError`:
|
||||
|
||||
```
|
||||
Interactive Elixir (1.11.0)
|
||||
iex(1)> ets = :ets.new(:example, [])
|
||||
#Reference<0.3845811859.2669281281.223553>
|
||||
iex(2)> :ets.delete(ets)
|
||||
true
|
||||
iex(3)> :ets.insert(ets, :should_be_a_tuple)
|
||||
** (ArgumentError) argument error
|
||||
(stdlib 3.15) :ets.insert(#Reference<0.3845811859.2669281281.223553>, :should_be_a_tuple)
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]])
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
However, in Elixir v1.12 with Erlang/OTP 24:
|
||||
To perform proper semantic comparison for dates, one would also need to pass a custom sorting function:
|
||||
|
||||
```
|
||||
Interactive Elixir (1.12.0)
|
||||
iex(1)> ets = :ets.new(:example, [])
|
||||
#Reference<0.105641012.1058144260.76455>
|
||||
iex(2)> :ets.delete(ets)
|
||||
true
|
||||
iex(3)> :ets.insert(ets, :should_be_a_tuple)
|
||||
** (ArgumentError) errors were found at the given arguments:
|
||||
|
||||
* 1st argument: the table identifier does not refer to an existing ETS table
|
||||
* 2nd argument: not a tuple
|
||||
|
||||
(stdlib 3.15) :ets.insert(#Reference<0.105641012.1058144260.76455>, :should_be_a_tuple)
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], &(Date.compare(&1, &2) != :lt))
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
```
|
||||
|
||||
Finally, note Rebar v2 no longer works on Erlang/OTP 24+. Mix defaults to Rebar v3 since v1.4, so no changes should be necessary by the huge majority of developers. However, if you are explicitly setting `manager: :rebar` in your dependency, you want to move to Rebar v3 by removing the `:manager` option. Support for unsupported Rebar versions will be removed from Mix in the future.
|
||||
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
|
||||
|
||||
## Stepped ranges
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :asc)
|
||||
["apple", "banana", "pineapple"]
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :desc)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
Elixir has support for ranges from before its v1.0 release. Ranges support only integers and are inclusive, using the mathematic notation `a..b`. Ranges in Elixir are either increasing `1..10` or decreasing `10..1` and the direction of the range was always inferred from the first and last positions. Ranges are always lazy as its values are emitted as they are enumerated rather than being computed upfront.
|
||||
As well as adding the possibility to pass a module to perform semantic comparisons. For example, to sort dates, one now only needs to pass the `Date` module or even `{:desc, Date}` for descending semantical sort:
|
||||
|
||||
Unfortunately, due to this inference, it is not possible to have empty ranges. For example, if you want to create a list of `n` elements, you cannot express it with a range from `1..n`, as `1..0` (for `n=0`) is a decreasing range with two elements.
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], Date)
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], {:desc, Date})
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
Elixir v1.12 supports stepped ranges via the `first..last//step` notation. For example: `1..10//2` will emit the numbers `1`, `3`, `5`, `7`, and `9`. You can consider the `//` operator as an equivalent to "range division", as it effectively divides the number of elements in the range by `step`, rounding up on inexact scenarios. Steps can be either positive (increasing ranges) or negative (decreasing ranges). Stepped ranges bring more expressive power to Elixir ranges and they elegantly solve the empty range problem, as they allow the direction of the steps to be explicitly declared instead of inferred.
|
||||
These API improvements make the code more concise and readable and they have also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
|
||||
|
||||
As of Elixir v1.12, implicitly decreasing ranges are soft-deprecated and warnings will be emitted in future Elixir versions based on our [deprecation policy](https://hexdocs.pm/elixir/compatibility-and-deprecations.html#deprecations).
|
||||
## Tracking of compile-time configuration
|
||||
|
||||
## Additional functions
|
||||
In Elixir, we organize our code in applications. Libraries, your dependencies, and your own project are all separate applications. All applications in Elixir also come with an application environment.
|
||||
|
||||
Elixir v1.12 has the additional of many functions across the standard library. The `Enum` module received additions such as `Enum.count_until/2`, `Enum.product/1`, `Enum.zip_with/2`, and more. The `Integer` module now includes `Integer.pow/2` and `Integer.extended_gcd/2`. Finally, the `Kernel` module got two new functions, `Kernel.then/2` and `Kernel.tap/2`, which are specially useful in `|>` pipelines.
|
||||
The application environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
|
||||
|
||||
## v1.12.2 (2021-07-01)
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
Because `config/releases.exs` is read after the code is compiled, the new value will have no effect as the code was compiled to connect to "db.local".
|
||||
|
||||
Of course, the obvious solution to this mismatch is to not read the application environment at compilation time in the first place, and instead move the code to inside a function:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: db_host())
|
||||
end
|
||||
defp db_host() do
|
||||
Application.get_env(:my_app, :db_host, "db.local")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
While this is the preferred approach, there are still two scenarios we need to address:
|
||||
|
||||
1. Not everyone may be aware of this pitfall, so they will mistakenly read the application environemnt at compile-time, until they are bitten by this behaviour
|
||||
|
||||
2. In rare occasions, you trully need to read the application environment at compile-time, and you want to be warned when you try to configure at runtime something that is valid only at compilation time
|
||||
|
||||
Elixir v1.10 aims to solve these two scenarios by introducing a `Application.compile_env/3` function. For example, to read the value at compile time, you can now do:
|
||||
|
||||
```elixir
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
```
|
||||
|
||||
By using `compile_env/3`, Elixir will store the values used during compilation and compare them with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
|
||||
|
||||
In future versions, we will deprecate the use `Application.get_env` at compile-time with a clear message pointing users to configuration best practices, effectively addressing the scenario where users read from the application environment at compile time unaware of its pitfalls.
|
||||
|
||||
## Compiler tracing
|
||||
|
||||
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
|
||||
|
||||
In previous Elixir versions, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project in order to perform all kinds of checks, such as deprecations and undefined functions.
|
||||
|
||||
Although this database was not public, developers would still use it to run their own checks against their projects. With time, developers would request more data to be included in the database, which was problematic as Elixir itself did not have a use for the additional data, and the database was not meant to be used externally in the first place.
|
||||
|
||||
In Elixir v1.10, we have addressed these problems by introducing compiler tracing. The compiler tracing allows developers to listen to events as they are emitted by the compiler, so they can store all of the information they need - and only the information they need.
|
||||
|
||||
Elixir itself is using the new compiler tracing to provide new functionality. One advantage of this approach is that developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
|
||||
|
||||
@compile {:no_warn_undefined, OptionalDependency}
|
||||
defdelegate my_function_call(arg), to: OptionalDependency
|
||||
|
||||
Previously, this information had to be added to the overall project configuration, which was far away from where the optional call effectively happened.
|
||||
|
||||
## Other enhancements
|
||||
|
||||
Elixir's calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
|
||||
|
||||
There are many improvements related to Elixir's AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information was already available to the Elixir code formatter and has now been made public. We have also extensively documented all of Elixir's AST metadata. These changes alongside compiler tracing means static analyzers and IDE integrations have a better foundation to analyze the source code.
|
||||
|
||||
ExUnit, our test framework, ships two small but important improvements: `ExUnit.CaptureIO` can now be used by tests that run concurrently and we have added "pattern-matching diffing". To understand the last feature, take this code:
|
||||
|
||||
```elixir
|
||||
assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload
|
||||
```
|
||||
|
||||
Now imagine that `json_payload` is a large JSON blob and the `"key"` inside the `"body"` did not have value of `"foo"`. In previous Elixir versions, if the assertion failed, Elixir would print the right side and let you up to your own devices to figure out what went wrong. In Elixir v1.10, we diff the data structure against the pattern so you can see exactly which parts of the data matched the pattern and which ones did not. Note ExUnit already performed diffing when comparing data types, this new version adds diffing when matching data against a pattern.
|
||||
|
||||
## v1.10.4 (2020-07-04)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Ensure deprecated macros emit warnings
|
||||
* [Kernel] Fix a bug where custom types were printed as built-in types
|
||||
* [Kernel] Don't add compile-time dependency on `defdelegate`
|
||||
* [Kernel] Add line numbers to warnings on deprecated imports
|
||||
* [Kernel] Report the correct line number when raising inside a macro
|
||||
* [Task] Include callers in translated Logger metadata for Task
|
||||
* [Task] Fix Task PID and caller in Task Supervisor reports
|
||||
|
||||
#### Mix
|
||||
#### ExUnit
|
||||
|
||||
* [mix deps] Ensure unconstrained rebar deps generate valid mix specifications
|
||||
* [ExUnit.Formatter] Avoid crashes when diffing guards when the pattern does not match
|
||||
* [ExUnit.Formatter] Also blame exceptions that come from linked and trapped exits
|
||||
|
||||
### 2. Enhancements
|
||||
#### IEx
|
||||
|
||||
#### Elixir
|
||||
* [IEx.Helpers] Do not crash when printing a type that cannot be code formatted
|
||||
|
||||
* [elixirc] Change the output of `--profile time` to make it easier to detect outliers
|
||||
* [Application] Do not add compile time deps on args to `Application.compile_env/2` and `Application.compile_env!/2`
|
||||
* [Enum] Optimize `Enum.into/3` and `Map.new/2`
|
||||
#### Mix
|
||||
|
||||
#### Mix
|
||||
* [mix app.start] Fix reading `.app` file located in archives (`.ez` files)
|
||||
* [mix local.hex] Provide more guidance when Hex can't be installed
|
||||
* [mix release] Properly encode config in releases
|
||||
|
||||
* [mix compile] Compile most recently changed files first
|
||||
* [mix compile, mix run, mix test] Speed up the time taken to load dependencies. This should make the usage of Mix inside projects quite more responsive
|
||||
|
||||
## v1.12.1 (2021-05-28)
|
||||
## v1.10.3 (2020-04-25)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Make sure `Code.format_string!/2` formats multiline expression inside interpolation on the first run
|
||||
* [Macro] Revert keeping of underscores between digits in camelize
|
||||
* [Code] Return `[{mod, bin}]` from `Code.compile_file/2`, `Code.require_file/2`, `Code.load_file/2`
|
||||
* [Code] Make sure the formatter respects newlines before and after module attributes
|
||||
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise in long compilation cycles
|
||||
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise if some of the modules being compiled referred to a module that has been loaded directly to memory
|
||||
* [Module] Fix accidental breaking change where bodiless clauses had their body value on `@on_definition` callbacks set to an empty list instead of `nil`
|
||||
* [String] Undeprecate `String.normalize/2` normalize and fix infinite loop caused by certain invalid strings
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on pinned variables
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching variable struct names
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on the binary concat operator (`<>`) and the left side is not a literal string
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on pseudo-vars (`__MODULE__`, `__DIR__`, etc)
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Make sure `Mix.install/2` expand paths for deps
|
||||
* [mix deps.get] Silence false positives on `httpc` warnings
|
||||
* [mix test] Do not run the whole suite when there are no --failed tests as it won't behave as expected inside umbrellas
|
||||
* [mix release] Respect the `:path` option when creating a `:tar` file for releases
|
||||
|
||||
## v1.12.0 (2021-05-19)
|
||||
## v1.10.2 (2020-02-26)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Macro] Fix a bug where `Macro.to_string/1` would emit invalid code for sigils
|
||||
* [Task] Do not crash `async_stream` monitor if it receives spurious DOWN messages
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Fix a bug where the Logger formatter would fail when handling unknown metadata values
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not write files to disk if `--warnings-as-errors` was given and warnings were emitted
|
||||
|
||||
## v1.10.1 (2020-02-10)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Do not emit invalid code when formatting `nil`, `false`, and `true` keys in maps
|
||||
* [Kernel] Ensure `with` clauses properly unpack "implicit guards" (such as matching on the struct name)
|
||||
* [Kernel] Do not warn if commas are used by themselves in `~w`/`~W` sigils
|
||||
* [Kernel] Do not validate the `:line` option in quote (the validation has been moved to v1.11 to give users more time to update their code)
|
||||
* [Module] Ensure the code verifier handles the `:erlang.size/1` guard properly
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Properly handle the `report_cb/2` option from Erlang
|
||||
* [Logger] Fix truncation for multi-byte characters
|
||||
* [Logger] Do not rebroadcast messages from remote nodes as this is now taken care by Erlang's logger
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Ensure `assert_receive` produces valid exception messages in case of errors
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix release] Make sure the install command (Window specific) works on paths with spaces in the name
|
||||
* [mix release] Allow using `remote` and `rpc` commands with `Application.compile_env/3`
|
||||
|
||||
## v1.10.0 (2020-01-27)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx.Engine] Add `c:EEx.Engine.handle_text/3` callback that receives text metadata
|
||||
* [EEx.Engine] Emit warnings for unused "do" expression in EEx
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Add `Code.cursor_context/2` to return the context of a code snippet
|
||||
* [Code] Do not add newlines around interpolation on code formatting. Note this means formatted code that has interpolation after the line length on Elixir v1.12 won't be considered as formatted on earlier Elixir versions
|
||||
* [Code] Do not add brackets when keywords is used in the access syntax
|
||||
* [Calendar] Support basic datetime format in `Calendar.ISO` parsing functions
|
||||
* [Code] Improve evaluation performance on systems running on Erlang/OTP 24+
|
||||
* [Date] Support steps via `Date.range/3`
|
||||
* [DateTime] Add `offset` to `DateTime.to_iso8601/2` (now `to_iso8601/3`)
|
||||
* [Enum] Add `Enum.count_until/2` and `Enum.count_until/3`
|
||||
* [Enum] Add `Enum.product/1`
|
||||
* [Enum] Add `Enum.zip_with/2`, `Enum.zip_with/3`, `Enum.zip_reduce/3`, and `Enum.zip_reduce/4`
|
||||
* [Enum] Add support for functions as the second argument of `Enum.with_index/2`
|
||||
* [Exception] Show `error_info` data for exceptions coming from Erlang
|
||||
* [Float] Add `Float.pow/2`
|
||||
* [Integer] Add `Integer.pow/2` and `Integer.extended_gcd/2`
|
||||
* [IO] Add `IO.stream/0` and `IO.binstream/0` which default to STDIO with line orientation
|
||||
* [List] Add default value for `List.first/1` and `List.last/1`
|
||||
* [Kernel] Add `first..last//step` as support for stepped ranges
|
||||
* [Kernel] Also warn for literal structs on `min/2` and `max/2`
|
||||
* [Kernel] Add `Kernel.tap/2` and `Kernel.then/2`
|
||||
* [Kernel] Do not add runtime dependencies to remotes in typespecs
|
||||
* [Kernel] When there is an unused variable warning and there is a variable with the same name previously defined, suggest the user may have wanted to use the pin operator
|
||||
* [Kernel] Improve error messages on invalid character right after a number
|
||||
* [Kernel] Show removal and deprecated tips from Erlang/OTP
|
||||
* [Macro] Add export dependencies on `Macro.struct!/2`
|
||||
* [Macro] Support `:newline` to customize newlines escaping in `Macro.unescape_string/2`
|
||||
* [Module] Raise on invalid `@dialyzer` attributes
|
||||
* [Module] Add `Module.get_definition/2` and `Module.delete_definition/2`
|
||||
* [Module] Allow `@on_load` to be a private function
|
||||
* [Module] Validate `@dialyzer` related module attributes
|
||||
* [Module] Add `Module.reserved_attributes/0` to list all reserved attributes by the language
|
||||
* [Range] Add `Range.new/3` and `Range.size/1`
|
||||
* [Regex] Add offset option to `Regex.scan/3` and `Regex.run/3`
|
||||
* [Registry] Support `:compression` on `Registry` tables
|
||||
* [Registry] Support `Registry.values/3` for reading values under a given key-pid pair
|
||||
* [Stream] Add `Stream.zip_with/2` and `Stream.zip_with/3`
|
||||
* [String] Add `:turkic` mode option to String case functions
|
||||
* [String] Update to Unicode 13.0
|
||||
* [System] Add `System.trap_signal/3` and `System.untrap_signal/2`
|
||||
* [System] Add `System.shell/2` to invoke a command that is interpreted by the shell
|
||||
* [Tuple] Add `Tuple.sum/1` and `Tuple.product/1`
|
||||
* [URI] Support RFC3986 compliant encoding and decoding of queries via the `:rfc3986` option
|
||||
* [Application] Add `Application.compile_env/3` and `Application.compile_env!/2` for reading values at compilation time and tracking if they accidentally change during runtime
|
||||
* [Calendar] Allow custom calendar representations in calendar sigils
|
||||
* [Calendar] Add `c:Calendar.parse_time/1`, `c:Calendar.parse_date/1`, `c:Calendar.parse_naive_datetime/1` and `c:Calendar.parse_utc_datetime/1` callbacks to calendar behaviour
|
||||
* [CLI] Add support for `NO_COLOR` environment variable
|
||||
* [Code] Add `:token_metadata` and `:literal_encoder` support to `Code.string_to_quoted/2`
|
||||
* [Code] Add compiler tracing to lift events done by the compiler
|
||||
* [Code] Return `{:error, :unavailable}` in `Code.ensure_compiled/1` if module is in a deadlock
|
||||
* [DateTime] Add `DateTime.now!/2` and `DateTime.shift_zone!/3`
|
||||
* [Enum] Speed up getting one random element from enumerables
|
||||
* [Enum] Add `Enum.frequencies/1`, `Enum.frequencies_by/2`, and `Enum.map_intersperse/2`
|
||||
* [Enum] Allow a sorting function on `Enum.min/max/min_by/max_by`
|
||||
* [Enum] Add `asc/desc` and `compare/1` support to `Enum.sort/2`
|
||||
* [Exception] Add version alongside app names in stacktraces
|
||||
* [Function] Add `Function.identity/1`
|
||||
* [Kernel] Add `Kernel.is_struct/1` and `Kernel.is_map_key/2`
|
||||
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
|
||||
* [Kernel] Warn if duplicate key is found in struct declaration
|
||||
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
|
||||
* [Kernel] Allow file, line and context to be dynamically set on `quote`
|
||||
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
|
||||
* [Map] Add `Map.pop!/2`
|
||||
* [MapSet] Optimize multiple operations
|
||||
* [Module] Add `Module.has_attribute?/2`
|
||||
* [Module] Add `@compile {:no_warn_undefined, mfa_or_module}` to turn off undefined function warnings
|
||||
* [NaiveDateTime] Add `NaiveDateTime.local_now/0`
|
||||
* [Record] Warn if duplicate key is found in record declaration
|
||||
* [String] Update to Unicode 12.1
|
||||
* [StringIO] Add `:encoding` option to StringIO and optimize `get_chars` operation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Intercept SIGQUIT (via Ctrl+\\) and show a list of all aborted tests as well as intermediate test results
|
||||
* [ExUnit] Interpolate module attributes in match assertions diffs
|
||||
* [ExUnit] Print how much time is spent on `async` vs `sync` tests
|
||||
* [ExUnit] Improve error messages for doctests
|
||||
* [ExUnit] Compile doctests faster (often by two times)
|
||||
* [ExUnit] Add `ExUnit.async_run/0` and `ExUnit.await_run/1`
|
||||
* [ExUnit.Assertions] Support diffs in pattern matching and in `assert_receive`
|
||||
* [ExUnit.CaptureIO] Supports capturing named devices in asynchronous tests
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Make IEx' parser configurable to allow special commands
|
||||
* [IEx] Show function signature when pressing tab after the opening parens of a function
|
||||
* [IEx] If an IEx expression starts with a binary operator, such as `|>`, automatically pipe in the result of the last expression
|
||||
* [IEx] Warn on circular file imports when loading default `.iex.exs`
|
||||
* [IEx] Allow customization of the continuation prompt on IEx
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Allow `start_options` to be configured on Logger's GenEvent
|
||||
* [Logger] Integrate Elixir's Logger with Erlang/OTP 21+'s logger. This means setting up the logger level in Elixir will automatically change the logger level for Erlang and vice-versa
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add `Mix.install/2` for dynamically installing a list of dependencies
|
||||
* [Mix] Support `:exit_code` option in `Mix.raise/2`
|
||||
* [Mix] Discard `MIX_ENV` and `MIX_TARGET` values if they are empty strings
|
||||
* [Mix] Print the time taken to execute a task with on `MIX_DEBUG=1`
|
||||
* [mix compile.erlang] Compile multiple files in parallel
|
||||
* [mix escript.build] Deep merge configuration and ensure argv is set when executing `config/runtime.exs`
|
||||
* [mix release] Add `RELEASE_PROG` to releases with the name of the executable starting the release
|
||||
* [mix release] Support `remote.vm.args` to customize how the connecting VM boots
|
||||
* [mix test] Run all available tests if there are no pending `--failed` tests. This provides a better workflow as you no longer need to toggle the `--failed` flag between runs
|
||||
* [mix compile] Add `--profile time` flag to profile compilation steps
|
||||
* [mix deps.compile] Add `--skip-umbrella-children` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
|
||||
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
|
||||
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
|
||||
* [mix release] Support overlays in `rel/overlays`
|
||||
* [mix release] Allow configuration reboot to be disabled in releases
|
||||
* [mix test] Add support for simple round-robin test partitioning across multiple machines
|
||||
* [Mix.Project] Add `MIX_DEPS_PATH` environment variable for setting `:deps_path`
|
||||
* [Mix.Project] Add `Mix.Project.deps_scms/1` that returns deps with their SCMs
|
||||
* [Mix.Task] Add `Mix.Task.Compiler.after_compiler/2` callback, to simplify compilers that may need to run something at multiple steps
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Ensure multiline do/end with no spaces compile under trim mode
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [CLI] Ensure `-e ""` (with an empty string) parses correctly on Windows
|
||||
* [Inspect] Do not override user supplied `:limit` option for derived implementations
|
||||
* [Kernel] Allow heredoc inside a heredoc interpolation
|
||||
* [Kernel] Preserve CRLF on heredocs
|
||||
* [Kernel] Public functions without documentation now appear as an empty map on `Code.fetch_docs/1`, unless they start with underscore, where they remain as `:none`. This aligns Elixir's implementation with EEP48
|
||||
* [Kernel] Do not crash when complex literals (binaries and maps) are used in guards
|
||||
* [Kernel] Properly parse keywords (such as `end`) followed by the `::` operator
|
||||
* [Kernel] Do not ignore unimplemented signatures from generated functions
|
||||
* [Kernel] Improve error message when an expression follows a keyword list without brackets
|
||||
* [Macro] `Macro.decompose_call/1` now also consider tuples with more than 2 elements to not be valid calls
|
||||
* [Macro] Fix `Macro.to_string/1` double-escaping of escape characters in sigils
|
||||
* [Macro] Fix `Macro.underscore/1` on digits preceded by capitals: "FOO10" now becomes "foo10" instead of "fo_o10"
|
||||
* [Macro] Preserve underscores between digits on `Macro.underscore/1`
|
||||
* [OptionParser] Properly parse when numbers follow-up aliases, for example, `-ab3` is now parsed as `-a -b 3`
|
||||
* [Path] Fix `Path.relative_to/2` when referencing self
|
||||
* [Path] Do not crash when a volume is given to `Path.absname/1`, such as "c:"
|
||||
* [Task] Ensure `Task.async_stream/2` with `ordered: false` discard results as they are emitted, instead of needlessly accumulating inside the stream manager
|
||||
* [Task] Raise if `:max_concurrency` is set to 0 on streaming operations
|
||||
* [URI] Do not discard empty paths on `URI.merge/2`
|
||||
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
|
||||
* [Kernel] Raise error on functions/guards without implementation
|
||||
* [Kernel] Do not expand expressions inside interpolation twice
|
||||
* [Keyword] Ensure keyword replace and update preserve order
|
||||
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
|
||||
* [Module] Fix `@macrocallback` definitions with a `when` clause
|
||||
* [Path] Fix `Path.absname/1` to correctly handle UNC paths on Windows
|
||||
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
|
||||
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
|
||||
* [URI] Preserve slashes in URIs without authority
|
||||
* [URI] Require a nil or an absolute path on URIs with host or authority
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Make `@tag tmp_dir` an absolute directory, avoiding inconsistencies if the test changes the current working directory
|
||||
* [ExUnit.Diff] Fix cases where the diffing algorithm would fail to print a pattern correct
|
||||
* [ExUnit.Assertions] Fix `assert_receive` and `assert match?` to behave consistently compared `receive` and `match?` when given an invalid macro
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix auto-completion inside remote shells
|
||||
* [IEx] Exit IEx session if the group leader exits
|
||||
* [IEx] Allow `pry` to be used in non-tty terminals
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.config] Do not emit false positive warnings when configured dependencies that have `runtime: false` set
|
||||
* [mix compile.elixir] Ensure that a manifest is generated even with no source code
|
||||
* [mix compile.elixir] Make sure export dependencies trigger recompilation when the dependency is removed as well as when the whole file is removed
|
||||
* [mix compile.elixir] Do not emit false positive warnings when a path dependency adds a module that is then used by the current application in the same `mix compile` cycle
|
||||
* [mix test] Ensure protocols within the current project are consolidated when `--cover` is given
|
||||
* [mix release] Improve compliance of release scripts with stripped down Linux installations
|
||||
* [mix release] Preserve file mode when copying non-beam ebin files
|
||||
* [mix xref] Ensure args are passed to the underlying `mix compile` call
|
||||
* [mix compile] Do not filter out warning for external files from diagnostics
|
||||
* [Mix.Project] Ensure user given `:manager` to dependencies has higher precedence than the SCM one
|
||||
* [Mix.Project] Recompile umbrella children when config files change and `mix compile` is called from the umbrella root
|
||||
* [Mix.Task] Always recompile before running tasks from dependencies
|
||||
* [Mix.Task] Ensure project's Logger config is used when running Mix tasks
|
||||
|
||||
### 3. Soft-deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Using `first..last` to match on ranges is soft-deprecated and will warn on future Elixir versions. Use `first..last//step` instead
|
||||
* [Kernel] Using `first..last` to create decreasing ranges is soft-deprecated and will warn on future versions. Use `first..last//-1` instead
|
||||
|
||||
### 4. Hard-deprecations
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx.Engine] `use EEx.Engine` is deprecated in favor of explicit delegation
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] The binary operator `^^^` is deprecated. If you are using `Bitwise.^^^/2`, use `Bitwise.bxor/2` instead
|
||||
* [Kernel] Deprecate `@foo()` in favor of `@foo`
|
||||
* [System] Deprecate `System.stacktrace/0` (it was already deprecated outside of catch/rescue and now it is deprecated everywhere)
|
||||
* [Code] `compiler_options/0` is deprecated in favor of `compiler_option/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] The `:xref` compiler is deprecated and it has no effect. Please remove it from your mix.exs file.
|
||||
* [Mix.Config] `Mix.Config.persist/1` has been deprecated. Instead of `Mix.Config.persist(config)` use `Application.put_all_env(config, persistent: true)` (`Application.put_all_env/2` was added in v1.9)
|
||||
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
|
||||
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
|
||||
|
||||
## v1.11
|
||||
### 4. Hard-deprecations
|
||||
|
||||
The CHANGELOG for v1.11 releases can be found [in the v1.11 branch](https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md).
|
||||
#### Elixir
|
||||
|
||||
* [Code] `Code.load_file/2` has been deprecated in favor of `Code.require_file/2` or `Code.compile_file/2`
|
||||
* [Code] `Code.loaded_files/0` and `Code.unload_file/1` have been deprecated in favor of `Code.required_files/0` and `Code.unrequire_file/1` respectively
|
||||
* [Code] `Code.ensure_compiled?/1` is deprecated in favor of `Code.ensure_compiled/1`
|
||||
* [String] `String.normalize/2` has been deprecated in favor of `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` which ship as part of Erlang/OTP 20+
|
||||
* [Supervisor] `Supervisor.Spec.supervise/2` has been deprecated in favor of the new Supervisor child specification
|
||||
* [Supervisor] The `:simple_one_for_one` strategy in `Supervisor` has been deprecated in favor of `DynamicSupervisor`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] `:compile_time_purge_level` application environment configuration has been deprecated in favor of the more general `:compile_time_purge_matching` config
|
||||
* [Logger] Deprecate logging non-chardata values
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.xref] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref] `xref` now only tracks dependencies between modules and files, no longer between functions. See "Compilation tracers" to learn more about how to track this information directly
|
||||
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
|
||||
|
||||
## v1.9
|
||||
|
||||
The CHANGELOG for v1.9 releases can be found [in the v1.9 branch](https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md).
|
||||
|
||||
@@ -2,11 +2,9 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := 1.12/
|
||||
CANONICAL ?= master/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||
CANONICAL := v1.10/ # master/ or vMAJOR.MINOR/
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
@@ -28,9 +26,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
#==> 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 >= 22)])' -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 >= 21)])' -s erlang halt | grep -q '^true'; \
|
||||
if [ $$? != 0 ]; then \
|
||||
echo "At least Erlang/OTP 22.0 is required to build Elixir"; \
|
||||
echo "At least Erlang/OTP 21.0 is required to build Elixir"; \
|
||||
exit 1; \
|
||||
fi
|
||||
endef
|
||||
@@ -76,14 +74,14 @@ compile: erlang $(APP) elixir
|
||||
|
||||
erlang: $(PARSER)
|
||||
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && erl -make
|
||||
|
||||
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
||||
|
||||
# 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
|
||||
# then Mix and then compile EEx fully
|
||||
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
|
||||
|
||||
stdlib: $(KERNEL) VERSION
|
||||
@@ -147,12 +145,12 @@ check_reproducible: compile
|
||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) $(MAKE) compile
|
||||
$(Q) echo "Diffing..."
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) echo "Builds are reproducible"
|
||||
|
||||
clean:
|
||||
@@ -180,7 +178,7 @@ clean_residual_files:
|
||||
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_FORMAT = html
|
||||
COMPILE_DOCS = 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" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
COMPILE_DOCS = 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" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
@@ -192,27 +190,27 @@ docs_elixir: compile ../ex_doc/bin/ex_doc
|
||||
docs_eex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (eex)"
|
||||
$(Q) rm -rf doc/eex
|
||||
$(call COMPILE_DOCS,EEx,eex,EEx,--config "lib/mix/docs.exs")
|
||||
$(call COMPILE_DOCS,EEx,eex,EEx)
|
||||
|
||||
docs_mix: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (mix)"
|
||||
$(Q) rm -rf doc/mix
|
||||
$(call COMPILE_DOCS,Mix,mix,Mix,--config "lib/mix/docs.exs")
|
||||
$(call COMPILE_DOCS,Mix,mix,Mix)
|
||||
|
||||
docs_iex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (iex)"
|
||||
$(Q) rm -rf doc/iex
|
||||
$(call COMPILE_DOCS,IEx,iex,IEx,--config "lib/mix/docs.exs")
|
||||
$(call COMPILE_DOCS,IEx,iex,IEx)
|
||||
|
||||
docs_ex_unit: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (ex_unit)"
|
||||
$(Q) rm -rf doc/ex_unit
|
||||
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit,--config "lib/mix/docs.exs")
|
||||
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit)
|
||||
|
||||
docs_logger: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (logger)"
|
||||
$(Q) rm -rf doc/logger
|
||||
$(call COMPILE_DOCS,Logger,logger,Logger,--config "lib/mix/docs.exs")
|
||||
$(call COMPILE_DOCS,Logger,logger,Logger)
|
||||
|
||||
../ex_doc/bin/ex_doc:
|
||||
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||
@@ -296,7 +294,7 @@ PLT = .elixir.plt
|
||||
|
||||
$(PLT):
|
||||
@ echo "==> Building PLT with Elixir's dependencies..."
|
||||
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets crypto runtime_tools ftp tftp mnesia public_key asn1 hipe sasl
|
||||
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets
|
||||
|
||||
clean_plt:
|
||||
$(Q) rm -f $(PLT)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png" width="200" alt="Elixir">
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions?query=branch%3Amaster+workflow%3ACI) [](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||

|
||||
=========
|
||||
[](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
and maintainable applications.
|
||||
@@ -29,7 +29,7 @@ For the many different ways to install Elixir,
|
||||
[see our installation instructions on the website](https://elixir-lang.org/install.html).
|
||||
To compile from source, you can follow the steps below.
|
||||
|
||||
First, [install Erlang](https://elixir-lang.org/install.html#installing-erlang). After that, clone this repository to your machine, compile and test it:
|
||||
First, [install Erlang](https://elixir-lang.org/install.html#installing-erlang). Then clone this repository to your machine, compile and test it:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/elixir-lang/elixir.git
|
||||
@@ -48,10 +48,10 @@ If Elixir fails to build (specifically when pulling in a new version via
|
||||
If tests pass, you can use Interactive Elixir by running `bin/iex` in your terminal.
|
||||
|
||||
However, if tests fail, it is likely that you have an outdated Erlang/OTP version
|
||||
(Elixir requires Erlang/OTP 22.0 or later). You can check your Erlang/OTP version
|
||||
(Elixir requires Erlang/OTP 21.0 or later). You can check your Erlang/OTP version
|
||||
by calling `erl` in the command line. You will see some information similar to:
|
||||
|
||||
Erlang/OTP 22 [erts-9.0] [smp:2:2] [async-threads:10] [kernel-poll:false]
|
||||
Erlang/OTP 21 [erts-9.0] [smp:2:2] [async-threads:10] [kernel-poll:false]
|
||||
|
||||
If you have properly set up your dependencies and tests still fail,
|
||||
you may want to open up a bug report, as explained next.
|
||||
@@ -131,9 +131,9 @@ With tests running and passing, you are ready to contribute to Elixir and
|
||||
We have saved some excellent pull requests we have received in the past in
|
||||
case you are looking for some examples:
|
||||
|
||||
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
* [Implement Enum.member? - Pull Request](https://github.com/elixir-lang/elixir/pull/992)
|
||||
* [Add String.valid? - Pull Request](https://github.com/elixir-lang/elixir/pull/1058)
|
||||
* [Implement capture_io for ExUnit - Pull Request](https://github.com/elixir-lang/elixir/pull/1059)
|
||||
|
||||
### Reviewing changes
|
||||
|
||||
|
||||
+2
-3
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
| Elixir version | Support
|
||||
| -------------- | ------------------------------
|
||||
| 1.12 | Bug fixes and security patches
|
||||
| 1.11 | Security patches only
|
||||
| 1.10 | Security patches only
|
||||
| 1.10 | Bug fixes and security patches
|
||||
| 1.9 | Security patches only
|
||||
| 1.8 | Security patches only
|
||||
| 1.7 | Security patches only
|
||||
| 1.6 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+14
-14
@@ -70,7 +70,7 @@ readlink_f () {
|
||||
ERL=""
|
||||
|
||||
# Stores erl arguments preserving spaces/quotes (mimics an array)
|
||||
erl_set () {
|
||||
erl () {
|
||||
eval "E${E}=\$1"
|
||||
E=$((E + 1))
|
||||
}
|
||||
@@ -137,34 +137,34 @@ while [ $I -le $LENGTH ]; do
|
||||
;;
|
||||
--cookie)
|
||||
S=2
|
||||
erl_set "-setcookie"
|
||||
erl_set "$2"
|
||||
erl "-setcookie"
|
||||
erl "$2"
|
||||
;;
|
||||
--sname|--name)
|
||||
S=2
|
||||
erl_set "$(echo "$1" | cut -c 2-)"
|
||||
erl_set "$2"
|
||||
erl "$(echo "$1" | cut -c 2-)"
|
||||
erl "$2"
|
||||
;;
|
||||
--erl-config)
|
||||
S=2
|
||||
erl_set "-config"
|
||||
erl_set "$2"
|
||||
erl "-config"
|
||||
erl "$2"
|
||||
;;
|
||||
--vm-args)
|
||||
S=2
|
||||
erl_set "-args_file"
|
||||
erl_set "$2"
|
||||
erl "-args_file"
|
||||
erl "$2"
|
||||
;;
|
||||
--boot)
|
||||
S=2
|
||||
erl_set "-boot"
|
||||
erl_set "$2"
|
||||
erl "-boot"
|
||||
erl "$2"
|
||||
;;
|
||||
--boot-var)
|
||||
S=3
|
||||
erl_set "-boot_var"
|
||||
erl_set "$2"
|
||||
erl_set "$3"
|
||||
erl "-boot_var"
|
||||
erl "$2"
|
||||
erl "$3"
|
||||
;;
|
||||
--pipe-to)
|
||||
S=3
|
||||
|
||||
@@ -99,21 +99,18 @@ if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && set runMode="elixi
|
||||
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
|
||||
@@ -155,10 +152,6 @@ for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
|
||||
)
|
||||
|
||||
:run
|
||||
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
|
||||
if %errorlevel% == 0 (
|
||||
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
|
||||
)
|
||||
if not !runMode! == "iex" (
|
||||
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ Usage: $(basename "$0") [elixir switches] [compiler switches] [.ex files]
|
||||
--ignore-module-conflict Does not emit warnings if a module was previously defined
|
||||
--no-debug-info Does not attach debug info to compiled modules
|
||||
--no-docs Does not attach documentation to compiled modules
|
||||
--profile time Profile the time to compile modules
|
||||
--verbose Prints compilation status
|
||||
--warnings-as-errors Treats warnings as errors and return non-zero exit code
|
||||
|
||||
|
||||
+2
-3
@@ -14,15 +14,14 @@ goto run
|
||||
:documentation
|
||||
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
|
||||
echo.
|
||||
echo -h, --help Prints this message and exits
|
||||
echo -o The directory to output compiled files
|
||||
echo -v, --version Prints Elixir version and exits
|
||||
echo.
|
||||
echo --help, -h Prints this message and exits
|
||||
echo --ignore-module-conflict Does not emit warnings if a module was previously defined
|
||||
echo --no-debug-info Does not attach debug info to compiled modules
|
||||
echo --no-docs Does not attach documentation to compiled modules
|
||||
echo --profile time Profile the time to compile modules
|
||||
echo --verbose Prints compilation status
|
||||
echo --version, -v Prints Elixir version and exits
|
||||
echo --warnings-as-errors Treats warnings as errors and returns non-zero exit code
|
||||
echo.
|
||||
echo ** Options given after -- are passed down to the executed code
|
||||
|
||||
+32
-81
@@ -1,9 +1,9 @@
|
||||
defmodule EEx.SyntaxError do
|
||||
defexception [:message, :file, :line, :column]
|
||||
defexception [:message, :file, :line]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
|
||||
"#{exception.file}:#{exception.line}: #{exception.message}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,19 +17,19 @@ defmodule EEx do
|
||||
|
||||
## API
|
||||
|
||||
This module provides three main APIs for you to use:
|
||||
This module provides 3 main APIs for you to use:
|
||||
|
||||
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
|
||||
1. Evaluate a string (`eval_string`) or a file (`eval_file`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated at runtime and not precompiled.
|
||||
slowest, since the code is evaluated and not compiled before.
|
||||
|
||||
2. Define a function from a string (`function_from_string/5`)
|
||||
or a file (`function_from_file/5`). This allows you to embed
|
||||
2. Define a function from a string (`function_from_string`)
|
||||
or a file (`function_from_file`). This allows you to embed
|
||||
the template as a function inside a module which will then
|
||||
be compiled. This is the preferred API if you have access
|
||||
to the template at compilation time.
|
||||
|
||||
3. Compile a string (`compile_string/2`) or a file (`compile_file/2`)
|
||||
3. Compile a string (`compile_string`) or a file (`compile_file`)
|
||||
into Elixir syntax tree. This is the API used by both functions
|
||||
above and is available to you if you want to provide your own
|
||||
ways of handling the compiled template.
|
||||
@@ -39,15 +39,12 @@ defmodule EEx do
|
||||
All functions in this module accept EEx-related options.
|
||||
They are:
|
||||
|
||||
* `:line` - the line to be used as the template start. Defaults to 1.
|
||||
* `:file` - the file to be used in the template. Defaults to the given
|
||||
file the template is read from or to `"nofile"` when compiling from a string.
|
||||
* `:line` - the line to be used as the template start. Defaults to `1`.
|
||||
* `:indentation` - (since v1.11.0) an integer added to the column after every
|
||||
new line. Defaults to `0`.
|
||||
file the template is read from or to "nofile" when compiling from a string.
|
||||
* `:engine` - the EEx engine to be used for compilation.
|
||||
* `:trim` - if `true`, trims whitespace left and right of quotation as
|
||||
long as at least one newline is present. All subsequent newlines and
|
||||
spaces are removed but one newline is retained. Defaults to `false`.
|
||||
* `:trim` - trims whitespace left/right of quotation tags. If a quotation
|
||||
tag appears on its own in a given line, line endings are also removed.
|
||||
|
||||
## Engine
|
||||
|
||||
@@ -70,7 +67,7 @@ defmodule EEx do
|
||||
**must** use the equals sign (`=`). Since everything in
|
||||
Elixir is an expression, there are no exceptions for this rule.
|
||||
For example, while some template languages would special-case
|
||||
`if` clauses, they are treated the same in EEx and
|
||||
`if/2` clauses, they are treated the same in EEx and
|
||||
also require `=` in order to have their result printed:
|
||||
|
||||
<%= if true do %>
|
||||
@@ -85,7 +82,7 @@ defmodule EEx do
|
||||
|
||||
will be rendered as `<%= x + 3 %>`.
|
||||
|
||||
Note that different engines may have different rules
|
||||
Notice that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
### Macros
|
||||
@@ -106,13 +103,10 @@ defmodule EEx do
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the given string.
|
||||
Generates a function definition from the string.
|
||||
|
||||
The first argument is the kind of the generated function (`:def` or `:defp`).
|
||||
The `name` argument is the name that the generated function will have.
|
||||
`template` is the string containing the EEx template. `args` is a list of arguments
|
||||
that the generated function will accept. They will be available inside the EEx
|
||||
template. `options` is a list of EEx compilation options (see the module documentation).
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -124,11 +118,11 @@ defmodule EEx do
|
||||
"3"
|
||||
|
||||
"""
|
||||
defmacro function_from_string(kind, name, template, args \\ [], options \\ []) do
|
||||
defmacro function_from_string(kind, name, source, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding() do
|
||||
info = Keyword.merge([file: __ENV__.file, line: __ENV__.line], options)
|
||||
args = Enum.map(args, fn arg -> {arg, [line: info[:line]], nil} end)
|
||||
compiled = EEx.compile_string(template, info)
|
||||
compiled = EEx.compile_string(source, info)
|
||||
|
||||
case kind do
|
||||
:def -> def unquote(name)(unquote_splicing(args)), do: unquote(compiled)
|
||||
@@ -140,11 +134,8 @@ defmodule EEx do
|
||||
@doc """
|
||||
Generates a function definition from the file contents.
|
||||
|
||||
The first argument is the kind of the generated function (`:def` or `:defp`).
|
||||
The `name` argument is the name that the generated function will have.
|
||||
`file` is the path to the EEx template file. `args` is a list of arguments
|
||||
that the generated function will accept. They will be available inside the EEx
|
||||
template. `options` is a list of EEx compilation options (see the module documentation).
|
||||
The kind (`:def` or `:defp`) must be given, the
|
||||
function name, its arguments and the compilation options.
|
||||
|
||||
This function is useful in case you have templates but
|
||||
you want to precompile inside a module for speed.
|
||||
@@ -167,7 +158,7 @@ defmodule EEx do
|
||||
"""
|
||||
defmacro function_from_file(kind, name, file, args \\ [], options \\ []) do
|
||||
quote bind_quoted: binding() do
|
||||
info = Keyword.merge([file: IO.chardata_to_string(file), line: 1], options)
|
||||
info = Keyword.merge(options, file: file, line: 1)
|
||||
args = Enum.map(args, fn arg -> {arg, [line: 1], nil} end)
|
||||
compiled = EEx.compile_file(file, info)
|
||||
|
||||
@@ -181,25 +172,8 @@ defmodule EEx do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a string `source` and generates a quoted expression
|
||||
Gets a string `source` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
|
||||
This is useful if you want to compile a EEx template into code and inject
|
||||
that code somewhere or evaluate it at runtime.
|
||||
|
||||
The generated quoted code will use variables defined in the template that
|
||||
will be taken from the context where the code is evaluated. If you
|
||||
have a template such as `<%= a + b %>`, then the returned quoted code
|
||||
will use the `a` and `b` variables in the context where it's evaluated. See
|
||||
examples below.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> quoted = EEx.compile_string("<%= a + b %>")
|
||||
iex> {result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
|
||||
iex> result
|
||||
"3"
|
||||
|
||||
"""
|
||||
@spec compile_string(String.t(), keyword) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
@@ -207,34 +181,12 @@ defmodule EEx do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a `filename` and generates a quoted expression
|
||||
Gets a `filename` and generate a quoted expression
|
||||
that can be evaluated by Elixir or compiled to a function.
|
||||
|
||||
This is useful if you want to compile a EEx template into code and inject
|
||||
that code somewhere or evaluate it at runtime.
|
||||
|
||||
The generated quoted code will use variables defined in the template that
|
||||
will be taken from the context where the code is evaluated. If you
|
||||
have a template such as `<%= a + b %>`, then the returned quoted code
|
||||
will use the `a` and `b` variables in the context where it's evaluated. See
|
||||
examples below.
|
||||
|
||||
## Examples
|
||||
|
||||
# sample.eex
|
||||
<%= a + b %>
|
||||
|
||||
# In code:
|
||||
quoted = EEx.compile_file("sample.eex")
|
||||
{result, _bindings} = Code.eval_quoted(quoted, a: 1, b: 2)
|
||||
result
|
||||
#=> "3"
|
||||
|
||||
"""
|
||||
@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)
|
||||
@spec compile_file(String.t(), keyword) :: Macro.t()
|
||||
def compile_file(filename, options \\ []) when is_binary(filename) and is_list(options) do
|
||||
options = Keyword.merge(options, file: filename, line: 1)
|
||||
compile_string(File.read!(filename), options)
|
||||
end
|
||||
|
||||
@@ -247,7 +199,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_string(String.t(), keyword, keyword) :: any
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -262,16 +214,15 @@ defmodule EEx do
|
||||
# sample.eex
|
||||
foo <%= bar %>
|
||||
|
||||
# IEx
|
||||
# iex
|
||||
EEx.eval_file("sample.eex", bar: "baz")
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_file(String.t(), keyword, keyword) :: any
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
options = Keyword.put_new(options, :file, filename)
|
||||
when is_binary(filename) and is_list(bindings) and is_list(options) do
|
||||
options = Keyword.put(options, :file, filename)
|
||||
compiled = compile_file(filename, options)
|
||||
do_eval(compiled, bindings, options)
|
||||
end
|
||||
|
||||
+27
-74
@@ -13,67 +13,41 @@ defmodule EEx.Compiler do
|
||||
def compile(source, opts) when is_binary(source) and is_list(opts) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
column = 1
|
||||
indentation = opts[:indentation] || 0
|
||||
trim = opts[:trim] || false
|
||||
tokenizer_options = %{trim: trim, indentation: indentation}
|
||||
|
||||
case EEx.Tokenizer.tokenize(source, line, column, tokenizer_options) do
|
||||
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
|
||||
{:ok, tokens} ->
|
||||
state = %{
|
||||
engine: opts[:engine] || @default_engine,
|
||||
file: file,
|
||||
line: line,
|
||||
quoted: [],
|
||||
start_line: nil,
|
||||
start_column: nil,
|
||||
parser_options: Code.get_compiler_option(:parser_options)
|
||||
start_line: nil
|
||||
}
|
||||
|
||||
init = state.engine.init(opts)
|
||||
generate_buffer(tokens, init, [], state)
|
||||
|
||||
{:error, line, column, message} ->
|
||||
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
|
||||
{:error, line, message} ->
|
||||
raise EEx.SyntaxError, line: line, file: file, message: message
|
||||
end
|
||||
end
|
||||
|
||||
# Generates the buffers by handling each expression from the tokenizer.
|
||||
# It returns Macro.t/0 or it raises.
|
||||
|
||||
defp generate_buffer([{:text, line, column, chars} | rest], buffer, scope, state) do
|
||||
buffer =
|
||||
if function_exported?(state.engine, :handle_text, 3) do
|
||||
meta = [line: line, column: column]
|
||||
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
|
||||
else
|
||||
# TODO: Remove this branch on Elixir v2.0
|
||||
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
end
|
||||
|
||||
defp generate_buffer([{:text, chars} | rest], buffer, scope, state) do
|
||||
buffer = state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:expr, line, column, mark, chars} | rest], buffer, scope, state) do
|
||||
options = [file: state.file, line: line, column: column(column, mark)] ++ state.parser_options
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
defp generate_buffer([{:expr, line, mark, chars, _} | rest], buffer, scope, state) do
|
||||
expr = Code.string_to_quoted!(chars, line: line, file: state.file)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:start_expr, start_line, start_column, mark, chars} | rest],
|
||||
buffer,
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
if mark != '=' do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn(start_line, state.file, message)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:start_expr, start_line, mark, chars, _} | rest], buffer, scope, state) do
|
||||
{contents, line, rest} = look_ahead_middle(rest, start_line, chars)
|
||||
|
||||
{contents, rest} =
|
||||
@@ -81,13 +55,7 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[contents | scope],
|
||||
%{
|
||||
state
|
||||
| quoted: [],
|
||||
line: line,
|
||||
start_line: start_line,
|
||||
start_column: column(start_column, mark)
|
||||
}
|
||||
%{state | quoted: [], line: line, start_line: start_line}
|
||||
)
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
@@ -95,7 +63,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:middle_expr, line, _column, '', chars} | rest],
|
||||
[{:middle_expr, line, '', chars, _} | rest],
|
||||
buffer,
|
||||
[current | scope],
|
||||
state
|
||||
@@ -106,7 +74,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:middle_expr, line, column, modifier, chars} | t],
|
||||
[{:middle_expr, line, modifier, chars, trimmed?} | t],
|
||||
buffer,
|
||||
[_ | _] = scope,
|
||||
state
|
||||
@@ -116,35 +84,27 @@ defmodule EEx.Compiler do
|
||||
"please remove \"#{modifier}\" accordingly"
|
||||
|
||||
:elixir_errors.erl_warn(line, state.file, message)
|
||||
generate_buffer([{:middle_expr, line, column, '', chars} | t], buffer, scope, state)
|
||||
generate_buffer([{:middle_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
|
||||
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line
|
||||
end
|
||||
|
||||
defp generate_buffer([{:middle_expr, line, column, _, chars} | _], _buffer, [], state) do
|
||||
defp generate_buffer([{:middle_expr, line, _, chars, _} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected middle of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: line,
|
||||
column: column
|
||||
line: line
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:end_expr, line, _column, '', chars} | rest],
|
||||
buffer,
|
||||
[current | _],
|
||||
state
|
||||
) do
|
||||
defp generate_buffer([{:end_expr, line, '', chars, _} | rest], buffer, [current | _], state) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
column = state.start_column
|
||||
options = [file: state.file, line: state.start_line, column: column] ++ state.parser_options
|
||||
tuples = Code.string_to_quoted!(wrapped, options)
|
||||
tuples = Code.string_to_quoted!(wrapped, line: state.start_line, file: state.file)
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, rest}
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:end_expr, line, column, modifier, chars} | t],
|
||||
[{:end_expr, line, modifier, chars, trimmed?} | t],
|
||||
buffer,
|
||||
[_ | _] = scope,
|
||||
state
|
||||
@@ -154,29 +114,27 @@ defmodule EEx.Compiler do
|
||||
"expression \"<%#{modifier}#{chars}%>\", please remove \"#{modifier}\" accordingly"
|
||||
|
||||
:elixir_errors.erl_warn(line, state.file, message)
|
||||
generate_buffer([{:end_expr, line, column, '', chars} | t], buffer, scope, state)
|
||||
generate_buffer([{:end_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
|
||||
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line, column: column
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, column, _, chars} | _], _buffer, [], state) do
|
||||
defp generate_buffer([{:end_expr, line, _, chars, _} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: line,
|
||||
column: column
|
||||
line: line
|
||||
end
|
||||
|
||||
defp generate_buffer([{:eof, _, _}], buffer, [], state) do
|
||||
defp generate_buffer([], buffer, [], state) do
|
||||
state.engine.handle_body(buffer)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:eof, line, column}], _buffer, _scope, state) do
|
||||
defp generate_buffer([], _buffer, _scope, state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of string, expected a closing '<% end %>'",
|
||||
file: state.file,
|
||||
line: line,
|
||||
column: column
|
||||
line: state.line
|
||||
end
|
||||
|
||||
# Creates a placeholder and wrap it inside the expression block
|
||||
@@ -194,7 +152,7 @@ defmodule EEx.Compiler do
|
||||
# Look middle expressions that immediately follow a start_expr
|
||||
|
||||
defp look_ahead_middle(
|
||||
[{:text, _, _, text}, {:middle_expr, line, _, _, chars} | rest] = tokens,
|
||||
[{:text, text}, {:middle_expr, line, _, chars, _} | rest] = tokens,
|
||||
start,
|
||||
contents
|
||||
) do
|
||||
@@ -205,7 +163,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp look_ahead_middle([{:middle_expr, line, _column, _, chars} | rest], _start, contents) do
|
||||
defp look_ahead_middle([{:middle_expr, line, _, chars, _} | rest], _start, contents) do
|
||||
{contents ++ chars, line, rest}
|
||||
end
|
||||
|
||||
@@ -239,9 +197,4 @@ defmodule EEx.Compiler do
|
||||
defp insert_quoted(other, _quoted) do
|
||||
other
|
||||
end
|
||||
|
||||
defp column(column, mark) do
|
||||
# length('<%') == 2
|
||||
column + 2 + length(mark)
|
||||
end
|
||||
end
|
||||
|
||||
+13
-15
@@ -4,8 +4,10 @@ defmodule EEx.Engine do
|
||||
|
||||
An engine needs to implement all callbacks below.
|
||||
|
||||
This module also ships with a default engine implementation
|
||||
you can delegate to. See `EEx.SmartEngine` as an example.
|
||||
An engine may also `use EEx.Engine` to get the default behaviour
|
||||
but this is not advised. In such cases, if any of the callbacks
|
||||
are overridden, they must call `super()` to delegate to the
|
||||
underlying `EEx.Engine`.
|
||||
"""
|
||||
|
||||
@type state :: term
|
||||
@@ -29,8 +31,7 @@ defmodule EEx.Engine do
|
||||
|
||||
It must return the updated state.
|
||||
"""
|
||||
@callback handle_text(state, [line: pos_integer, column: pos_integer], text :: String.t()) ::
|
||||
state
|
||||
@callback handle_text(state, text :: String.t()) :: state
|
||||
|
||||
@doc """
|
||||
Called for the dynamic/code parts of a template.
|
||||
@@ -68,7 +69,6 @@ defmodule EEx.Engine do
|
||||
@callback handle_end(state) :: Macro.t()
|
||||
|
||||
@doc false
|
||||
@deprecated "Use explicit delegation to EEx.Engine instead"
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
@behaviour EEx.Engine
|
||||
@@ -90,7 +90,7 @@ defmodule EEx.Engine do
|
||||
end
|
||||
|
||||
def handle_text(state, text) do
|
||||
EEx.Engine.handle_text(state, [], text)
|
||||
EEx.Engine.handle_text(state, text)
|
||||
end
|
||||
|
||||
def handle_expr(state, marker, expr) do
|
||||
@@ -147,7 +147,7 @@ defmodule EEx.Engine do
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:init/1`."
|
||||
@doc false
|
||||
def init(_opts) do
|
||||
%{
|
||||
binary: [],
|
||||
@@ -156,18 +156,18 @@ defmodule EEx.Engine do
|
||||
}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_begin/1`."
|
||||
@doc false
|
||||
def handle_begin(state) do
|
||||
check_state!(state)
|
||||
%{state | binary: [], dynamic: []}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_end/1`."
|
||||
@doc false
|
||||
def handle_end(quoted) do
|
||||
handle_body(quoted)
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_body/1`."
|
||||
@doc false
|
||||
def handle_body(state) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic} = state
|
||||
@@ -176,16 +176,14 @@ defmodule EEx.Engine do
|
||||
{:__block__, [], Enum.reverse(dynamic)}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_text/3`."
|
||||
def handle_text(state, _meta, text) do
|
||||
check_state!(state)
|
||||
@doc false
|
||||
def handle_text(state, text) do
|
||||
%{binary: binary} = state
|
||||
%{state | binary: [text | binary]}
|
||||
end
|
||||
|
||||
@doc "Default implementation for `c:handle_expr/3`."
|
||||
@doc false
|
||||
def handle_expr(state, "=", ast) do
|
||||
check_state!(state)
|
||||
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
|
||||
var = Macro.var(:"arg#{vars_count}", __MODULE__)
|
||||
|
||||
|
||||
@@ -33,26 +33,10 @@ defmodule EEx.SmartEngine do
|
||||
|
||||
"""
|
||||
|
||||
@behaviour EEx.Engine
|
||||
use EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate init(opts), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_body(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_begin(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_end(state), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
defdelegate handle_text(state, meta, text), to: EEx.Engine
|
||||
|
||||
@impl true
|
||||
def handle_expr(state, marker, expr) do
|
||||
def handle_expr(buffer, mark, expr) do
|
||||
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
|
||||
EEx.Engine.handle_expr(state, marker, expr)
|
||||
super(buffer, mark, expr)
|
||||
end
|
||||
end
|
||||
|
||||
+156
-144
@@ -3,98 +3,83 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
@type content :: IO.chardata()
|
||||
@type line :: non_neg_integer
|
||||
@type column :: non_neg_integer
|
||||
@type marker :: '=' | '/' | '|' | ''
|
||||
@type trimmed? :: boolean
|
||||
@type token ::
|
||||
{:text, line, column, content}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, line, column, marker, content}
|
||||
| {:eof, line, column}
|
||||
{:text, content}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, line, marker, content, trimmed?}
|
||||
|
||||
@spaces [?\s, ?\t]
|
||||
@closing_brackets ')]}'
|
||||
|
||||
@doc """
|
||||
Tokenizes the given charlist or binary.
|
||||
|
||||
It returns {:ok, list} with the following tokens:
|
||||
|
||||
* `{:text, line, column, content}`
|
||||
* `{:expr, line, column, marker, content}`
|
||||
* `{:start_expr, line, column, marker, content}`
|
||||
* `{:middle_expr, line, column, marker, content}`
|
||||
* `{:end_expr, line, column, marker, content}`
|
||||
* `{:eof, line, column}`
|
||||
* `{:text, content}`
|
||||
* `{:expr, line, marker, content, trimmed?}`
|
||||
* `{:start_expr, line, marker, content, trimmed?}`
|
||||
* `{:middle_expr, line, marker, content, trimmed?}`
|
||||
* `{:end_expr, line, marker, content, trimmed?}`
|
||||
|
||||
Or `{:error, line, column, message}` in case of errors.
|
||||
Or `{:error, line, error}` in case of errors.
|
||||
"""
|
||||
@spec tokenize(binary | charlist, line, column, map) ::
|
||||
{:ok, [token]} | {:error, line, column, String.t()}
|
||||
@spec tokenize(binary | charlist, line, keyword) :: {:ok, [token]} | {:error, line, String.t()}
|
||||
def tokenize(bin, line, opts \\ [])
|
||||
|
||||
def tokenize(bin, line, column, opts) when is_binary(bin) do
|
||||
tokenize(String.to_charlist(bin), line, column, opts)
|
||||
def tokenize(bin, line, opts)
|
||||
when is_binary(bin) and is_integer(line) and line >= 0 and is_list(opts) do
|
||||
tokenize(String.to_charlist(bin), line, opts)
|
||||
end
|
||||
|
||||
def tokenize(list, line, column, opts)
|
||||
when is_list(list) and is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
|
||||
column = opts.indentation + column
|
||||
|
||||
{list, line, column} =
|
||||
(opts.trim && trim_init(list, line, column, opts)) || {list, line, column}
|
||||
|
||||
tokenize(list, line, column, opts, [{line, column}], [])
|
||||
def tokenize(list, line, opts)
|
||||
when is_list(list) and is_integer(line) and line >= 0 and is_list(opts) do
|
||||
tokenize(list, line, opts, [], [])
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, column, opts, buffer, acc) do
|
||||
tokenize(t, line, column + 3, opts, [?%, ?< | buffer], acc)
|
||||
defp tokenize('<%%' ++ t, line, opts, buffer, acc) do
|
||||
tokenize(t, line, opts, [?%, ?< | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize('<%#' ++ t, line, column, opts, buffer, acc) do
|
||||
case expr(t, line, column + 3, opts, []) do
|
||||
{:error, _, _, _} = error ->
|
||||
defp tokenize('<%#' ++ t, line, opts, buffer, acc) do
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
{:ok, _, new_line, new_column, rest} ->
|
||||
{rest, new_line, new_column, buffer} =
|
||||
trim_if_needed(rest, new_line, new_column, opts, buffer)
|
||||
|
||||
acc = tokenize_text(buffer, acc)
|
||||
tokenize(rest, new_line, new_column, opts, [{new_line, new_column}], acc)
|
||||
{:ok, _, new_line, rest} ->
|
||||
{_, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
|
||||
tokenize(rest, new_line, opts, buffer, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('<%' ++ t, line, column, opts, buffer, acc) do
|
||||
defp tokenize('<%' ++ t, line, opts, buffer, acc) do
|
||||
{marker, t} = retrieve_marker(t)
|
||||
|
||||
case expr(t, line, column + 2 + length(marker), opts, []) do
|
||||
{:error, _, _, _} = error ->
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, tokens} -> token_key(tokens, expr)
|
||||
{:error, _, _, _} -> {:expr, expr}
|
||||
end
|
||||
|
||||
{rest, new_line, new_column, buffer} =
|
||||
trim_if_needed(rest, new_line, new_column, opts, buffer)
|
||||
|
||||
{:ok, expr, new_line, rest} ->
|
||||
token = token_name(expr)
|
||||
{trimmed?, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
|
||||
expr = pad_if_needed(token, expr, trimmed?)
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {key, line, column, marker, expr}
|
||||
tokenize(rest, new_line, new_column, opts, [{new_line, new_column}], [final | acc])
|
||||
final = {token, line, marker, Enum.reverse(expr), trimmed?}
|
||||
tokenize(rest, new_line, opts, [], [final | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, line, _column, opts, buffer, acc) do
|
||||
tokenize(t, line + 1, opts.indentation + 1, opts, [?\n | buffer], acc)
|
||||
defp tokenize('\n' ++ t, line, opts, buffer, acc) do
|
||||
tokenize(t, line + 1, opts, [?\n | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([h | t], line, column, opts, buffer, acc) do
|
||||
tokenize(t, line, column + 1, opts, [h | buffer], acc)
|
||||
defp tokenize([h | t], line, opts, buffer, acc) do
|
||||
tokenize(t, line, opts, [h | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([], line, column, _opts, buffer, acc) do
|
||||
eof = {:eof, line, column}
|
||||
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
|
||||
defp tokenize([], _line, _opts, buffer, acc) do
|
||||
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
@@ -109,126 +94,153 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
# Tokenize an expression until we find %>
|
||||
|
||||
defp expr([?%, ?> | t], line, column, _opts, buffer) do
|
||||
{:ok, Enum.reverse(buffer), line, column + 2, t}
|
||||
defp expr([?%, ?> | t], line, buffer) do
|
||||
{:ok, buffer, line, t}
|
||||
end
|
||||
|
||||
defp expr('\n' ++ t, line, _column, opts, buffer) do
|
||||
expr(t, line + 1, opts.indentation + 1, opts, [?\n | buffer])
|
||||
defp expr('\n' ++ t, line, buffer) do
|
||||
expr(t, line + 1, [?\n | buffer])
|
||||
end
|
||||
|
||||
defp expr([h | t], line, column, opts, buffer) do
|
||||
expr(t, line, column + 1, opts, [h | buffer])
|
||||
defp expr([h | t], line, buffer) do
|
||||
expr(t, line, [h | buffer])
|
||||
end
|
||||
|
||||
defp expr([], line, column, _opts, _buffer) do
|
||||
{:error, line, column, "missing token '%>'"}
|
||||
defp expr([], line, _buffer) do
|
||||
{:error, line, "missing token '%>'"}
|
||||
end
|
||||
|
||||
# Receives tokens and check if it is a start, middle or an end token.
|
||||
defp token_key(tokens, expr) do
|
||||
case {tokens, Enum.reverse(tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
# Receive an expression content and check
|
||||
# if it is a start, middle or an end token.
|
||||
#
|
||||
# Start tokens finish with "do" and "fn ->"
|
||||
# Middle tokens are marked with "->" or keywords
|
||||
# End tokens contain only the end word and optionally
|
||||
# combinations of ")", "]" and "}".
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
defp token_name([h | t]) when h in @spaces do
|
||||
token_name(t)
|
||||
end
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
else
|
||||
{:middle_expr, expr}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
end
|
||||
defp token_name('od' ++ [h | rest]) when h in @spaces or h in @closing_brackets do
|
||||
case tokenize_rest(rest) do
|
||||
{:ok, [{:end, _} | _]} -> :middle_expr
|
||||
_ -> :start_expr
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_append_space([?\s]), do: [?\s]
|
||||
defp maybe_append_space([h]), do: [h, ?\s]
|
||||
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
|
||||
defp token_name('>-' ++ rest) do
|
||||
case tokenize_rest(rest) do
|
||||
{:ok, [{:end, _} | _]} ->
|
||||
:middle_expr
|
||||
|
||||
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
|
||||
defp closing_bracket?(_), do: false
|
||||
# Check if there is a "fn" token and, if so, it is not
|
||||
# followed by an "end" token. If this is the case, we
|
||||
# are on a start expr.
|
||||
{:ok, tokens} ->
|
||||
tokens = Enum.reverse(tokens)
|
||||
fn_index = fn_index(tokens)
|
||||
|
||||
if fn_index && end_index(tokens) > fn_index do
|
||||
:start_expr
|
||||
else
|
||||
:middle_expr
|
||||
end
|
||||
|
||||
_error ->
|
||||
:middle_expr
|
||||
end
|
||||
end
|
||||
|
||||
defp token_name('esle' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('retfa' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('hctac' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
|
||||
|
||||
defp token_name(rest) do
|
||||
case Enum.drop_while(rest, &(&1 in @spaces or &1 in @closing_brackets)) do
|
||||
'dne' ++ t -> check_spaces(t, :end_expr)
|
||||
_ -> :expr
|
||||
end
|
||||
end
|
||||
|
||||
# Tokenize the remaining passing check_terminators as false,
|
||||
# which relax the tokenizer to not error on unmatched pairs.
|
||||
# If the tokens start with an "end" we have a middle expr.
|
||||
defp tokenize_rest(rest) do
|
||||
:elixir_tokenizer.tokenize(Enum.reverse(rest), 1, file: "eex", check_terminators: false)
|
||||
end
|
||||
|
||||
defp fn_index(tokens) do
|
||||
Enum.find_index(tokens, fn
|
||||
{:fn_paren, _} -> true
|
||||
{:fn, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp end_index(tokens) do
|
||||
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
|
||||
end
|
||||
|
||||
defp check_spaces(string, token) do
|
||||
if Enum.all?(string, &(&1 in @spaces)) do
|
||||
token
|
||||
else
|
||||
:expr
|
||||
end
|
||||
end
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text([{_line, _column}], acc) do
|
||||
defp tokenize_text([], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(buffer, acc) do
|
||||
[{line, column} | buffer] = Enum.reverse(buffer)
|
||||
[{:text, line, column, buffer} | acc]
|
||||
[{:text, Enum.reverse(buffer)} | acc]
|
||||
end
|
||||
|
||||
defp trim_if_needed(rest, line, column, opts, buffer) do
|
||||
if opts.trim do
|
||||
buffer = trim_left(buffer, 0)
|
||||
{rest, line, column} = trim_right(rest, line, column, 0, opts)
|
||||
{rest, line, column, buffer}
|
||||
# If trim mode is enabled and the token is on a line with
|
||||
# only itself and whitespace, trim the whitespace around it,
|
||||
# including the line break following it if there is one.
|
||||
defp trim_if_needed(rest, line, opts, buffer, acc) do
|
||||
with true <- opts[:trim],
|
||||
{true, new_buffer} <- trim_left(buffer, acc),
|
||||
{true, new_rest, new_line} <- trim_right(rest, line) do
|
||||
{true, new_rest, new_line, new_buffer}
|
||||
else
|
||||
{rest, line, column, buffer}
|
||||
_ -> {false, rest, line, buffer}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_init([h | t], line, column, opts) when h in @spaces,
|
||||
do: trim_init(t, line, column + 1, opts)
|
||||
|
||||
defp trim_init([?\r, ?\n | t], line, _column, opts),
|
||||
do: trim_init(t, line + 1, opts.indentation + 1, opts)
|
||||
|
||||
defp trim_init([?\n | t], line, _column, opts),
|
||||
do: trim_init(t, line + 1, opts.indentation + 1, opts)
|
||||
|
||||
defp trim_init([?<, ?% | _] = rest, line, column, _opts),
|
||||
do: {rest, line, column}
|
||||
|
||||
defp trim_init(_, _, _, _), do: false
|
||||
|
||||
defp trim_left(buffer, count) do
|
||||
case trim_whitespace(buffer, 0) do
|
||||
{[?\n, ?\r | rest], _} -> trim_left(rest, count + 1)
|
||||
{[?\n | rest], _} -> trim_left(rest, count + 1)
|
||||
_ when count > 0 -> [?\n | buffer]
|
||||
_ -> buffer
|
||||
defp trim_left(buffer, acc) do
|
||||
case {trim_whitespace(buffer), acc} do
|
||||
{[?\n | _] = trimmed_buffer, _} -> {true, trimmed_buffer}
|
||||
{[], [{_, _, _, _, true} | _]} -> {true, []}
|
||||
{[], []} -> {true, []}
|
||||
_ -> {false, buffer}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_right(rest, line, column, last_column, opts) do
|
||||
case trim_whitespace(rest, column) do
|
||||
{[?\r, ?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, opts.indentation + 1, column + 1, opts)
|
||||
|
||||
{[?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, opts.indentation + 1, column, opts)
|
||||
|
||||
{[], column} ->
|
||||
{[], line, column}
|
||||
|
||||
_ when last_column > 0 ->
|
||||
{[?\n | rest], line - 1, last_column}
|
||||
|
||||
_ ->
|
||||
{rest, line, column}
|
||||
defp trim_right(rest, line) do
|
||||
case trim_whitespace(rest) do
|
||||
[?\r, ?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
|
||||
[?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
|
||||
[] -> {true, [], line}
|
||||
_ -> {false, rest, line}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_whitespace([h | t], column) when h in @spaces, do: trim_whitespace(t, column + 1)
|
||||
defp trim_whitespace(list, column), do: {list, column}
|
||||
defp trim_whitespace([h | t]) when h in @spaces do
|
||||
trim_whitespace(t)
|
||||
end
|
||||
|
||||
defp trim_whitespace(list) do
|
||||
list
|
||||
end
|
||||
|
||||
defp pad_if_needed(:start_expr, [h | _] = expr, true) when h not in @spaces, do: [?\s | expr]
|
||||
defp pad_if_needed(_, expr, _), do: expr
|
||||
end
|
||||
|
||||
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
|
||||
assert_received :found
|
||||
end
|
||||
|
||||
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "the contents of this expression won't be output"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||
assert result == expected
|
||||
|
||||
@@ -4,39 +4,37 @@ defmodule EEx.TokenizerTest do
|
||||
use ExUnit.Case, async: true
|
||||
require EEx.Tokenizer, as: T
|
||||
|
||||
@opts %{indentation: 0, trim: false}
|
||||
|
||||
test "simple chars lists" do
|
||||
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 1, 1, 'foo'}, {:eof, 1, 4}]}
|
||||
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 1, 1, 'foo'}, {:eof, 1, 4}]}
|
||||
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert T.tokenize('foo <% bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
|
||||
assert T.tokenize('foo <% bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '', ' bar ', false}]}
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert T.tokenize('foo <%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
|
||||
assert T.tokenize('foo <%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '=', ' bar ', false}]}
|
||||
end
|
||||
|
||||
test "strings with embedded slash code" do
|
||||
assert T.tokenize('foo <%/ bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
|
||||
assert T.tokenize('foo <%/ bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '/', ' bar ', false}]}
|
||||
end
|
||||
|
||||
test "strings with embedded pipe code" do
|
||||
assert T.tokenize('foo <%| bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
|
||||
assert T.tokenize('foo <%| bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '|', ' bar ', false}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert T.tokenize('foo\n<%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
|
||||
assert T.tokenize('foo\n<%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo\n'}, {:expr, 2, '=', ' bar ', false}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
@@ -48,210 +46,168 @@ defmodule EEx.TokenizerTest do
|
||||
'''
|
||||
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:expr, 1, 5, '=', ' bar\n\nbaz '},
|
||||
{:text, 3, 7, '\n'},
|
||||
{:expr, 4, 1, '', ' foo '},
|
||||
{:text, 4, 10, '\n'},
|
||||
{:eof, 5, 1}
|
||||
{:text, 'foo '},
|
||||
{:expr, 1, '=', ' bar\n\nbaz ', false},
|
||||
{:text, '\n'},
|
||||
{:expr, 4, '', ' foo ', false},
|
||||
{:text, '\n'}
|
||||
]
|
||||
|
||||
assert T.tokenize(string, 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize(string, 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "quotation" do
|
||||
assert T.tokenize('foo <%% true %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo <% true %>'}, {:eof, 1, 16}]}
|
||||
assert T.tokenize('foo <%% true %>', 1) == {:ok, [{:text, 'foo <% true %>'}]}
|
||||
end
|
||||
|
||||
test "quotation with do/end" do
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 1, 1, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) ==
|
||||
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}]}
|
||||
end
|
||||
|
||||
test "quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'a <% b '},
|
||||
{:expr, 1, 9, '=', ' c '},
|
||||
{:text, 1, 17, ' '},
|
||||
{:expr, 1, 18, '=', ' d '},
|
||||
{:text, 1, 26, ' e %> f'},
|
||||
{:eof, 1, 33}
|
||||
{:text, 'a <% b '},
|
||||
{:expr, 1, '=', ' c ', false},
|
||||
{:text, ' '},
|
||||
{:expr, 1, '=', ' d ', false},
|
||||
{:text, ' e %> f'}
|
||||
]
|
||||
|
||||
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "improperly formatted quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, 1, 1, '<%% a <%= b %> c %>'},
|
||||
{:eof, 1, 22}
|
||||
{:text, '<%% a <%= b %> c %>'}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%%% a <%%= b %> c %>', 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize('<%%% a <%%= b %> c %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "eex comments" do
|
||||
test "comments" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:eof, 1, 16}
|
||||
{:text, 'foo '}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <%# true %>', 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize('foo <%# true %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "eex comments with do/end" do
|
||||
test "comments with do/end" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:text, 1, 19, 'bar'},
|
||||
{:eof, 1, 32}
|
||||
{:text, 'foo bar'}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "elixir comments" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:expr, 1, 5, [], ' true # this is a boolean '},
|
||||
{:eof, 1, 35}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% true # this is a boolean %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "elixir comments with do/end" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, [], ' if true do # startif '},
|
||||
{:text, 1, 27, 'text'},
|
||||
{:end_expr, 1, 31, [], ' end # closeif '},
|
||||
{:eof, 1, 50}
|
||||
]
|
||||
|
||||
assert T.tokenize('<% if true do # startif %>text<% end # closeif %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded do end" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 1, 21, 'bar'},
|
||||
{:end_expr, 1, 24, '', ' end '},
|
||||
{:eof, 1, 33}
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' if true do ', false},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' cond do '},
|
||||
{:middle_expr, 1, 18, '', ' false -> '},
|
||||
{:text, 1, 32, 'bar'},
|
||||
{:middle_expr, 1, 35, '', ' true -> '},
|
||||
{:text, 1, 48, 'baz'},
|
||||
{:end_expr, 1, 51, '', ' end '},
|
||||
{:eof, 1, 60}
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' cond do ', false},
|
||||
{:middle_expr, 1, '', ' false -> ', false},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, '', ' true -> ', false},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1, 1, @opts) ==
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with multiple callbacks" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 1, 15, 'foo'},
|
||||
{:middle_expr, 1, 18, '', ' end, fn -> '},
|
||||
{:text, 1, 34, 'bar'},
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
{:start_expr, 1, '=', ' a fn -> ', false},
|
||||
{:text, 'foo'},
|
||||
{:middle_expr, 1, '', ' end, fn -> ', false},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with callback followed by do block" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 1, 15, 'foo'},
|
||||
{:middle_expr, 1, 18, '', ' end do '},
|
||||
{:text, 1, 30, 'bar'},
|
||||
{:end_expr, 1, 33, '', ' end '},
|
||||
{:eof, 1, 42}
|
||||
{:start_expr, 1, '=', ' a fn -> ', false},
|
||||
{:text, 'foo'},
|
||||
{:middle_expr, 1, '', ' end do ', false},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
exprs = [
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 1, 21, 'bar'},
|
||||
{:middle_expr, 1, 24, '', ' else '},
|
||||
{:text, 1, 34, 'baz'},
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' if true do ', false},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, '', ' else ', false},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
|
||||
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> '
|
||||
|
||||
exprs = [
|
||||
{:start_expr, 1, 2, '=', ' if true do '},
|
||||
{:text, 1, 20, '\n TRUE \n'},
|
||||
{:middle_expr, 3, 3, '', ' else '},
|
||||
{:text, 3, 13, '\n FALSE \n'},
|
||||
{:end_expr, 5, 3, '', ' end '},
|
||||
{:eof, 7, 3}
|
||||
{:start_expr, 1, '=', ' if true do ', true},
|
||||
{:text, ' TRUE \n'},
|
||||
{:middle_expr, 3, '', ' else ', true},
|
||||
{:text, ' FALSE \n'},
|
||||
{:end_expr, 5, '', ' end ', true}
|
||||
]
|
||||
|
||||
assert T.tokenize(template, 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
assert T.tokenize(template, 1, trim: true) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with comment" do
|
||||
exprs = [
|
||||
{:text, 1, 19, '\n123'},
|
||||
{:eof, 2, 4}
|
||||
{:text, '123'}
|
||||
]
|
||||
|
||||
assert T.tokenize(' <%# comment %> \n123', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
assert T.tokenize(' <%# comment %> \n123', 1, trim: true) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with CRLF" do
|
||||
exprs = [
|
||||
{:text, 1, 1, '0\n'},
|
||||
{:expr, 2, 3, '=', ' 12 '},
|
||||
{:text, 2, 15, '\n34'},
|
||||
{:eof, 3, 3}
|
||||
{:text, '0\r\n'},
|
||||
{:expr, 2, '=', ' 12 ', true},
|
||||
{:text, '34'}
|
||||
]
|
||||
|
||||
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, trim: true) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode set to false" do
|
||||
exprs = [
|
||||
{:text, 1, 1, ' '},
|
||||
{:expr, 1, 2, '=', ' 12 '},
|
||||
{:text, 1, 11, ' \n'},
|
||||
{:eof, 2, 1}
|
||||
{:text, ' '},
|
||||
{:expr, 1, '=', ' 12 ', false},
|
||||
{:text, ' \n'}
|
||||
]
|
||||
|
||||
assert T.tokenize(' <%= 12 %> \n', 1, 1, %{@opts | trim: false}) == {:ok, exprs}
|
||||
assert T.tokenize(' <%= 12 %> \n', 1, trim: false) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode no false positives" do
|
||||
assert_not_trimmed = fn x ->
|
||||
assert T.tokenize(x, 1, 1, %{@opts | trim: false}) == T.tokenize(x, 1, 1, @opts)
|
||||
end
|
||||
assert_not_trimmed = fn x -> assert T.tokenize(x, 1, trim: true) == T.tokenize(x, 1) end
|
||||
|
||||
assert_not_trimmed.('foo <%= "bar" %> ')
|
||||
assert_not_trimmed.('\n <%= "foo" %>bar')
|
||||
@@ -259,13 +215,8 @@ defmodule EEx.TokenizerTest do
|
||||
assert_not_trimmed.(' <%= 01 %><%= 23 %>\n')
|
||||
end
|
||||
|
||||
test "returns error when there is start mark and no end mark" do
|
||||
assert T.tokenize('foo <% :bar', 1, 1, @opts) == {:error, 1, 12, "missing token '%>'"}
|
||||
assert T.tokenize('<%# true ', 1, 1, @opts) == {:error, 1, 10, "missing token '%>'"}
|
||||
end
|
||||
|
||||
test "marks invalid expressions as regular expressions" do
|
||||
assert T.tokenize('<% 1 $ 2 %>', 1, 1, @opts) ==
|
||||
{:ok, [{:expr, 1, 1, [], ' 1 $ 2 '}, {:eof, 1, 12}]}
|
||||
test "raise syntax error when there is start mark and no end mark" do
|
||||
assert T.tokenize('foo <% :bar', 1) == {:error, 1, "missing token '%>'"}
|
||||
assert T.tokenize('<%# true ', 1) == {:error, 1, "missing token '%>'"}
|
||||
end
|
||||
end
|
||||
|
||||
+33
-107
@@ -79,28 +79,8 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
string = "<%= 123 %> \n \n <%= 789 %>"
|
||||
expected = "123\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n456\n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n\n456\n\n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n \n456\n \n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\n <%= 123 %> \n <%= 456 %> \n <%= 789 %> \n"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\r\n <%= 123 %> \r\n <%= 456 %> \r\n <%= 789 %> \r\n"
|
||||
expected = "123\n456\n789"
|
||||
expected = "123456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -114,7 +94,7 @@ defmodule EExTest do
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
expected = "\n that\n"
|
||||
expected = " that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -126,22 +106,11 @@ defmodule EExTest do
|
||||
<%= "Fourth line" %>
|
||||
"""
|
||||
|
||||
expected = "First line\nSecond line\nThird line\nFourth line"
|
||||
expected = "First lineSecond lineThird lineFourth line"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with no spaces" do
|
||||
string = """
|
||||
<%=if true do%>
|
||||
this
|
||||
<%else%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n this\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
@@ -151,7 +120,7 @@ defmodule EExTest do
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n that\n"
|
||||
expected = " that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -251,36 +220,34 @@ defmodule EExTest do
|
||||
|
||||
describe "raises syntax errors" do
|
||||
test "when the token is invalid" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:12: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar")
|
||||
end
|
||||
end
|
||||
|
||||
test "when middle expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError,
|
||||
"nofile:1:18: unexpected middle of expression <% else %>",
|
||||
fn ->
|
||||
EEx.compile_string("<%= if true %>foo<% else %>bar<% end %>")
|
||||
end
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected middle of expression <% else %>", fn ->
|
||||
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:5: unexpected end of expression <% end %>", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when start expression is found without an end expression" do
|
||||
msg = "nofile:2:18: unexpected end of string, expected a closing '<% end %>'"
|
||||
msg = "nofile:2: unexpected end of string, expected a closing '<% end %>'"
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
EEx.compile_string("foo\n<%= if true do %>")
|
||||
EEx.compile_string("foo\n<% if true do %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when nested end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1:31: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <%= if true do %><% end %><% end %>")
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <% if true do %><% end %><% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -318,13 +285,13 @@ defmodule EExTest do
|
||||
|
||||
describe "error messages" do
|
||||
test "honor line numbers" do
|
||||
assert_raise EEx.SyntaxError, "nofile:99:12: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:99: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", line: 99)
|
||||
end
|
||||
end
|
||||
|
||||
test "honor file names" do
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
@@ -528,51 +495,28 @@ defmodule EExTest do
|
||||
<% "a" in y -> %>
|
||||
Good
|
||||
<% true -> %>
|
||||
<%= if true do %>true<% else %>false<% end %>
|
||||
<% if true do %>true<% else %>false<% end %>
|
||||
Bad
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n\n Good\n \n", string)
|
||||
end
|
||||
|
||||
test "line and column meta" do
|
||||
parser_options = Code.get_compiler_option(:parser_options)
|
||||
Code.put_compiler_option(:parser_options, columns: true)
|
||||
|
||||
try do
|
||||
indentation = 12
|
||||
|
||||
ast =
|
||||
EEx.compile_string(
|
||||
"""
|
||||
<%= f() %> <% f() %>
|
||||
<%= f fn -> %>
|
||||
<%= f() %>
|
||||
<% end %>
|
||||
""",
|
||||
indentation: indentation
|
||||
)
|
||||
|
||||
{_, calls} =
|
||||
Macro.prewalk(ast, [], fn
|
||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||
other, acc -> {other, acc}
|
||||
end)
|
||||
|
||||
assert Enum.reverse(calls) == [
|
||||
[line: 1, column: indentation + 5],
|
||||
[line: 1, column: indentation + 15],
|
||||
[line: 2, column: indentation + 7],
|
||||
[line: 3, column: indentation + 9]
|
||||
]
|
||||
after
|
||||
Code.put_compiler_option(:parser_options, parser_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "buffers" do
|
||||
test "unused buffers are kept out" do
|
||||
string = """
|
||||
<%= 123 %>
|
||||
<% if true do %>
|
||||
<%= 456 %>
|
||||
<% end %>
|
||||
<%= 789 %>
|
||||
"""
|
||||
|
||||
assert_eval("123\n\n789\n", string)
|
||||
end
|
||||
|
||||
test "inside comprehensions" do
|
||||
string = """
|
||||
<%= for _name <- packages || [] do %>
|
||||
@@ -610,24 +554,6 @@ defmodule EExTest do
|
||||
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
|
||||
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
|
||||
end
|
||||
|
||||
test "supports t:Path.t() paths" do
|
||||
filename = to_charlist(Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex"))
|
||||
result = EEx.eval_file(filename, bar: 1)
|
||||
assert_normalized_newline_equal("foo 1\n", result)
|
||||
end
|
||||
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
|
||||
test "supports overriding file and line through options" do
|
||||
filename = Path.join(__DIR__, "fixtures/eex_template_with_syntax_error.eex")
|
||||
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:11:1: missing token '%>'", fn ->
|
||||
EEx.eval_file(filename, _bindings = [], file: "my_file.eex", line: 10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "precompiled" do
|
||||
@@ -673,8 +599,8 @@ defmodule EExTest do
|
||||
buffer <> ":END"
|
||||
end
|
||||
|
||||
def handle_text(buffer, meta, text) do
|
||||
buffer <> ":TEXT-#{meta[:line]}-#{meta[:column]}(#{String.trim(text)})"
|
||||
def handle_text(buffer, text) do
|
||||
buffer <> ":TEXT(#{String.trim(text)})"
|
||||
end
|
||||
|
||||
def handle_expr(buffer, "/", expr) do
|
||||
@@ -692,16 +618,16 @@ defmodule EExTest do
|
||||
|
||||
describe "custom engines" do
|
||||
test "text" do
|
||||
assert_eval("BODY(INIT:TEXT-1-1(foo))", "foo", [], engine: TestEngine)
|
||||
assert_eval("BODY(INIT:TEXT(foo))", "foo", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
test "custom marker" do
|
||||
assert_eval("BODY(INIT:TEXT-1-1(foo):DIV(:bar))", "foo <%/ :bar %>", [], engine: TestEngine)
|
||||
assert_eval("BODY(INIT:TEXT(foo):DIV(:bar))", "foo <%/ :bar %>", [], engine: TestEngine)
|
||||
end
|
||||
|
||||
test "begin/end" do
|
||||
assert_eval(
|
||||
~s[BODY(INIT:TEXT-1-1(foo):EQUAL(if do\n "BEGIN:TEXT-1-17(this):END"\nelse\n "BEGIN:TEXT-1-31(that):END"\nend))],
|
||||
~s[BODY(INIT:TEXT(foo):EQUAL(if do\n "BEGIN:TEXT(this):END"\nelse\n "BEGIN:TEXT(that):END"\nend))],
|
||||
"foo <%= if do %>this<% else %>that<% end %>",
|
||||
[],
|
||||
engine: TestEngine
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
foo <%= bar
|
||||
@@ -11,6 +11,7 @@
|
||||
warn_exported_vars,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
warnings_as_errors,
|
||||
debug_info,
|
||||
{outdir, "ebin/"}
|
||||
]}.
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
defmodule Diff do
|
||||
@moduledoc """
|
||||
Utilities for comparing build artifacts.
|
||||
"""
|
||||
|
||||
@known_chunks ~w(
|
||||
abstract_code
|
||||
debug_info
|
||||
attributes
|
||||
compile_info
|
||||
exports
|
||||
labeled_exports
|
||||
imports
|
||||
indexed_imports
|
||||
locals
|
||||
labeled_locals
|
||||
atoms
|
||||
)a
|
||||
|
||||
@doc """
|
||||
Compares the build artifacts of two build directories.
|
||||
"""
|
||||
@spec compare_dirs(Path.t(), Path.t()) ::
|
||||
{
|
||||
only1_paths :: list(Path.t()),
|
||||
only2_paths :: list(Path.t()),
|
||||
diff :: list({Path.t(), diff :: String.t()})
|
||||
}
|
||||
def compare_dirs(dir1, dir2) do
|
||||
dir1 = Path.expand(dir1)
|
||||
dir2 = Path.expand(dir2)
|
||||
|
||||
assert_dir!(dir1)
|
||||
assert_dir!(dir2)
|
||||
|
||||
dir1_paths = relative_paths(dir1)
|
||||
dir2_paths = relative_paths(dir2)
|
||||
|
||||
only1_paths = dir1_paths -- dir2_paths
|
||||
only2_paths = dir2_paths -- dir1_paths
|
||||
common_paths = dir1_paths -- only1_paths
|
||||
common_files = Enum.reject(common_paths, &File.dir?/1)
|
||||
|
||||
diff =
|
||||
Enum.flat_map(common_files, fn path ->
|
||||
file1 = Path.join(dir1, path)
|
||||
file2 = Path.join(dir2, path)
|
||||
|
||||
case compare_files(file1, file2) do
|
||||
:eq -> []
|
||||
{:diff, diff} -> [{path, diff}]
|
||||
end
|
||||
end)
|
||||
|
||||
{only1_paths, only2_paths, diff}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares the contents of two files.
|
||||
|
||||
If the files are BEAM files, it performs a more human-friendly
|
||||
"BEAM-diff".
|
||||
"""
|
||||
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
|
||||
def compare_files(file1, file2) do
|
||||
content1 = File.read!(file1)
|
||||
content2 = File.read!(file2)
|
||||
|
||||
if content1 == content2 do
|
||||
:eq
|
||||
else
|
||||
diff =
|
||||
if String.ends_with?(file1, ".beam") do
|
||||
beam_diff(file1, content1, file2, content2)
|
||||
else
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
|
||||
{:diff, diff}
|
||||
end
|
||||
end
|
||||
|
||||
defp beam_diff(file1, content1, file2, content2) do
|
||||
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
|
||||
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
|
||||
true <- chunks1 != chunks2 do
|
||||
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
|
||||
tmp_file1 =
|
||||
chunk1
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
tmp_file2 =
|
||||
chunk2
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
file_diff(tmp_file1, tmp_file2)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
end
|
||||
|
||||
defp file_diff(file1, file2) do
|
||||
{diff, _} = System.cmd("diff", [file1, file2])
|
||||
diff
|
||||
end
|
||||
|
||||
defp relative_paths(dir) do
|
||||
dir
|
||||
|> Path.join("**")
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&Path.relative_to(&1, dir))
|
||||
end
|
||||
|
||||
defp assert_dir!(dir) do
|
||||
unless File.dir?(dir) do
|
||||
raise ArgumentError, "#{inspect(dir)} is not a directory"
|
||||
end
|
||||
end
|
||||
|
||||
defp write_tmp(content) do
|
||||
filename = generate_tmp_filename()
|
||||
File.mkdir_p!("tmp")
|
||||
File.write!(Path.join("tmp", filename), content)
|
||||
Path.join("tmp", filename)
|
||||
end
|
||||
|
||||
defp generate_tmp_filename do
|
||||
sec = :os.system_time(:second)
|
||||
rand = :rand.uniform(999_999_999)
|
||||
scheduler_id = :erlang.system_info(:scheduler_id)
|
||||
"tmp-#{sec}-#{rand}-#{scheduler_id}"
|
||||
end
|
||||
end
|
||||
|
||||
case System.argv() do
|
||||
[dir1, dir2] ->
|
||||
case Diff.compare_dirs(dir1, dir2) do
|
||||
{[], [], []} ->
|
||||
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
|
||||
|
||||
{only1, only2, diff} ->
|
||||
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
|
||||
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
|
||||
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
|
||||
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
_ ->
|
||||
IO.puts("Please, provide two directories as arguments")
|
||||
System.halt(1)
|
||||
end
|
||||
+13
-26
@@ -1,20 +1,10 @@
|
||||
# Returns config for Elixir docs
|
||||
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
|
||||
[
|
||||
extras: Path.wildcard("lib/elixir/pages/*.md") ++ ["CHANGELOG.md"],
|
||||
deps: [
|
||||
eex: "https://hexdocs.pm/eex/#{canonical}",
|
||||
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
|
||||
iex: "https://hexdocs.pm/iex/#{canonical}",
|
||||
logger: "https://hexdocs.pm/logger/#{canonical}",
|
||||
mix: "https://hexdocs.pm/mix/#{canonical}"
|
||||
],
|
||||
extras: Path.wildcard("lib/elixir/pages/*.md"),
|
||||
groups_for_functions: [
|
||||
Guards: &(&1[:guard] == true)
|
||||
Guards: & &1[:guard] == true
|
||||
],
|
||||
skip_undefined_reference_warnings_on: ["lib/elixir/pages/compatibility-and-deprecations.md"],
|
||||
skip_undefined_reference_warnings_on: ["compatibility-and-deprecations"],
|
||||
groups_for_modules: [
|
||||
# [Kernel, Kernel.SpecialForms],
|
||||
|
||||
@@ -63,7 +53,7 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
StringIO,
|
||||
System
|
||||
],
|
||||
Calendar: [
|
||||
"Calendar": [
|
||||
Calendar,
|
||||
Calendar.ISO,
|
||||
Calendar.TimeZoneDatabase,
|
||||
@@ -99,18 +89,15 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
Kernel.ParallelCompiler,
|
||||
Macro,
|
||||
Macro.Env
|
||||
],
|
||||
Deprecated: [
|
||||
Behaviour,
|
||||
Dict,
|
||||
GenEvent,
|
||||
HashDict,
|
||||
HashSet,
|
||||
Set,
|
||||
Supervisor.Spec
|
||||
]
|
||||
|
||||
## Automatically detected groups
|
||||
|
||||
# Deprecated: [
|
||||
# Behaviour,
|
||||
# Dict,
|
||||
# GenEvent,
|
||||
# HashDict,
|
||||
# HashSet,
|
||||
# Set,
|
||||
# Supervisor.Spec
|
||||
# ]
|
||||
]
|
||||
]
|
||||
|
||||
+44
-77
@@ -6,8 +6,8 @@ defmodule Access do
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both return `nil` if the key does not exist:
|
||||
of the box. The key can be of any type and it returns `nil` if
|
||||
the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
@@ -47,11 +47,12 @@ defmodule Access do
|
||||
> `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). 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.
|
||||
> See the `Map` module for more information.
|
||||
> is missing. This is important because, if a map has a predefined
|
||||
> set of keys and a key is missing, it is most likely a bug
|
||||
> in your software or a typo on the key name. For this reason,
|
||||
> because structs are predefined in nature, they only allow
|
||||
> the `struct.key` syntax and they do not allow the `struct[key]`
|
||||
> access syntax. See the `Map` module for more information.
|
||||
|
||||
## Nested data structures
|
||||
|
||||
@@ -99,15 +100,16 @@ defmodule Access do
|
||||
@type key :: any
|
||||
@type value :: any
|
||||
|
||||
@type get_fun(data) ::
|
||||
(:get, data, (term -> term) -> new_data :: container)
|
||||
@type get_fun(data, get_value) ::
|
||||
(:get, data, (term -> term) ->
|
||||
{get_value, new_data :: container})
|
||||
|
||||
@type get_and_update_fun(data, current_value) ::
|
||||
@type get_and_update_fun(data, get_value) ::
|
||||
(:get_and_update, data, (term -> term) ->
|
||||
{current_value, new_data :: container} | :pop)
|
||||
{get_value, new_data :: container} | :pop)
|
||||
|
||||
@type access_fun(data, current_value) ::
|
||||
get_fun(data) | get_and_update_fun(data, current_value)
|
||||
@type access_fun(data, get_value) ::
|
||||
get_fun(data, get_value) | get_and_update_fun(data, get_value)
|
||||
|
||||
@doc """
|
||||
Invoked in order to access the value stored under `key` in the given term `term`.
|
||||
@@ -133,16 +135,16 @@ defmodule Access do
|
||||
|
||||
The implementation of this callback should invoke `fun` with the value under
|
||||
`key` in the passed structure `data`, or with `nil` if `key` is not present in it.
|
||||
This function must return either `{current_value, new_value}` or `:pop`.
|
||||
This function must return either `{get_value, update_value}` or `:pop`.
|
||||
|
||||
If the passed function returns `{current_value, new_value}`,
|
||||
the return value of this callback should be `{current_value, new_data}`, where:
|
||||
If the passed function returns `{get_value, update_value}`,
|
||||
the return value of this callback should be `{get_value, new_data}`, where:
|
||||
|
||||
* `current_value` is the retrieved value (which can be operated on before being returned)
|
||||
* `get_value` is the retrieved value (which can be operated on before being returned)
|
||||
|
||||
* `new_value` is the new value to be stored under `key`
|
||||
* `update_value` is the new value to be stored under `key`
|
||||
|
||||
* `new_data` is `data` after updating the value of `key` with `new_value`.
|
||||
* `new_data` is `data` after updating the value of `key` with `update_value`.
|
||||
|
||||
If the passed function returns `:pop`, the return value of this callback
|
||||
must be `{value, new_data}` where `value` is the value under `key`
|
||||
@@ -151,9 +153,8 @@ defmodule Access do
|
||||
See the implementations of `Map.get_and_update/3` or `Keyword.get_and_update/3`
|
||||
for more examples.
|
||||
"""
|
||||
@callback get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_data :: data}
|
||||
when current_value: value, data: container | any_container
|
||||
@callback get_and_update(data, key, (value -> {get_value, value} | :pop)) :: {get_value, data}
|
||||
when get_value: var, data: container | any_container
|
||||
|
||||
@doc """
|
||||
Invoked to "pop" the value under `key` out of the given data structure.
|
||||
@@ -320,9 +321,9 @@ defmodule Access do
|
||||
a struct that implements the `Access` behaviour).
|
||||
|
||||
The `fun` argument receives the value of `key` (or `nil` if `key` is not
|
||||
present in `container`) and must return a two-element tuple `{current_value, new_value}`:
|
||||
the "get" value `current_value` (the retrieved value, which can be operated on before
|
||||
being returned) and the new value to be stored under `key` (`new_value`).
|
||||
present in `container`) and must return a two-element tuple `{get_value, update_value}`:
|
||||
the "get" value `get_value` (the retrieved value, which can be operated on before
|
||||
being returned) and the new value to be stored under `key` (`update_value`).
|
||||
`fun` may also return `:pop`, which means the current value
|
||||
should be removed from the container and returned.
|
||||
|
||||
@@ -337,9 +338,8 @@ defmodule Access do
|
||||
{1, [a: 2]}
|
||||
|
||||
"""
|
||||
@spec get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_data :: data}
|
||||
when current_value: var, data: container
|
||||
@spec get_and_update(data, key, (value -> {get_value, value} | :pop)) :: {get_value, data}
|
||||
when get_value: var, data: container
|
||||
def get_and_update(container, key, fun)
|
||||
|
||||
def get_and_update(%module{} = container, key, fun) do
|
||||
@@ -422,7 +422,7 @@ defmodule Access do
|
||||
The returned function uses the default value if the key does not exist.
|
||||
This can be used to specify defaults and safely traverse missing keys:
|
||||
|
||||
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name, "meg")])
|
||||
iex> get_in(%{}, [Access.key(:user, %{name: "meg"}), Access.key(:name)])
|
||||
"meg"
|
||||
|
||||
Such is also useful when using update functions, allowing us to introduce
|
||||
@@ -452,7 +452,7 @@ defmodule Access do
|
||||
** (BadMapError) expected a map, got: []
|
||||
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, get_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, data, next ->
|
||||
@@ -496,7 +496,7 @@ defmodule Access do
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key!(key) :: access_fun(data :: struct | map, get_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
@@ -544,7 +544,7 @@ defmodule Access do
|
||||
** (RuntimeError) Access.elem/1 expected a tuple, got: %{}
|
||||
|
||||
"""
|
||||
@spec elem(non_neg_integer) :: access_fun(data :: tuple, current_value :: term)
|
||||
@spec elem(non_neg_integer) :: access_fun(data :: tuple, get_value :: term)
|
||||
def elem(index) when is_integer(index) and index >= 0 do
|
||||
pos = index + 1
|
||||
|
||||
@@ -598,7 +598,7 @@ defmodule Access do
|
||||
** (RuntimeError) Access.all/0 expected a list, got: %{}
|
||||
|
||||
"""
|
||||
@spec all() :: access_fun(data :: list, current_value :: list)
|
||||
@spec all() :: access_fun(data :: list, get_value :: list)
|
||||
def all() do
|
||||
&all/3
|
||||
end
|
||||
@@ -673,7 +673,7 @@ defmodule Access do
|
||||
** (RuntimeError) Access.at/1 expected a list, got: %{}
|
||||
|
||||
"""
|
||||
@spec at(integer) :: access_fun(data :: list, current_value :: term)
|
||||
@spec at(integer) :: access_fun(data :: list, get_value :: term)
|
||||
def at(index) when is_integer(index) do
|
||||
fn op, data, next -> at(op, data, index, next) end
|
||||
end
|
||||
@@ -683,69 +683,36 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp at(:get_and_update, data, index, next) when is_list(data) do
|
||||
get_and_update_at(data, index, next, [], fn -> nil end)
|
||||
get_and_update_at(data, index, next, [])
|
||||
end
|
||||
|
||||
defp at(_op, data, _index, _next) do
|
||||
raise "Access.at/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
|
||||
defp get_and_update_at([head | rest], 0, next, updates) do
|
||||
case next.(head) do
|
||||
{get, update} -> {get, :lists.reverse([update | updates], rest)}
|
||||
:pop -> {head, :lists.reverse(updates, rest)}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
|
||||
defp get_and_update_at(list, index, next, updates) when index < 0 do
|
||||
list_length = length(list)
|
||||
|
||||
if list_length + index >= 0 do
|
||||
get_and_update_at(list, list_length + index, next, updates, default_fun)
|
||||
get_and_update_at(list, list_length + index, next, updates)
|
||||
else
|
||||
{default_fun.(), list}
|
||||
{nil, list}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
|
||||
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
|
||||
defp get_and_update_at([head | rest], index, next, updates) when index > 0 do
|
||||
get_and_update_at(rest, index - 1, next, [head | updates])
|
||||
end
|
||||
|
||||
defp get_and_update_at([], _index, _next, updates, default_fun) do
|
||||
{default_fun.(), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
|
||||
if the given index is out of bounds.
|
||||
|
||||
## Examples
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec at!(integer) :: access_fun(data :: list, current_value :: term)
|
||||
def at!(index) when is_integer(index) do
|
||||
fn op, data, next -> at!(op, data, index, next) end
|
||||
end
|
||||
|
||||
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
|
||||
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 end)
|
||||
end
|
||||
|
||||
defp at!(_op, data, _index, _next) do
|
||||
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
|
||||
defp get_and_update_at([], _index, _next, updates) do
|
||||
{nil, :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -795,7 +762,7 @@ defmodule Access do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
||||
@spec filter((term -> boolean)) :: access_fun(data :: list, get_value :: list)
|
||||
def filter(func) when is_function(func) do
|
||||
fn op, data, next -> filter(op, data, func, next) end
|
||||
end
|
||||
|
||||
@@ -241,7 +241,7 @@ defmodule Agent do
|
||||
and the start function will return `{:error, :timeout}`.
|
||||
|
||||
If the `:debug` option is present, the corresponding function in the
|
||||
[`:sys` module](`:sys`) will be invoked.
|
||||
[`:sys` module](http://www.erlang.org/doc/man/sys.html) will be invoked.
|
||||
|
||||
If the `:spawn_opt` option is present, its value will be passed as options
|
||||
to the underlying process as in `Process.spawn/4`.
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Application do
|
||||
programming languages, but with some additional characteristics.
|
||||
|
||||
An application is a component implementing some specific functionality, with a
|
||||
standardized directory structure, configuration, and life cycle. Applications
|
||||
standardized directory structure, configuration, and lifecycle. Applications
|
||||
are *loaded*, *started*, and *stopped*. Each application also has its own
|
||||
environment, which provides a unified API for configuring each application.
|
||||
|
||||
@@ -84,8 +84,8 @@ defmodule Application do
|
||||
|
||||
This approach has one big limitation: if you change the value of the
|
||||
application environment after the code is compiled, the value used at
|
||||
runtime is not going to change! For example, if your `config/runtime.exs`
|
||||
has:
|
||||
runtime is not going to change! For example, if you are using `mix release`
|
||||
and your `config/releases.exs` has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
@@ -163,7 +163,7 @@ defmodule Application do
|
||||
In the sections above, we have configured an application in the
|
||||
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
|
||||
this configuration to create an [*application resource
|
||||
file*](https://erlang.org/doc/man/application.html), which is a file called
|
||||
file*](http://erlang.org/doc/man/app.html), which is a file called
|
||||
`APP_NAME.app`. For example, the application resource file of the OTP
|
||||
application `ex_unit` is called `ex_unit.app`.
|
||||
|
||||
@@ -171,7 +171,7 @@ defmodule Application do
|
||||
the documentation of `Mix.Tasks.Compile.App`, available as well by running
|
||||
`mix help compile.app`.
|
||||
|
||||
## The application life cycle
|
||||
## The application lifecycle
|
||||
|
||||
### Loading applications
|
||||
|
||||
@@ -210,7 +210,7 @@ defmodule Application do
|
||||
dependencies for you.
|
||||
|
||||
If the application does not have a callback module configured, starting is
|
||||
done at this point. Otherwise, its `c:start/2` callback is invoked. The PID of
|
||||
done at this point. Otherwise, its `c:start/2` callback if invoked. The PID of
|
||||
the top-level supervisor returned by this function is stored by the runtime
|
||||
for later use, and the returned application state is saved too, if any.
|
||||
|
||||
@@ -267,10 +267,11 @@ defmodule Application do
|
||||
## Further information
|
||||
|
||||
For further details on applications please check the documentation of the
|
||||
[`:application` Erlang module](`:application`), and the
|
||||
[Applications](https://erlang.org/doc/design_principles/applications.html)
|
||||
[`application`](http://www.erlang.org/doc/man/application.html) Erlang module,
|
||||
and the
|
||||
[Applications](http://www.erlang.org/doc/design_principles/applications.html)
|
||||
section of the [OTP Design Principles User's
|
||||
Guide](https://erlang.org/doc/design_principles/users_guide.html).
|
||||
Guide](http://erlang.org/doc/design_principles/users_guide.html).
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -498,8 +499,7 @@ defmodule Application do
|
||||
Giving a path is useful to let Elixir know that only certain paths
|
||||
in a large configuration are compile time dependent.
|
||||
"""
|
||||
# TODO: Warn on v1.14 if get_env/fetch_env/fetch_env! is used at
|
||||
# compile time instead of compile_env
|
||||
# TODO: Warn if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env(app, key | list, value) :: value
|
||||
defmacro compile_env(app, key_or_path, default \\ nil) when is_atom(app) do
|
||||
@@ -507,22 +507,11 @@ defmodule Application do
|
||||
raise "Application.compile_env/3 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
key_or_path = expand_key_or_path(key_or_path, __CALLER__)
|
||||
|
||||
quote do
|
||||
Application.__compile_env__(unquote(app), unquote(key_or_path), unquote(default), __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_key_or_path({:__aliases__, _, _} = alias, env),
|
||||
do: Macro.expand(alias, %{env | function: {:__info__, 1}})
|
||||
|
||||
defp expand_key_or_path(list, env) when is_list(list),
|
||||
do: Enum.map(list, &expand_key_or_path(&1, env))
|
||||
|
||||
defp expand_key_or_path(other, _env),
|
||||
do: other
|
||||
|
||||
@doc false
|
||||
def __compile_env__(app, key_or_path, default, env) do
|
||||
case fetch_compile_env(app, key_or_path, env) do
|
||||
@@ -535,7 +524,7 @@ defmodule Application do
|
||||
Reads the application environment at compilation time or raises.
|
||||
|
||||
This is the same as `compile_env/3` but it raises an
|
||||
`ArgumentError` if the configuration is not available.
|
||||
ArgumentError if the configuration is not available.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env!(app, key | list) :: value
|
||||
@@ -544,8 +533,6 @@ defmodule Application do
|
||||
raise "Application.compile_env!/2 cannot be called inside functions, only in the module body"
|
||||
end
|
||||
|
||||
key_or_path = expand_key_or_path(key_or_path, __CALLER__)
|
||||
|
||||
quote do
|
||||
Application.__compile_env__!(unquote(app), unquote(key_or_path), __ENV__)
|
||||
end
|
||||
@@ -597,7 +584,7 @@ defmodule Application do
|
||||
**Important:** 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).
|
||||
read our [library guidelines](library-guidelines.html).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -631,7 +618,6 @@ defmodule Application do
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.get_env(app, key, default)
|
||||
end
|
||||
|
||||
@@ -642,8 +628,6 @@ defmodule Application do
|
||||
"""
|
||||
@spec fetch_env(app, key) :: {:ok, value} | :error
|
||||
def fetch_env(app, key) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
:undefined -> :error
|
||||
@@ -680,7 +664,9 @@ defmodule Application do
|
||||
"because configuration at #{inspect(key)} was not set"
|
||||
|
||||
:undefined ->
|
||||
"because the application was not loaded nor configured"
|
||||
"because the application was not loaded/started. If your application " <>
|
||||
"depends on #{inspect(app)} at runtime, make sure to load/start it or " <>
|
||||
"list it under :extra_applications in your mix.exs file"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -703,10 +689,12 @@ defmodule Application do
|
||||
"""
|
||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
# TODO: Remove this once we support Erlang/OTP 22+ exclusively.
|
||||
@compile {:no_warn_undefined, {:application, :set_env, 2}}
|
||||
|
||||
@doc """
|
||||
Puts the environment for multiple apps at the same time.
|
||||
|
||||
@@ -715,14 +703,28 @@ defmodule Application do
|
||||
* 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, it will raise.
|
||||
If those conditions are not met, the behaviour is undefined
|
||||
(on Erlang/OTP 21 and earlier) or will raise (on Erlang/OTP 22
|
||||
and later).
|
||||
|
||||
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
|
||||
def put_all_env(config, opts \\ []) when is_list(config) and is_list(opts) do
|
||||
:application.set_env(config, opts)
|
||||
# TODO: Remove function exported? check when we require Erlang/OTP 22+
|
||||
if function_exported?(:application, :set_env, 2) do
|
||||
:application.set_env(config, opts)
|
||||
else
|
||||
for app_keyword <- config,
|
||||
{app, keyword} = app_keyword,
|
||||
key_value <- keyword,
|
||||
{key, value} = key_value do
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -732,19 +734,9 @@ defmodule Application do
|
||||
"""
|
||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
|
||||
defp maybe_warn_on_app_env_key(_app, key) when is_atom(key),
|
||||
do: :ok
|
||||
|
||||
# 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 = "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
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` is started.
|
||||
|
||||
@@ -884,7 +876,7 @@ defmodule Application do
|
||||
#=> "bar-123"
|
||||
|
||||
For more information on code paths, check the `Code` module in
|
||||
Elixir and also Erlang's [`:code` module](`:code`).
|
||||
Elixir and also Erlang's [`:code` module](http://www.erlang.org/doc/man/code.html).
|
||||
"""
|
||||
@spec app_dir(app) :: String.t()
|
||||
def app_dir(app) when is_atom(app) do
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Behaviour do
|
||||
|
||||
This module is deprecated. Instead of `defcallback/1` and
|
||||
`defmacrocallback/1`, the `@callback` and `@macrocallback`
|
||||
module attributes can be used respectively. See the
|
||||
module attributes can be used (respectively). See the
|
||||
documentation for `Module` for more information on these
|
||||
attributes.
|
||||
|
||||
@@ -17,7 +17,6 @@ defmodule Behaviour do
|
||||
@doc """
|
||||
Defines a function callback according to the given type specification.
|
||||
"""
|
||||
@deprecated "Use the @callback module attribute instead"
|
||||
defmacro defcallback(spec) do
|
||||
do_defcallback(:def, split_spec(spec, quote(do: term)))
|
||||
end
|
||||
@@ -25,7 +24,6 @@ defmodule Behaviour do
|
||||
@doc """
|
||||
Defines a macro callback according to the given type specification.
|
||||
"""
|
||||
@deprecated "Use the @macrocallback module attribute instead"
|
||||
defmacro defmacrocallback(spec) do
|
||||
do_defcallback(:defmacro, split_spec(spec, quote(do: Macro.t())))
|
||||
end
|
||||
@@ -113,9 +111,9 @@ defmodule Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
defp __behaviour__doc_value(%{"en" => doc}), do: doc
|
||||
defp __behaviour__doc_value(:none), do: nil
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(_), do: nil
|
||||
defp __behaviour__doc_value(%{"en" => doc}), do: doc
|
||||
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
|
||||
+20
-18
@@ -61,7 +61,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise NOT of the argument.
|
||||
Calculates the bitwise NOT of its argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -81,9 +81,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise NOT unary operator.
|
||||
|
||||
Calculates the bitwise NOT of the argument.
|
||||
Prefix (unary) operator; calculates the bitwise NOT of its argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -120,9 +118,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise AND operator.
|
||||
|
||||
Calculates the bitwise AND of its arguments.
|
||||
Infix operator; calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -156,9 +152,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Bitwise OR operator.
|
||||
|
||||
Calculates the bitwise OR of its arguments.
|
||||
Infix operator; calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -191,8 +185,20 @@ defmodule Bitwise do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unquote(:^^^)(left, right) do
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise XOR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 9 ^^^ 3
|
||||
10
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec integer ^^^ integer :: integer
|
||||
def left ^^^ right do
|
||||
:erlang.bxor(left, right)
|
||||
end
|
||||
|
||||
@@ -223,9 +229,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic left bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic left bitshift.
|
||||
Infix operator; calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -277,9 +281,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic right bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic right bitshift.
|
||||
Infix operator; calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
|
||||
+3
-523
@@ -68,18 +68,10 @@ defmodule Calendar do
|
||||
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
|
||||
@type zone_abbr :: String.t()
|
||||
|
||||
@typedoc """
|
||||
The time zone UTC offset in seconds for standard time.
|
||||
|
||||
See also `t:std_offset/0`.
|
||||
"""
|
||||
@typedoc "The time zone UTC offset in seconds"
|
||||
@type utc_offset :: integer
|
||||
|
||||
@typedoc """
|
||||
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".
|
||||
"""
|
||||
@typedoc "The time zone standard offset in seconds (not zero in summer times)"
|
||||
@type std_offset :: integer
|
||||
|
||||
@typedoc "Any map/struct that contains the date fields"
|
||||
@@ -167,14 +159,8 @@ defmodule Calendar do
|
||||
|
||||
@doc """
|
||||
Calculates the day of the week from the given `year`, `month`, and `day`.
|
||||
|
||||
The `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.
|
||||
"""
|
||||
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
|
||||
{day_of_week(), first_day_of_week :: non_neg_integer(),
|
||||
last_day_of_week :: non_neg_integer()}
|
||||
@callback day_of_week(year, month, day) :: day_of_week()
|
||||
|
||||
@doc """
|
||||
Calculates the day of the year from the given `year`, `month`, and `day`.
|
||||
@@ -377,510 +363,4 @@ defmodule Calendar do
|
||||
def get_time_zone_database() do
|
||||
Application.get_env(:elixir, :time_zone_database, Calendar.UTCOnlyTimeZoneDatabase)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats received datetime into a string.
|
||||
|
||||
The datetime can be any of the Calendar types (`Time`, `Date`,
|
||||
`NaiveDateTime`, and `DateTime`) or any map, as long as they
|
||||
contain all of the relevant fields necessary for formatting.
|
||||
For example, if you use `%Y` to format the year, the datetime
|
||||
must have the `:year` field. Therefore, if you pass a `Time`,
|
||||
or a map without the `:year` field to a format that expects `%Y`,
|
||||
an error will be raised.
|
||||
|
||||
## Options
|
||||
|
||||
* `:preferred_datetime` - a string for the preferred format to show datetimes,
|
||||
it can't contain the `%c` format and defaults to `"%Y-%m-%d %H:%M:%S"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_date` - a string for the preferred format to show dates,
|
||||
it can't contain the `%x` format and defaults to `"%Y-%m-%d"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_time` - a string for the preferred format to show times,
|
||||
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 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 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 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 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 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
|
||||
|
||||
The formatting syntax for strftime is a sequence of characters in the following format:
|
||||
|
||||
%<padding><width><format>
|
||||
|
||||
where:
|
||||
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
|
||||
* `-`: no padding, removes all padding from the format
|
||||
* `_`: pad with spaces
|
||||
* `0`: pad with zeroes
|
||||
|
||||
### Accepted formats
|
||||
|
||||
The accepted formats are:
|
||||
|
||||
Format | Description | Examples (in ISO)
|
||||
:----- | :-----------------------------------------------------------------------| :------------------------
|
||||
a | Abbreviated name of day | Mon
|
||||
A | Full name of day | Monday
|
||||
b | Abbreviated month name | Jan
|
||||
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 *(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
|
||||
m | Month | 01, 12
|
||||
M | Minute | 00, 59
|
||||
p | "AM" or "PM" (noon is "PM", midnight as "AM") | AM, PM
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
S | Second | 00, 59, 60
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
% | Literal "%" character | %
|
||||
|
||||
Any other character will be interpreted as an invalid format and raise an error
|
||||
|
||||
## Examples
|
||||
|
||||
Without options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
|
||||
"19-08-26 01:52:06 PM"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
|
||||
"Mon, August 26 2019"
|
||||
|
||||
iex> Calendar.strftime(~U[2020-04-02 13:52:06.0Z], "%B %-d, %Y")
|
||||
"April 2, 2020"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c")
|
||||
"2019-08-26 13:52:06"
|
||||
|
||||
With options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c", preferred_datetime: "%H:%M:%S %d-%m-%y")
|
||||
"13:52:06 26-08-19"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%A",
|
||||
...> day_of_week_names: fn day_of_week ->
|
||||
...> {"segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
|
||||
...> "sexta-feira", "sábado", "domingo"}
|
||||
...> |> elem(day_of_week - 1)
|
||||
...> end
|
||||
...>)
|
||||
"segunda-feira"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%B",
|
||||
...> month_names: fn month ->
|
||||
...> {"январь", "февраль", "март", "апрель", "май", "июнь",
|
||||
...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"}
|
||||
...> |> elem(month - 1)
|
||||
...> end
|
||||
...>)
|
||||
"август"
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@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(
|
||||
string_format,
|
||||
date_or_time_or_datetime,
|
||||
options(user_options),
|
||||
[]
|
||||
)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp parse("", _datetime, _format_options, acc),
|
||||
do: Enum.reverse(acc)
|
||||
|
||||
defp parse("%" <> rest, datetime, format_options, acc),
|
||||
do: parse_modifiers(rest, nil, nil, {datetime, format_options, acc})
|
||||
|
||||
defp parse(<<char, rest::binary>>, datetime, format_options, acc),
|
||||
do: parse(rest, datetime, format_options, [char | acc])
|
||||
|
||||
defp parse_modifiers("-" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, "", parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("0" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, ?0, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("_" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, ?\s, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, default_pad(format), parser_data)
|
||||
end
|
||||
|
||||
# set default width if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, nil, pad, parser_data) do
|
||||
parse_modifiers(rest, default_width(format), pad, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(rest, width, pad, {datetime, format_options, acc}) do
|
||||
format_modifiers(rest, width, pad, datetime, format_options, acc)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour > 11 do
|
||||
format_options.am_pm_names.(:pm)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour <= 11 do
|
||||
format_options.am_pm_names.(:am)
|
||||
end
|
||||
|
||||
defp default_pad(format) when format in 'aAbBpPZ', do: ?\s
|
||||
defp default_pad(_format), do: ?0
|
||||
|
||||
defp default_width(format) when format in 'dHImMSy', do: 2
|
||||
defp default_width(?j), do: 3
|
||||
defp default_width(format) when format in 'Yz', do: 4
|
||||
defp default_width(_format), do: 0
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
defp format_modifiers("a" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.abbreviated_day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full name of day
|
||||
defp format_modifiers("A" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Abbreviated month name
|
||||
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime.month
|
||||
|> format_options.abbreviated_month_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full month name
|
||||
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date+time representation
|
||||
defp format_modifiers(
|
||||
"c" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_datetime_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_datetime within another preferred_datetime format"
|
||||
end
|
||||
|
||||
defp format_modifiers("c" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_datetime
|
||||
|> parse(datetime, %{format_options | preferred_datetime_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Microseconds
|
||||
defp format_modifiers("f" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
{microsecond, precision} = datetime.microsecond
|
||||
|
||||
result =
|
||||
microsecond
|
||||
|> Integer.to_string()
|
||||
|> String.pad_leading(6, "0")
|
||||
|> binary_part(0, max(precision, 1))
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# 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)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "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) |> String.upcase() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "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)
|
||||
|> String.downcase()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date (without time) representation
|
||||
defp format_modifiers(
|
||||
"x" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_date_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_date within another preferred_date format"
|
||||
end
|
||||
|
||||
defp format_modifiers("x" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_date
|
||||
|> parse(datetime, %{format_options | preferred_date_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred time (without date) representation
|
||||
defp format_modifiers(
|
||||
"X" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_time_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_time within another preferred_time format"
|
||||
end
|
||||
|
||||
defp format_modifiers("X" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_time
|
||||
|> parse(datetime, %{format_options | preferred_time_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# +hhmm/-hhmm time zone offset from UTC (empty string if naive)
|
||||
defp format_modifiers(
|
||||
"z" <> rest,
|
||||
width,
|
||||
pad,
|
||||
datetime = %{utc_offset: utc_offset, std_offset: std_offset},
|
||||
format_options,
|
||||
acc
|
||||
) do
|
||||
absolute_offset = abs(utc_offset + std_offset)
|
||||
|
||||
offset_number =
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers("z" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, ["" | acc])
|
||||
end
|
||||
|
||||
# Time zone abbreviation (empty string if naive)
|
||||
defp format_modifiers("Z" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Map.get(:zone_abbr, "") |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers(rest, _width, _pad, _datetime, _format_options, _acc) do
|
||||
{next, _rest} = String.next_grapheme(rest) || {"", ""}
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
defp do_pad_leading(0, _, acc), do: acc
|
||||
|
||||
defp do_pad_leading(count, padding, acc),
|
||||
do: do_pad_leading(count - 1, padding, [padding | acc])
|
||||
|
||||
defp options(user_options) do
|
||||
default_options = %{
|
||||
preferred_date: "%Y-%m-%d",
|
||||
preferred_time: "%H:%M:%S",
|
||||
preferred_datetime: "%Y-%m-%d %H:%M:%S",
|
||||
am_pm_names: fn
|
||||
:am -> "am"
|
||||
:pm -> "pm"
|
||||
end,
|
||||
month_names: fn month ->
|
||||
{"January", "February", "March", "April", "May", "June", "July", "August", "September",
|
||||
"October", "November", "December"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
day_of_week_names: fn day_of_week ->
|
||||
{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|
||||
|> elem(day_of_week - 1)
|
||||
end,
|
||||
abbreviated_month_names: fn month ->
|
||||
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
abbreviated_day_of_week_names: fn day_of_week ->
|
||||
{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} |> elem(day_of_week - 1)
|
||||
end,
|
||||
preferred_datetime_invoked: false,
|
||||
preferred_date_invoked: false,
|
||||
preferred_time_invoked: false
|
||||
}
|
||||
|
||||
Enum.reduce(user_options, default_options, fn {key, value}, acc ->
|
||||
if Map.has_key?(acc, key) do
|
||||
%{acc | key => value}
|
||||
else
|
||||
raise ArgumentError, "unknown option #{inspect(key)} given to Calendar.strftime/3"
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
+22
-293
@@ -83,70 +83,28 @@ defmodule Date do
|
||||
366
|
||||
iex> Enum.member?(range, ~D[2001-02-01])
|
||||
true
|
||||
iex> Enum.take(range, 3)
|
||||
[~D[2001-01-01], ~D[2001-01-02], ~D[2001-01-03]]
|
||||
iex> Enum.reduce(range, 0, fn _date, acc -> acc - 1 end)
|
||||
-366
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec range(Calendar.date(), Calendar.date()) :: Date.Range.t()
|
||||
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
|
||||
@spec range(Date.t(), Date.t()) :: Date.Range.t()
|
||||
def range(%Date{calendar: calendar} = first, %Date{calendar: calendar} = last) do
|
||||
{first_days, _} = to_iso_days(first)
|
||||
{last_days, _} = to_iso_days(last)
|
||||
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.16
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
range(first, first_days, last, last_days, calendar, step)
|
||||
end
|
||||
|
||||
def range(%{calendar: _, year: _, month: _, day: _}, %{calendar: _, year: _, month: _, day: _}) do
|
||||
raise ArgumentError, "both dates must have matching calendars"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a range of dates with a step.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> range = Date.range(~D[2001-01-01], ~D[2002-01-01], 2)
|
||||
iex> range
|
||||
#DateRange<~D[2001-01-01], ~D[2002-01-01], 2>
|
||||
iex> Enum.count(range)
|
||||
183
|
||||
iex> Enum.member?(range, ~D[2001-01-03])
|
||||
true
|
||||
iex> Enum.take(range, 3)
|
||||
[~D[2001-01-01], ~D[2001-01-03], ~D[2001-01-05]]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec range(Calendar.date(), Calendar.date(), step :: pos_integer | neg_integer) ::
|
||||
Date.Range.t()
|
||||
def range(%{calendar: calendar} = first, %{calendar: calendar} = last, step)
|
||||
when is_integer(step) and step != 0 do
|
||||
{first_days, _} = to_iso_days(first)
|
||||
{last_days, _} = to_iso_days(last)
|
||||
range(first, first_days, last, last_days, calendar, step)
|
||||
end
|
||||
|
||||
def range(
|
||||
%{calendar: _, year: _, month: _, day: _} = first,
|
||||
%{calendar: _, year: _, month: _, day: _} = last,
|
||||
step
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"both dates must have matching calendar and the step must be a " <>
|
||||
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
|
||||
end
|
||||
|
||||
defp range(first, first_days, last, last_days, calendar, step) do
|
||||
%Date.Range{
|
||||
first: %Date{calendar: calendar, year: first.year, month: first.month, day: first.day},
|
||||
last: %Date{calendar: calendar, year: last.year, month: last.month, day: last.day},
|
||||
first: first,
|
||||
last: last,
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
last_in_iso_days: last_days
|
||||
}
|
||||
end
|
||||
|
||||
def range(%Date{}, %Date{}) do
|
||||
raise ArgumentError, "both dates must have matching calendars"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current date in UTC.
|
||||
|
||||
@@ -266,33 +224,6 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new ISO date.
|
||||
|
||||
Expects all values to be integers. Returns `date` if each
|
||||
entry fits its appropriate range, raises if the date is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.new!(2000, 1, 1)
|
||||
~D[2000-01-01]
|
||||
iex> Date.new!(2000, 13, 1)
|
||||
** (ArgumentError) cannot build date, reason: :invalid_date
|
||||
iex> Date.new!(2000, 2, 29)
|
||||
~D[2000-02-29]
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Calendar.year(), Calendar.month(), Calendar.day(), Calendar.calendar()) :: t
|
||||
def new!(year, month, day, calendar \\ Calendar.ISO) do
|
||||
case new(year, month, day, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build date, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given date to a string according to its calendar.
|
||||
|
||||
@@ -315,7 +246,7 @@ defmodule Date do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Dates" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
The year parsed by this function is limited to four digits.
|
||||
|
||||
@@ -340,7 +271,7 @@ defmodule Date do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Dates" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Raises if the format is invalid.
|
||||
|
||||
@@ -365,7 +296,7 @@ defmodule Date do
|
||||
|
||||
@doc """
|
||||
Converts the given `date` to
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
By default, `Date.to_iso8601/2` returns dates formatted in the "extended"
|
||||
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
|
||||
@@ -467,45 +398,6 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian days to a `Date` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.from_gregorian_days(1)
|
||||
~D[0000-01-02]
|
||||
iex> Date.from_gregorian_days(730_485)
|
||||
~D[2000-01-01]
|
||||
iex> Date.from_gregorian_days(-1)
|
||||
~D[-0001-12-31]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_gregorian_days(integer(), Calendar.calendar()) :: t
|
||||
def from_gregorian_days(days, calendar \\ Calendar.ISO) when is_integer(days) do
|
||||
from_iso_days({days, 0}, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `date` struct to a number of gregorian days.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.to_gregorian_days(~D[0000-01-02])
|
||||
1
|
||||
iex> Date.to_gregorian_days(~D[2000-01-01])
|
||||
730_485
|
||||
iex> Date.to_gregorian_days(~N[2000-01-01 00:00:00])
|
||||
730_485
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_days(Calendar.date()) :: integer()
|
||||
def to_gregorian_days(date) do
|
||||
{days, _} = to_iso_days(date)
|
||||
days
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares two date structs.
|
||||
|
||||
@@ -542,8 +434,8 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
def compare(%{calendar: calendar1} = date1, %{calendar: calendar2} = date2) do
|
||||
if Calendar.compatible_calendars?(calendar1, calendar2) do
|
||||
def compare(date1, date2) do
|
||||
if Calendar.compatible_calendars?(date1.calendar, date2.calendar) do
|
||||
case {to_iso_days(date1), to_iso_days(date2)} do
|
||||
{first, second} when first > second -> :gt
|
||||
{first, second} when first < second -> :lt
|
||||
@@ -701,12 +593,11 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
|
||||
defp 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}}
|
||||
end
|
||||
|
||||
def to_iso_days(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||
defp to_iso_days(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||
calendar.naive_datetime_to_iso_days(year, month, day, 0, 0, 0, {0, 0})
|
||||
end
|
||||
|
||||
@@ -727,11 +618,6 @@ defmodule Date do
|
||||
calendar (the default), it is an integer from 1 to 7, where
|
||||
1 is Monday and 7 is Sunday.
|
||||
|
||||
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 calendar. Any other weekday may be given to.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.day_of_week(~D[2016-10-31])
|
||||
@@ -743,131 +629,13 @@ defmodule Date do
|
||||
iex> Date.day_of_week(~D[-0015-10-30])
|
||||
3
|
||||
|
||||
iex> Date.day_of_week(~D[2016-10-31], :sunday)
|
||||
2
|
||||
iex> Date.day_of_week(~D[2016-11-01], :sunday)
|
||||
3
|
||||
iex> Date.day_of_week(~N[2016-11-01 01:23:45], :sunday)
|
||||
3
|
||||
iex> Date.day_of_week(~D[-0015-10-30], :sunday)
|
||||
4
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec day_of_week(Calendar.date(), starting_on :: :default | atom) :: Calendar.day_of_week()
|
||||
def day_of_week(date, starting_on \\ :default)
|
||||
@spec day_of_week(Calendar.date()) :: Calendar.day()
|
||||
def day_of_week(date)
|
||||
|
||||
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}, starting_on) do
|
||||
{day_of_week, _first, _last} = calendar.day_of_week(year, month, day, starting_on)
|
||||
day_of_week
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the first day of the week for the given `date`.
|
||||
|
||||
If the day is already the first day of the week, it returns the
|
||||
day itself. For the built-in ISO calendar, the week starts on Monday.
|
||||
A weekday rather than `:default` can be given as `starting_on`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.beginning_of_week(~D[2020-07-11])
|
||||
~D[2020-07-06]
|
||||
iex> Date.beginning_of_week(~D[2020-07-06])
|
||||
~D[2020-07-06]
|
||||
iex> Date.beginning_of_week(~D[2020-07-11], :sunday)
|
||||
~D[2020-07-05]
|
||||
iex> Date.beginning_of_week(~D[2020-07-11], :saturday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.beginning_of_week(~N[2020-07-11 01:23:45])
|
||||
~D[2020-07-06]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec beginning_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
|
||||
def beginning_of_week(date, starting_on \\ :default)
|
||||
|
||||
def beginning_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
|
||||
|
||||
{year, month, day} =
|
||||
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
1 ->
|
||||
{year, month, day}
|
||||
|
||||
day_of_week ->
|
||||
Calendar.ISO.date_from_iso_days(iso_days - day_of_week + 1)
|
||||
end
|
||||
|
||||
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
||||
end
|
||||
|
||||
def beginning_of_week(%{calendar: calendar} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
|
||||
case calendar.day_of_week(year, month, day, starting_on) do
|
||||
{day_of_week, day_of_week, _} ->
|
||||
%Date{calendar: calendar, year: year, month: month, day: day}
|
||||
|
||||
{day_of_week, first_day_of_week, _} ->
|
||||
add(date, -(day_of_week - first_day_of_week))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the last day of the week for the given `date`.
|
||||
|
||||
If the day is already the last day of the week, it returns the
|
||||
day itself. For the built-in ISO calendar, the week ends on Sunday.
|
||||
A weekday rather than `:default` can be given as `starting_on`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.end_of_week(~D[2020-07-11])
|
||||
~D[2020-07-12]
|
||||
iex> Date.end_of_week(~D[2020-07-05])
|
||||
~D[2020-07-05]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :sunday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :sunday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :saturday)
|
||||
~D[2020-07-10]
|
||||
iex> Date.end_of_week(~N[2020-07-11 01:23:45])
|
||||
~D[2020-07-12]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec end_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
|
||||
def end_of_week(date, starting_on \\ :default)
|
||||
|
||||
def end_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
|
||||
|
||||
{year, month, day} =
|
||||
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
7 ->
|
||||
{year, month, day}
|
||||
|
||||
day_of_week ->
|
||||
Calendar.ISO.date_from_iso_days(iso_days + 7 - day_of_week)
|
||||
end
|
||||
|
||||
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
||||
end
|
||||
|
||||
def end_of_week(%{calendar: calendar} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
|
||||
case calendar.day_of_week(year, month, day, starting_on) do
|
||||
{day_of_week, _, day_of_week} ->
|
||||
%Date{calendar: calendar, year: year, month: month, day: day}
|
||||
|
||||
{day_of_week, _, last_day_of_week} ->
|
||||
add(date, last_day_of_week - day_of_week)
|
||||
end
|
||||
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||
calendar.day_of_week(year, month, day)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -971,45 +739,6 @@ defmodule Date do
|
||||
calendar.day_of_era(year, month, day)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the first day of the month for the given `date`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.beginning_of_month(~D[2000-01-31])
|
||||
~D[2000-01-01]
|
||||
iex> Date.beginning_of_month(~D[2000-01-01])
|
||||
~D[2000-01-01]
|
||||
iex> Date.beginning_of_month(~N[2000-01-31 01:23:45])
|
||||
~D[2000-01-01]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec beginning_of_month(Calendar.date()) :: t()
|
||||
def beginning_of_month(%{year: year, month: month, calendar: calendar}) do
|
||||
%Date{year: year, month: month, day: 1, calendar: calendar}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the last day of the month for the given `date`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.end_of_month(~D[2000-01-01])
|
||||
~D[2000-01-31]
|
||||
iex> Date.end_of_month(~D[2000-01-31])
|
||||
~D[2000-01-31]
|
||||
iex> Date.end_of_month(~N[2000-01-01 01:23:45])
|
||||
~D[2000-01-31]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec end_of_month(Calendar.date()) :: t()
|
||||
def end_of_month(%{year: year, month: month, calendar: calendar} = date) do
|
||||
day = Date.days_in_month(date)
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defimpl String.Chars do
|
||||
|
||||
@@ -2,13 +2,12 @@ defmodule Date.Range do
|
||||
@moduledoc """
|
||||
Returns an inclusive range between dates.
|
||||
|
||||
Ranges must be created with the `Date.range/2` or `Date.range/3` function.
|
||||
Ranges must be created with the `Date.range/2` function.
|
||||
|
||||
The following fields are public:
|
||||
|
||||
* `:first` - the initial date on the range
|
||||
* `:last` - the last date on the range
|
||||
* `:step` - (since v1.12.0) the step
|
||||
|
||||
The remaining fields are private and should not be accessed.
|
||||
"""
|
||||
@@ -17,33 +16,33 @@ defmodule Date.Range do
|
||||
first: Date.t(),
|
||||
last: Date.t(),
|
||||
first_in_iso_days: iso_days(),
|
||||
last_in_iso_days: iso_days(),
|
||||
step: pos_integer | neg_integer
|
||||
last_in_iso_days: iso_days()
|
||||
}
|
||||
|
||||
@typep iso_days() :: Calendar.iso_days()
|
||||
|
||||
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days, :step]
|
||||
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days]
|
||||
|
||||
defimpl Enumerable do
|
||||
def member?(%{first: %{calendar: calendar}} = range, %Date{calendar: calendar} = date) do
|
||||
%{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
first: first,
|
||||
last: last,
|
||||
first_in_iso_days: first_in_iso_days,
|
||||
last_in_iso_days: last_in_iso_days
|
||||
} = range
|
||||
|
||||
{days, _} = Date.to_iso_days(date)
|
||||
%{year: first_year, month: first_month, day: first_day} = first
|
||||
%{year: last_year, month: last_month, day: last_day} = last
|
||||
%{year: year, month: month, day: day} = date
|
||||
first = {first_year, first_month, first_day}
|
||||
last = {last_year, last_month, last_day}
|
||||
date = {year, month, day}
|
||||
|
||||
cond do
|
||||
empty?(range) ->
|
||||
{:ok, false}
|
||||
|
||||
first_days <= last_days ->
|
||||
{:ok, first_days <= days and days <= last_days and rem(days - first_days, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last_days <= days and days <= first_days and rem(days - first_days, step) == 0}
|
||||
if first_in_iso_days <= last_in_iso_days do
|
||||
{:ok, date >= first and date <= last}
|
||||
else
|
||||
{:ok, date >= last and date <= first}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,64 +50,64 @@ defmodule Date.Range do
|
||||
{:ok, false}
|
||||
end
|
||||
|
||||
def count(range) do
|
||||
{:ok, size(range)}
|
||||
def count(%{first_in_iso_days: first, last_in_iso_days: last}) do
|
||||
{:ok, abs(first - last) + 1}
|
||||
end
|
||||
|
||||
def slice(range) do
|
||||
%{
|
||||
first_in_iso_days: first,
|
||||
first: %{calendar: calendar},
|
||||
step: step
|
||||
last_in_iso_days: last,
|
||||
first: %{calendar: calendar}
|
||||
} = range
|
||||
|
||||
{:ok, size(range), &slice(first + &1 * step, step, &2, calendar)}
|
||||
if first <= last do
|
||||
{:ok, last - first + 1, &slice_asc(first + &1, &2, calendar)}
|
||||
else
|
||||
{:ok, first - last + 1, &slice_desc(first - &1, &2, calendar)}
|
||||
end
|
||||
end
|
||||
|
||||
defp slice(current, _step, 1, calendar) do
|
||||
[date_from_iso_days(current, calendar)]
|
||||
defp slice_asc(current, 1, calendar), do: [date_from_iso_days(current, calendar)]
|
||||
|
||||
defp slice_asc(current, remaining, calendar) do
|
||||
[date_from_iso_days(current, calendar) | slice_asc(current + 1, remaining - 1, calendar)]
|
||||
end
|
||||
|
||||
defp slice(current, step, remaining, calendar) do
|
||||
[
|
||||
date_from_iso_days(current, calendar)
|
||||
| slice(current + step, step, remaining - 1, calendar)
|
||||
]
|
||||
defp slice_desc(current, 1, calendar), do: [date_from_iso_days(current, calendar)]
|
||||
|
||||
defp slice_desc(current, remaining, calendar) do
|
||||
[date_from_iso_days(current, calendar) | slice_desc(current - 1, remaining - 1, calendar)]
|
||||
end
|
||||
|
||||
def reduce(range, acc, fun) do
|
||||
%{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
first: %{calendar: calendar},
|
||||
step: step
|
||||
first_in_iso_days: first_in_iso_days,
|
||||
last_in_iso_days: last_in_iso_days,
|
||||
first: %{calendar: calendar}
|
||||
} = range
|
||||
|
||||
reduce(first_days, last_days, acc, fun, step, calendar)
|
||||
up? = first_in_iso_days <= last_in_iso_days
|
||||
reduce(first_in_iso_days, last_in_iso_days, acc, fun, calendar, up?)
|
||||
end
|
||||
|
||||
defp reduce(_first_days, _last_days, {:halt, acc}, _fun, _step, _calendar) do
|
||||
defp reduce(_x, _y, {:halt, acc}, _fun, _calendar, _up?) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp reduce(first_days, last_days, {:suspend, acc}, fun, step, calendar) do
|
||||
{:suspended, acc, &reduce(first_days, last_days, &1, fun, step, calendar)}
|
||||
defp reduce(x, y, {:suspend, acc}, fun, calendar, up?) do
|
||||
{:suspended, acc, &reduce(x, y, &1, fun, calendar, up?)}
|
||||
end
|
||||
|
||||
defp reduce(first_days, last_days, {:cont, acc}, fun, step, calendar)
|
||||
when step > 0 and first_days <= last_days
|
||||
when step < 0 and first_days >= last_days do
|
||||
reduce(
|
||||
first_days + step,
|
||||
last_days,
|
||||
fun.(date_from_iso_days(first_days, calendar), acc),
|
||||
fun,
|
||||
step,
|
||||
calendar
|
||||
)
|
||||
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = true) when x <= y do
|
||||
reduce(x + 1, y, fun.(date_from_iso_days(x, calendar), acc), fun, calendar, up?)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _step, _calendar) do
|
||||
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = false) when x >= y do
|
||||
reduce(x - 1, y, fun.(date_from_iso_days(x, calendar), acc), fun, calendar, up?)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _calendar, _up) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
@@ -123,44 +122,11 @@ defmodule Date.Range do
|
||||
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step})
|
||||
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})
|
||||
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 empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step > 0 and first_days > last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days,
|
||||
step: step
|
||||
})
|
||||
when step < 0 and first_days < last_days,
|
||||
do: true
|
||||
|
||||
defp empty?(%Date.Range{}), do: false
|
||||
end
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
|
||||
def inspect(%Date.Range{first: first, last: last}, _) do
|
||||
"#DateRange<" <> inspect(first) <> ", " <> inspect(last) <> ">"
|
||||
end
|
||||
|
||||
def inspect(%Date.Range{first: first, last: last, step: step}, _) do
|
||||
"#DateRange<" <> inspect(first) <> ", " <> inspect(last) <> ", #{step}>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,14 +25,10 @@ defmodule DateTime do
|
||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||
for any other time zone.
|
||||
|
||||
Other time zone databases can also be configured. For example,
|
||||
two of the available options are:
|
||||
|
||||
* [`tz`](https://hexdocs.pm/tz/)
|
||||
* [`tzdata`](https://hexdocs.pm/tzdata/)
|
||||
|
||||
To use them, first make sure it is added as a dependency in `mix.exs`.
|
||||
It can then be configured either via configuration:
|
||||
Other time zone databases can also be configured. For example, to use the
|
||||
[tzdata](https://hexdocs.pm/tzdata/) database, 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, Tzdata.TimeZoneDatabase
|
||||
|
||||
@@ -40,7 +36,6 @@ defmodule DateTime do
|
||||
|
||||
Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)
|
||||
|
||||
See the proper names in the library installation instructions.
|
||||
"""
|
||||
|
||||
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
||||
@@ -77,7 +72,6 @@ defmodule DateTime do
|
||||
}
|
||||
|
||||
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current datetime in UTC.
|
||||
@@ -94,164 +88,12 @@ defmodule DateTime do
|
||||
System.os_time() |> from_unix!(:native, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a datetime from date and time structs.
|
||||
|
||||
It expects a time zone to put the `DateTime` in.
|
||||
If the time zone is not passed it will default to `"Etc/UTC"`,
|
||||
which always succeeds. Otherwise, the `DateTime` is checked against the time zone database
|
||||
given as `time_zone_database`. See the "Time zone database"
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
|
||||
{:ok, ~U[2016-05-24 13:26:08.003Z]}
|
||||
|
||||
When the datetime is ambiguous - for instance during changing from summer
|
||||
to winter time - the two possible valid datetimes are returned in a tuple.
|
||||
The first datetime is also the one which comes first chronologically, while
|
||||
the second one comes last.
|
||||
|
||||
iex> {:ambiguous, first_dt, second_dt} = DateTime.new(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> first_dt
|
||||
#DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen>
|
||||
iex> second_dt
|
||||
#DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
|
||||
|
||||
When there is a gap in wall time - for instance in spring when the clocks are
|
||||
turned forward - the latest valid datetime just before the gap and the first
|
||||
valid datetime just after the gap.
|
||||
|
||||
iex> {:gap, just_before, just_after} = DateTime.new(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> just_before
|
||||
#DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen>
|
||||
iex> just_after
|
||||
#DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
Most of the time there is one, and just one, valid datetime for a certain
|
||||
date and time in a certain time zone.
|
||||
|
||||
iex> {:ok, datetime} = DateTime.new(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> datetime
|
||||
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
|
||||
{:ok, t}
|
||||
| {:ambiguous, first_datetime :: t, second_datetime :: t}
|
||||
| {:gap, t, t}
|
||||
| {:error,
|
||||
:incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database}
|
||||
def new(
|
||||
date,
|
||||
time,
|
||||
time_zone \\ "Etc/UTC",
|
||||
time_zone_database \\ Calendar.get_time_zone_database()
|
||||
)
|
||||
|
||||
def new(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time, "Etc/UTC", _db) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime}
|
||||
end
|
||||
|
||||
def new(date, time, time_zone, time_zone_database) do
|
||||
with {:ok, naive_datetime} <- NaiveDateTime.new(date, time) do
|
||||
from_naive(naive_datetime, time_zone, time_zone_database)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a datetime from date and time structs, raising on errors.
|
||||
|
||||
It expects a time zone to put the `DateTime` in.
|
||||
If the time zone is not passed it will default to `"Etc/UTC"`,
|
||||
which always succeeds. Otherwise, the DateTime is checked against the time zone database
|
||||
given as `time_zone_database`. See the "Time zone database"
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.new!(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
|
||||
~U[2016-05-24 13:26:08.003Z]
|
||||
|
||||
When the datetime is ambiguous - for instance during changing from summer
|
||||
to winter time - an error will be raised.
|
||||
|
||||
iex> DateTime.new!(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot build datetime with ~D[2018-10-28] and ~T[02:30:00] because such instant is ambiguous in time zone Europe/Copenhagen as there is an overlap between #DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen> and #DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
|
||||
|
||||
When there is a gap in wall time - for instance in spring when the clocks are
|
||||
turned forward - an error will be raised.
|
||||
|
||||
iex> DateTime.new!(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot build datetime with ~D[2019-03-31] and ~T[02:30:00] because such instant does not exist in time zone Europe/Copenhagen as there is a gap between #DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen> and #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
Most of the time there is one, and just one, valid datetime for a certain
|
||||
date and time in a certain time zone.
|
||||
|
||||
iex> datetime = DateTime.new!(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> datetime
|
||||
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) :: t
|
||||
def new!(
|
||||
date,
|
||||
time,
|
||||
time_zone \\ "Etc/UTC",
|
||||
time_zone_database \\ Calendar.get_time_zone_database()
|
||||
)
|
||||
|
||||
def new!(date, time, time_zone, time_zone_database) do
|
||||
case new(date, time, time_zone, time_zone_database) do
|
||||
{:ok, datetime} ->
|
||||
datetime
|
||||
|
||||
{:ambiguous, dt1, dt2} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
|
||||
"instant is ambiguous in time zone #{time_zone} as there is an overlap " <>
|
||||
"between #{inspect(dt1)} and #{inspect(dt2)}"
|
||||
|
||||
{:gap, dt1, dt2} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
|
||||
"instant does not exist in time zone #{time_zone} as there is a gap " <>
|
||||
"between #{inspect(dt1)} and #{inspect(dt2)}"
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)}, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@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. Up to
|
||||
253402300799 seconds is supported.
|
||||
be converted to microseconds internally.
|
||||
|
||||
Unix times are always in UTC and therefore the DateTime
|
||||
will be returned in UTC.
|
||||
@@ -266,26 +108,14 @@ defmodule DateTime do
|
||||
iex> datetime
|
||||
~U[2015-05-25 13:26:08.868569Z]
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(253_402_300_799)
|
||||
iex> datetime
|
||||
~U[9999-12-31 23:59:59Z]
|
||||
|
||||
iex> {:error, :invalid_unix_time} = DateTime.from_unix(253_402_300_800)
|
||||
|
||||
The unit can also be an integer as in `t:System.time_unit/0`:
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(143_256_036_886_856, 1024)
|
||||
iex> datetime
|
||||
~U[6403-03-17 07:05:22.320312Z]
|
||||
|
||||
Negative Unix times are supported up to -377705116800 seconds:
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(-377_705_116_800)
|
||||
iex> datetime
|
||||
~U[-9999-01-01 00:00:00Z]
|
||||
|
||||
iex> {:error, :invalid_unix_time} = DateTime.from_unix(-377_705_116_801)
|
||||
|
||||
Negative Unix times are supported, up to -62167219200 seconds,
|
||||
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
|
||||
"""
|
||||
@spec from_unix(integer, :native | System.time_unit(), Calendar.calendar()) ::
|
||||
{:ok, t} | {:error, atom}
|
||||
@@ -365,9 +195,8 @@ defmodule DateTime do
|
||||
{:ok, ~U[2016-05-24 13:26:08.003Z]}
|
||||
|
||||
When the datetime is ambiguous - for instance during changing from summer
|
||||
to winter time - the two possible valid datetimes are returned in a tuple.
|
||||
The first datetime is also the one which comes first chronologically, while
|
||||
the second one comes last.
|
||||
to winter time - the two possible valid datetimes are returned. First the one
|
||||
that happens first, then the one that happens after.
|
||||
|
||||
iex> {:ambiguous, first_dt, second_dt} = DateTime.from_naive(~N[2018-10-28 02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> first_dt
|
||||
@@ -418,7 +247,7 @@ defmodule DateTime do
|
||||
Calendar.time_zone_database()
|
||||
) ::
|
||||
{:ok, t}
|
||||
| {:ambiguous, first_datetime :: t, second_datetime :: t}
|
||||
| {:ambiguous, t, t}
|
||||
| {:gap, t, t}
|
||||
| {:error,
|
||||
:incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database}
|
||||
@@ -892,14 +721,13 @@ defmodule DateTime do
|
||||
|
||||
@doc """
|
||||
Converts the given datetime to
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601) format.
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601) format.
|
||||
|
||||
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.
|
||||
|
||||
WARNING: the ISO 8601 datetime format does not contain the time zone nor
|
||||
its abbreviation, which means information is lost when converting to such
|
||||
@@ -931,31 +759,11 @@ defmodule DateTime do
|
||||
iex> DateTime.to_iso8601(dt, :basic)
|
||||
"20000229T230007-0400"
|
||||
|
||||
iex> dt = %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"}
|
||||
iex> DateTime.to_iso8601(dt, :extended, 3600)
|
||||
"2000-03-01T04:00:07+01:00"
|
||||
|
||||
iex> dt = %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"}
|
||||
iex> DateTime.to_iso8601(dt, :extended, 0)
|
||||
"2000-03-01T03:00:07+00:00"
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 3, day: 01, zone_abbr: "UTC",
|
||||
...> hour: 03, minute: 0, second: 7, microsecond: {0, 0},
|
||||
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
iex> DateTime.to_iso8601(dt, :extended, 0)
|
||||
"2000-03-01T03:00:07Z"
|
||||
|
||||
iex> {:ok, dt, offset} = DateTime.from_iso8601("2000-03-01T03:00:07Z")
|
||||
iex> "2000-03-01T03:00:07Z" = DateTime.to_iso8601(dt, :extended, offset)
|
||||
"""
|
||||
@spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t()
|
||||
def to_iso8601(datetime, format \\ :extended, offset \\ nil)
|
||||
@spec to_iso8601(Calendar.datetime(), :extended | :basic) :: String.t()
|
||||
def to_iso8601(datetime, format \\ :extended)
|
||||
|
||||
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, nil)
|
||||
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format)
|
||||
when format in [:extended, :basic] do
|
||||
%{
|
||||
year: year,
|
||||
@@ -970,56 +778,21 @@ defmodule DateTime do
|
||||
std_offset: std_offset
|
||||
} = datetime
|
||||
|
||||
datetime_to_string(year, month, day, 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) <>
|
||||
Calendar.ISO.offset_to_string(utc_offset, std_offset, time_zone, format)
|
||||
end
|
||||
|
||||
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_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
|
||||
"Z"
|
||||
end
|
||||
|
||||
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_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
|
||||
def to_iso8601(%{calendar: _} = datetime, format) when format in [:extended, :basic] do
|
||||
datetime
|
||||
|> convert!(Calendar.ISO)
|
||||
|> to_iso8601(format, offset)
|
||||
end
|
||||
|
||||
defp shift_by_offset(%{calendar: calendar} = datetime, offset) do
|
||||
total_offset = datetime.utc_offset + datetime.std_offset
|
||||
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
# Subtract total original offset in order to get UTC and add the new offset
|
||||
|> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|
||||
|> calendar.naive_datetime_from_iso_days()
|
||||
end
|
||||
|
||||
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)
|
||||
|> to_iso8601(format)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses the extended "Date and time of day" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Since ISO 8601 does not include the proper time zone, the given
|
||||
string will be converted to UTC and its offset in seconds will be
|
||||
@@ -1029,6 +802,9 @@ defmodule DateTime do
|
||||
As specified in the standard, the separator "T" may be omitted if
|
||||
desired as there is no ambiguity within this function.
|
||||
|
||||
The year parsed by this function is limited to four digits and,
|
||||
while ISO 8601 allows datetimes to specify 24:00:00 as the zero
|
||||
hour of the next day, this notation is not supported by Elixir.
|
||||
Note leap seconds are not supported by the built-in Calendar.ISO.
|
||||
|
||||
## Examples
|
||||
@@ -1091,93 +867,6 @@ defmodule DateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||
|
||||
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||
`DateTime.shift_zone/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.from_gregorian_seconds(1)
|
||||
~U[0000-01-01 00:00:01Z]
|
||||
iex> DateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
|
||||
~U[2020-05-01 00:26:31.005Z]
|
||||
iex> DateTime.from_gregorian_seconds(-1)
|
||||
~U[-0001-12-31 23:59:59Z]
|
||||
|
||||
"""
|
||||
@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
|
||||
)
|
||||
when is_integer(seconds) do
|
||||
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
||||
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days)
|
||||
|
||||
%DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 0000, month: 1, day: 1, zone_abbr: "UTC",
|
||||
...> hour: 0, minute: 0, second: 1, microsecond: {0, 0},
|
||||
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{1, 0}
|
||||
|
||||
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "UTC",
|
||||
...> hour: 0, minute: 26, second: 31, microsecond: {5000, 0},
|
||||
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "CET",
|
||||
...> hour: 1, minute: 26, second: 31, microsecond: {5000, 0},
|
||||
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(
|
||||
%{
|
||||
std_offset: std_offset,
|
||||
utc_offset: utc_offset,
|
||||
microsecond: {microsecond, _}
|
||||
} = datetime
|
||||
) do
|
||||
{days, day_fraction} =
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
|> apply_tz_offset(utc_offset + std_offset)
|
||||
|
||||
seconds_in_day = seconds_from_day_fraction(day_fraction)
|
||||
{days * @seconds_per_day + seconds_in_day, microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `datetime` to a string according to its calendar.
|
||||
|
||||
@@ -1578,12 +1267,6 @@ defmodule DateTime do
|
||||
}
|
||||
end
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
|
||||
do: parts_in_day
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
|
||||
do: div(parts_in_day * @seconds_per_day, parts_per_day)
|
||||
|
||||
defimpl String.Chars do
|
||||
def to_string(datetime) do
|
||||
%{
|
||||
|
||||
+168
-434
@@ -1,127 +1,16 @@
|
||||
defmodule Calendar.ISO do
|
||||
@moduledoc """
|
||||
The default calendar implementation, a Gregorian calendar following ISO 8601.
|
||||
A calendar implementation that follows to ISO 8601.
|
||||
|
||||
This calendar implements a proleptic Gregorian calendar and
|
||||
This calendar implements the proleptic Gregorian calendar and
|
||||
is therefore compatible with the calendar used in most countries
|
||||
today. The proleptic means the Gregorian rules for leap years are
|
||||
applied for all time, consequently the dates give different results
|
||||
before the year 1583 from when the Gregorian calendar was adopted.
|
||||
|
||||
## ISO 8601 compliance
|
||||
|
||||
The ISO 8601 specification is feature-rich, but allows applications
|
||||
to selectively implement most parts of it. The choices Elixir makes
|
||||
are catalogued below.
|
||||
|
||||
### Features
|
||||
|
||||
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.
|
||||
|
||||
By default Elixir only parses extended-formatted date/times. You can opt-in
|
||||
to parse basic-formatted date/times.
|
||||
|
||||
`NaiveDateTime.to_iso8601/2` and `DateTime.to_iso8601/2` allow you to produce
|
||||
either basic or extended formatted strings, and `Calendar.strftime/2` allows
|
||||
you to format datetimes however else you desire.
|
||||
|
||||
Elixir does not support reduced accuracy formats (for example, a date without
|
||||
the day component) nor decimal precisions in the lowest component (such as
|
||||
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
|
||||
|
||||
#### Examples
|
||||
|
||||
Elixir expects the extended format by default when parsing:
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("20150123T235007")
|
||||
{:error, :invalid_format}
|
||||
|
||||
Parsing can be restricted to basic if desired:
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("20150123T235007Z", :basic)
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("20150123T235007Z", :extended)
|
||||
{:error, :invalid_format}
|
||||
|
||||
Only calendar dates are supported in parsing; ordinal and week dates are not.
|
||||
|
||||
iex> Calendar.ISO.parse_date("2015-04-15")
|
||||
{:ok, {2015, 4, 15}}
|
||||
iex> Calendar.ISO.parse_date("2015-105")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_date("2015-W16")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_date("2015-W016-3")
|
||||
{:error, :invalid_format}
|
||||
|
||||
Years, months, days, hours, minutes, and seconds must be fully specified:
|
||||
|
||||
iex> Calendar.ISO.parse_date("2015-04-15")
|
||||
{:ok, {2015, 4, 15}}
|
||||
iex> Calendar.ISO.parse_date("2015-04")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_date("2015")
|
||||
{:error, :invalid_format}
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07.0123456")
|
||||
{:ok, {23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_time("23:50")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23")
|
||||
{:error, :invalid_format}
|
||||
|
||||
### Extensions
|
||||
|
||||
The parser and formatter adopt one ISO 8601 extension: extended year notation.
|
||||
|
||||
This allows dates to be prefixed with a `+` or `-` sign, extending the range of
|
||||
expressible years from the default (`0000..9999`) to `-9999..9999`. Elixir still
|
||||
restricts years in this format to four digits.
|
||||
|
||||
#### Examples
|
||||
|
||||
iex> Calendar.ISO.parse_date("-2015-01-23")
|
||||
{:ok, {-2015, 1, 23}}
|
||||
iex> Calendar.ISO.parse_date("+2015-01-23")
|
||||
{:ok, {2015, 1, 23}}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("-2015-01-23 23:50:07")
|
||||
{:ok, {-2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("+2015-01-23 23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("-2015-01-23 23:50:07Z")
|
||||
{:ok, {-2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
iex> Calendar.ISO.parse_utc_datetime("+2015-01-23 23:50:07Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
|
||||
### Additions
|
||||
|
||||
ISO 8601 does not allow a whitespace instead of `T` as a separator
|
||||
between date and times, both when parsing and formatting.
|
||||
This is a common enough representation, Elixir allows it during parsing.
|
||||
|
||||
The formatting of dates in `NaiveDateTime.to_iso8601/1` and `DateTime.to_iso8601/1`
|
||||
do produce specification-compliant string representations using the `T` separator.
|
||||
|
||||
#### Examples
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:07.0123456Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}, 0}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.0123456Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}, 0}
|
||||
|
||||
Note that while ISO 8601 allows times and datetimes to specify
|
||||
24:00:00 as the zero hour of the next day, this notation is not
|
||||
supported by Elixir.
|
||||
"""
|
||||
|
||||
@behaviour Calendar
|
||||
@@ -155,7 +44,6 @@ defmodule Calendar.ISO do
|
||||
@type hour :: 0..23
|
||||
@type minute :: 0..59
|
||||
@type second :: 0..59
|
||||
@type weekday :: :monday | :tuesday | :wednesday | :thursday | :friday | :saturday | :sunday
|
||||
|
||||
@typedoc """
|
||||
Microseconds with stored precision.
|
||||
@@ -183,10 +71,7 @@ defmodule Calendar.ISO do
|
||||
@microseconds_per_second 1_000_000
|
||||
@parts_per_day @seconds_per_day * @microseconds_per_second
|
||||
|
||||
@datetime_seps [?\s, ?T]
|
||||
@ext_date_sep ?-
|
||||
@ext_time_sep ?:
|
||||
|
||||
@sep [?\s, ?T]
|
||||
@days_per_nonleap_year 365
|
||||
@days_per_leap_year 366
|
||||
|
||||
@@ -195,11 +80,10 @@ defmodule Calendar.ISO do
|
||||
# on ~D[0001-01-01] which is 366 days later.
|
||||
@iso_epoch 366
|
||||
|
||||
[match_basic_date, match_ext_date, guard_date, read_date] =
|
||||
[match_date, guard_date, read_date] =
|
||||
quote do
|
||||
[
|
||||
<<y1, y2, y3, y4, m1, m2, d1, d2>>,
|
||||
<<y1, y2, y3, y4, @ext_date_sep, m1, m2, @ext_date_sep, d1, d2>>,
|
||||
<<y1, y2, y3, y4, ?-, m1, m2, ?-, d1, d2>>,
|
||||
y1 >= ?0 and y1 <= ?9 and y2 >= ?0 and y2 <= ?9 and y3 >= ?0 and y3 <= ?9 and y4 >= ?0 and
|
||||
y4 <= ?9 and m1 >= ?0 and m1 <= ?9 and m2 >= ?0 and m2 <= ?9 and d1 >= ?0 and d1 <= ?9 and
|
||||
d2 >= ?0 and d2 <= ?9,
|
||||
@@ -211,11 +95,10 @@ defmodule Calendar.ISO do
|
||||
]
|
||||
end
|
||||
|
||||
[match_basic_time, match_ext_time, guard_time, read_time] =
|
||||
[match_time, guard_time, read_time] =
|
||||
quote do
|
||||
[
|
||||
<<h1, h2, i1, i2, s1, s2>>,
|
||||
<<h1, h2, @ext_time_sep, i1, i2, @ext_time_sep, s1, s2>>,
|
||||
<<h1, h2, ?:, i1, i2, ?:, s1, s2>>,
|
||||
h1 >= ?0 and h1 <= ?9 and h2 >= ?0 and h2 <= ?9 and i1 >= ?0 and i1 <= ?9 and i2 >= ?0 and
|
||||
i2 <= ?9 and s1 >= ?0 and s1 <= ?9 and s2 >= ?0 and s2 <= ?9,
|
||||
{
|
||||
@@ -244,69 +127,49 @@ defmodule Calendar.ISO do
|
||||
defguardp is_std_offset(offset) when is_integer(offset)
|
||||
|
||||
@doc """
|
||||
Parses a time `string` in the `:extended` format.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
Parses a time string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07Z")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_time("T23:50:07Z")
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_time("23:50:07,0123456")
|
||||
{:ok, {23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.0123456")
|
||||
{:ok, {23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.123Z")
|
||||
{:ok, {23, 50, 7, {123000, 3}}}
|
||||
|
||||
iex> Calendar.ISO.parse_time("2015:01:23 23-50-07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:07A")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:07.")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_time("23:50:61")
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_time("T" <> string) when is_binary(string),
|
||||
do: do_parse_time(string)
|
||||
|
||||
def parse_time(string) when is_binary(string),
|
||||
do: parse_time(string, :extended)
|
||||
do: do_parse_time(string)
|
||||
|
||||
@doc """
|
||||
Parses a time `string` according to a given `format`.
|
||||
|
||||
The `format` can either be `:basic` or `:extended`.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_time("235007", :basic)
|
||||
{:ok, {23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_time("235007", :extended)
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def parse_time("T" <> string, format) when is_binary(string),
|
||||
do: do_parse_time(string, format)
|
||||
|
||||
def parse_time(string, format) when is_binary(string),
|
||||
do: do_parse_time(string, format)
|
||||
|
||||
defp do_parse_time(<<unquote(match_basic_time), rest::binary>>, :basic)
|
||||
when unquote(guard_time) do
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_time(hour, minute, second, rest)
|
||||
end
|
||||
|
||||
defp do_parse_time(<<unquote(match_ext_time), rest::binary>>, :extended)
|
||||
when unquote(guard_time) do
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_time(hour, minute, second, rest)
|
||||
end
|
||||
|
||||
defp do_parse_time(_, _) do
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
|
||||
defp parse_formatted_time(hour, minute, second, rest) do
|
||||
with {microsecond, rest} <- parse_microsecond(rest),
|
||||
defp do_parse_time(string) do
|
||||
with <<unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{_offset, ""} <- parse_offset(rest) do
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
|
||||
if valid_time?(hour, minute, second, microsecond) do
|
||||
{:ok, {hour, minute, second, microsecond}}
|
||||
else
|
||||
@@ -318,16 +181,14 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a date `string` in the `:extended` format.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
Parses a date string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_date("2015-01-23")
|
||||
{:ok, {2015, 1, 23}}
|
||||
|
||||
iex> Calendar.ISO.parse_date("-2015-01-23")
|
||||
{:ok, {-2015, 1, 23}}
|
||||
iex> Calendar.ISO.parse_date("2015:01:23")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_date("2015-01-32")
|
||||
@@ -336,142 +197,91 @@ defmodule Calendar.ISO do
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_date("-" <> string) when is_binary(string),
|
||||
do: parse_date(string, -1)
|
||||
|
||||
def parse_date(string) when is_binary(string),
|
||||
do: parse_date(string, :extended)
|
||||
do: parse_date(string, 1)
|
||||
|
||||
@doc """
|
||||
Parses a date `string` according to a given `format`.
|
||||
defp parse_date(string, multiplier) do
|
||||
with unquote(match_date) <- string, true <- unquote(guard_date) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
year = multiplier * year
|
||||
|
||||
The `format` can either be `:basic` or `:extended`.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_date("20150123", :basic)
|
||||
{:ok, {2015, 1, 23}}
|
||||
iex> Calendar.ISO.parse_date("20150123", :extended)
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def parse_date("-" <> string, format) when is_binary(string),
|
||||
do: do_parse_date(string, -1, format)
|
||||
|
||||
def parse_date("+" <> string, format) when is_binary(string),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
def parse_date(string, format) when is_binary(string),
|
||||
do: do_parse_date(string, 1, format)
|
||||
|
||||
defp do_parse_date(unquote(match_basic_date), multiplier, :basic) when unquote(guard_date) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
parse_formatted_date(year, month, day, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_date(unquote(match_ext_date), multiplier, :extended) when unquote(guard_date) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
parse_formatted_date(year, month, day, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_date(_, _, _) do
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
|
||||
defp parse_formatted_date(year, month, day, multiplier) do
|
||||
year = multiplier * year
|
||||
|
||||
if valid_date?(year, month, day) do
|
||||
{:ok, {year, month, day}}
|
||||
if valid_date?(year, month, day) do
|
||||
{:ok, {year, month, day}}
|
||||
else
|
||||
{:error, :invalid_date}
|
||||
end
|
||||
else
|
||||
{:error, :invalid_date}
|
||||
_ ->
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a naive datetime `string` in the `:extended` format.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
Parses a naive datetime string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07Z")
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07-02:30")
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.0")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 1}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07,0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07.0123456")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {12345, 6}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23P23:50:07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015:01:23 23-50-07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:07A")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23 23:50:61")
|
||||
{:error, :invalid_time}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-32 23:50:07")
|
||||
{:error, :invalid_date}
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123+02:30")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123+00:00")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-02:30")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {123000, 3}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-00:00")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-00:60")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_naive_datetime("2015-01-23T23:50:07.123-24:00")
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_naive_datetime("-" <> string) when is_binary(string),
|
||||
do: parse_naive_datetime(string, -1)
|
||||
|
||||
def parse_naive_datetime(string) when is_binary(string),
|
||||
do: parse_naive_datetime(string, :extended)
|
||||
do: parse_naive_datetime(string, 1)
|
||||
|
||||
@doc """
|
||||
Parses a naive datetime `string` according to a given `format`.
|
||||
|
||||
The `format` can either be `:basic` or `:extended`.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_naive_datetime("20150123 235007", :basic)
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}}
|
||||
iex> Calendar.ISO.parse_naive_datetime("20150123 235007", :extended)
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def parse_naive_datetime("-" <> string, format) when is_binary(string),
|
||||
do: do_parse_naive_datetime(string, -1, format)
|
||||
|
||||
def parse_naive_datetime("+" <> string, format) when is_binary(string),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
def parse_naive_datetime(string, format) when is_binary(string),
|
||||
do: do_parse_naive_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_naive_datetime(
|
||||
<<unquote(match_basic_date), datetime_sep, unquote(match_basic_time), rest::binary>>,
|
||||
multiplier,
|
||||
:basic
|
||||
)
|
||||
when unquote(guard_date) and datetime_sep in @datetime_seps and unquote(guard_time) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_naive_datetime(year, month, day, hour, minute, second, rest, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_naive_datetime(
|
||||
<<unquote(match_ext_date), datetime_sep, unquote(match_ext_time), rest::binary>>,
|
||||
multiplier,
|
||||
:extended
|
||||
)
|
||||
when unquote(guard_date) and datetime_sep in @datetime_seps and unquote(guard_time) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_naive_datetime(year, month, day, hour, minute, second, rest, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_naive_datetime(_, _, _) do
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
|
||||
defp parse_formatted_naive_datetime(year, month, day, hour, minute, second, rest, multiplier) do
|
||||
year = multiplier * year
|
||||
|
||||
with {microsecond, rest} <- parse_microsecond(rest),
|
||||
defp parse_naive_datetime(string, multiplier) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{_offset, ""} <- parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
year = multiplier * year
|
||||
|
||||
cond do
|
||||
not valid_date?(year, month, day) ->
|
||||
{:error, :invalid_date}
|
||||
@@ -488,85 +298,54 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses a UTC datetime `string` in the `:extended` format.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
Parses a UTC datetime string.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:07Z")
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07Z")
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:07+02:30")
|
||||
{:ok, {2015, 1, 23, 21, 20, 7, {0, 0}}, 9000}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123+02:30")
|
||||
{:ok, {2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:07")
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07,123+02:30")
|
||||
{:ok, {2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("-2015-01-23T23:50:07Z")
|
||||
{:ok, {-2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("-2015-01-23T23:50:07,123+02:30")
|
||||
{:ok, {-2015, 1, 23, 21, 20, 7, {123000, 3}}, 9000}
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23P23:50:07")
|
||||
{:error, :invalid_format}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07")
|
||||
{:error, :missing_offset}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23 23:50:61")
|
||||
{:error, :invalid_time}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-32 23:50:07")
|
||||
{:error, :invalid_date}
|
||||
iex> Calendar.ISO.parse_utc_datetime("2015-01-23T23:50:07.123-00:00")
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@impl true
|
||||
def parse_utc_datetime("-" <> string) when is_binary(string),
|
||||
do: parse_utc_datetime(string, -1)
|
||||
|
||||
def parse_utc_datetime(string) when is_binary(string),
|
||||
do: parse_utc_datetime(string, :extended)
|
||||
do: parse_utc_datetime(string, 1)
|
||||
|
||||
@doc """
|
||||
Parses a UTC datetime `string` according to a given `format`.
|
||||
|
||||
The `format` can either be `:basic` or `:extended`.
|
||||
|
||||
For more information on supported strings, see how this
|
||||
module implements [ISO 8601](#module-iso-8601-compliance).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.parse_utc_datetime("20150123 235007Z", :basic)
|
||||
{:ok, {2015, 1, 23, 23, 50, 7, {0, 0}}, 0}
|
||||
iex> Calendar.ISO.parse_utc_datetime("20150123 235007Z", :extended)
|
||||
{:error, :invalid_format}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def parse_utc_datetime("-" <> string, format) when is_binary(string),
|
||||
do: do_parse_utc_datetime(string, -1, format)
|
||||
|
||||
def parse_utc_datetime("+" <> string, format) when is_binary(string),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
def parse_utc_datetime(string, format) when is_binary(string),
|
||||
do: do_parse_utc_datetime(string, 1, format)
|
||||
|
||||
defp do_parse_utc_datetime(
|
||||
<<unquote(match_basic_date), datetime_sep, unquote(match_basic_time), rest::binary>>,
|
||||
multiplier,
|
||||
:basic
|
||||
)
|
||||
when unquote(guard_date) and datetime_sep in @datetime_seps and unquote(guard_time) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_utc_datetime(year, month, day, hour, minute, second, rest, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_utc_datetime(
|
||||
<<unquote(match_ext_date), datetime_sep, unquote(match_ext_time), rest::binary>>,
|
||||
multiplier,
|
||||
:extended
|
||||
)
|
||||
when unquote(guard_date) and datetime_sep in @datetime_seps and unquote(guard_time) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
parse_formatted_utc_datetime(year, month, day, hour, minute, second, rest, multiplier)
|
||||
end
|
||||
|
||||
defp do_parse_utc_datetime(_, _, _) do
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
|
||||
defp parse_formatted_utc_datetime(year, month, day, hour, minute, second, rest, multiplier) do
|
||||
year = multiplier * year
|
||||
|
||||
with {microsecond, rest} <- parse_microsecond(rest),
|
||||
defp parse_utc_datetime(string, multiplier) do
|
||||
with <<unquote(match_date), sep, unquote(match_time), rest::binary>> <- string,
|
||||
true <- unquote(guard_date) and sep in @sep and unquote(guard_time),
|
||||
{microsecond, rest} <- parse_microsecond(rest),
|
||||
{offset, ""} <- parse_offset(rest) do
|
||||
{year, month, day} = unquote(read_date)
|
||||
{hour, minute, second} = unquote(read_time)
|
||||
year = multiplier * year
|
||||
|
||||
cond do
|
||||
not valid_date?(year, month, day) ->
|
||||
{:error, :invalid_date}
|
||||
@@ -596,7 +375,8 @@ defmodule Calendar.ISO do
|
||||
{:ok, {year, month, day, hour, minute, second, microsecond}, offset}
|
||||
end
|
||||
else
|
||||
_ -> {:error, :invalid_format}
|
||||
_ ->
|
||||
{:error, :invalid_format}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -713,12 +493,12 @@ defmodule Calendar.ISO do
|
||||
total_microseconds = divide_by_parts_per_day(parts_in_day, parts_per_day)
|
||||
|
||||
{hours, rest_microseconds1} =
|
||||
div_rem(total_microseconds, @seconds_per_hour * @microseconds_per_second)
|
||||
div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)
|
||||
|
||||
{minutes, rest_microseconds2} =
|
||||
div_rem(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
|
||||
div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
|
||||
|
||||
{seconds, microseconds} = div_rem(rest_microseconds2, @microseconds_per_second)
|
||||
{seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
|
||||
{hours, minutes, seconds, {microseconds, 6}}
|
||||
end
|
||||
|
||||
@@ -753,7 +533,7 @@ defmodule Calendar.ISO do
|
||||
{year, month, day_in_month + 1}
|
||||
end
|
||||
|
||||
defp div_rem(int1, int2) do
|
||||
defp div_mod(int1, int2) do
|
||||
div = div(int1, int2)
|
||||
rem = int1 - div * int2
|
||||
|
||||
@@ -839,83 +619,43 @@ defmodule Calendar.ISO do
|
||||
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on v1.15
|
||||
@doc false
|
||||
def day_of_week(year, month, day) do
|
||||
day_of_week(year, month, day, :default) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the day of the week from the given `year`, `month`, and `day`.
|
||||
|
||||
It is an integer from 1 to 7, where 1 is the given `starting_on` weekday.
|
||||
For example, if `starting_on` is set to `:monday`, then 1 is Monday and
|
||||
7 is Sunday.
|
||||
|
||||
`starting_on` can also be `:default`, which is equivalent to `:monday`.
|
||||
It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :monday)
|
||||
{1, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1, :monday)
|
||||
{2, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2, :monday)
|
||||
{3, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3, :monday)
|
||||
{4, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4, :monday)
|
||||
{5, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5, :monday)
|
||||
{6, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6, :monday)
|
||||
{7, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31, :monday)
|
||||
{4, 1, 7}
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :sunday)
|
||||
{2, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1, :sunday)
|
||||
{3, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2, :sunday)
|
||||
{4, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3, :sunday)
|
||||
{5, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4, :sunday)
|
||||
{6, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5, :sunday)
|
||||
{7, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6, :sunday)
|
||||
{1, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31, :sunday)
|
||||
{5, 1, 7}
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :saturday)
|
||||
{3, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31)
|
||||
1
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1)
|
||||
2
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2)
|
||||
3
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3)
|
||||
4
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4)
|
||||
5
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5)
|
||||
6
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6)
|
||||
7
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31)
|
||||
4
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec day_of_week(year, month, day, :default | weekday) :: {day_of_week(), 1, 7}
|
||||
@doc since: "1.4.0"
|
||||
@spec day_of_week(year, month, day) :: day_of_week()
|
||||
@impl true
|
||||
def day_of_week(year, month, day, starting_on) do
|
||||
iso_days = date_to_iso_days(year, month, day)
|
||||
{iso_days_to_day_of_week(iso_days, starting_on), 1, 7}
|
||||
def day_of_week(year, month, day) do
|
||||
date_to_iso_days(year, month, day)
|
||||
|> iso_days_to_day_of_week()
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
Integer.mod(iso_days + day_of_week_offset(starting_on), 7) + 1
|
||||
defp iso_days_to_day_of_week(iso_days) do
|
||||
Integer.mod(iso_days + 5, 7) + 1
|
||||
end
|
||||
|
||||
defp day_of_week_offset(:default), do: 5
|
||||
defp day_of_week_offset(:wednesday), do: 3
|
||||
defp day_of_week_offset(:thursday), do: 2
|
||||
defp day_of_week_offset(:friday), do: 1
|
||||
defp day_of_week_offset(:saturday), do: 0
|
||||
defp day_of_week_offset(:sunday), do: 6
|
||||
defp day_of_week_offset(:monday), do: 5
|
||||
defp day_of_week_offset(:tuesday), do: 4
|
||||
|
||||
@doc """
|
||||
Calculates the day of the year from the given `year`, `month`, and `day`.
|
||||
|
||||
@@ -1288,7 +1028,9 @@ defmodule Calendar.ISO do
|
||||
@doc """
|
||||
Determines if the date given is valid according to the proleptic Gregorian calendar.
|
||||
|
||||
Leap seconds are not supported by the built-in Calendar.ISO.
|
||||
Note that while ISO 8601 allows times to specify 24:00:00 as the
|
||||
zero hour of the next day, this notation is not supported by Elixir.
|
||||
Leap seconds are not supported as well by the built-in Calendar.ISO.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1418,14 +1160,6 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def gregorian_seconds_to_iso_days(seconds, microsecond) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
microseconds_in_day = rest_seconds * @microseconds_per_second + microsecond
|
||||
day_fraction = {microseconds_in_day, @parts_per_day}
|
||||
{days, day_fraction}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_unit({days, {parts, ppd}}, unit) do
|
||||
day_microseconds = days * @parts_per_day
|
||||
@@ -1578,7 +1312,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
defp iso_seconds_to_datetime(seconds) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
{days, rest_seconds} = div_mod(seconds, @seconds_per_day)
|
||||
|
||||
date = date_from_iso_days(days)
|
||||
time = seconds_to_time(rest_seconds)
|
||||
@@ -1586,8 +1320,8 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
defp seconds_to_time(seconds) when seconds in 0..@last_second_of_the_day do
|
||||
{hour, rest_seconds} = div_rem(seconds, @seconds_per_hour)
|
||||
{minute, second} = div_rem(rest_seconds, @seconds_per_minute)
|
||||
{hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
|
||||
{minute, second} = div_mod(rest_seconds, @seconds_per_minute)
|
||||
|
||||
{hour, minute, second}
|
||||
end
|
||||
|
||||
@@ -78,8 +78,6 @@ defmodule NaiveDateTime do
|
||||
microsecond: Calendar.microsecond()
|
||||
}
|
||||
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current naive datetime in UTC.
|
||||
|
||||
@@ -244,61 +242,6 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new ISO naive datetime.
|
||||
|
||||
Expects all values to be integers. Returns `naive_datetime`
|
||||
if each entry fits its appropriate range, raises if
|
||||
time or date is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 0, 0, 0)
|
||||
~N[2000-01-01 00:00:00]
|
||||
iex> NaiveDateTime.new!(2000, 2, 29, 0, 0, 0)
|
||||
~N[2000-02-29 00:00:00]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1})
|
||||
~N[2000-01-01 23:59:59.0]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, 999_999)
|
||||
~N[2000-01-01 23:59:59.999999]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1}, Calendar.ISO)
|
||||
~N[2000-01-01 23:59:59.0]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 24, 59, 59, 999_999)
|
||||
** (ArgumentError) cannot build naive datetime, reason: :invalid_time
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(
|
||||
Calendar.year(),
|
||||
Calendar.month(),
|
||||
Calendar.day(),
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
|
||||
def new!(year, month, day, hour, minute, second, microsecond, calendar) do
|
||||
case new(year, month, day, hour, minute, second, microsecond, calendar) do
|
||||
{:ok, naive_datetime} ->
|
||||
naive_datetime
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build naive datetime, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a naive datetime from date and time structs.
|
||||
|
||||
@@ -329,24 +272,6 @@ defmodule NaiveDateTime do
|
||||
{:ok, naive_datetime}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a naive datetime from date and time structs.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.new!(~D[2010-01-13], ~T[23:00:07.005])
|
||||
~N[2010-01-13 23:00:07.005]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Date.t(), Time.t()) :: t
|
||||
def new!(date, time)
|
||||
|
||||
def new!(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time) do
|
||||
{:ok, naive_datetime} = new(date, time)
|
||||
naive_datetime
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a specified amount of time to a `NaiveDateTime`.
|
||||
|
||||
@@ -598,7 +523,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Date and time of day" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Time zone offset may be included in the string but they will be
|
||||
simply discarded as such information is not included in naive date
|
||||
@@ -607,6 +532,9 @@ defmodule NaiveDateTime do
|
||||
As specified in the standard, the separator "T" may be omitted if
|
||||
desired as there is no ambiguity within this function.
|
||||
|
||||
The year parsed by this function is limited to four digits and,
|
||||
while ISO 8601 allows datetimes to specify 24:00:00 as the zero
|
||||
hour of the next day, this notation is not supported by Elixir.
|
||||
Note leap seconds are not supported by the built-in Calendar.ISO.
|
||||
|
||||
## Examples
|
||||
@@ -673,7 +601,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Date and time of day" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Raises if the format is invalid.
|
||||
|
||||
@@ -701,7 +629,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
@doc """
|
||||
Converts the given naive datetime to
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
By default, `NaiveDateTime.to_iso8601/2` returns naive datetimes formatted in the "extended"
|
||||
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
|
||||
@@ -841,82 +769,6 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `NaiveDateTime` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.from_gregorian_seconds(1)
|
||||
~N[0000-01-01 00:00:01]
|
||||
iex> NaiveDateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
|
||||
~N[2020-05-01 00:26:31.005]
|
||||
iex> NaiveDateTime.from_gregorian_seconds(-1)
|
||||
~N[-0001-12-31 23:59:59]
|
||||
|
||||
"""
|
||||
@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
|
||||
)
|
||||
when is_integer(seconds) do
|
||||
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
||||
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days)
|
||||
|
||||
%NaiveDateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_gregorian_seconds(~N[0000-01-01 00:00:01])
|
||||
{1, 0}
|
||||
iex> NaiveDateTime.to_gregorian_seconds(~N[2020-05-01 00:26:31.005])
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}) do
|
||||
{days, day_fraction} =
|
||||
calendar.naive_datetime_to_iso_days(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
{microsecond, precision}
|
||||
)
|
||||
|
||||
seconds_in_day = seconds_from_day_fraction(day_fraction)
|
||||
{days * @seconds_per_day + seconds_in_day, microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares two `NaiveDateTime` structs.
|
||||
|
||||
@@ -1061,12 +913,6 @@ defmodule NaiveDateTime do
|
||||
|
||||
## Helpers
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
|
||||
do: parts_in_day
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
|
||||
do: div(parts_in_day * @seconds_per_day, parts_per_day)
|
||||
|
||||
# Keep it multiline for proper function clause errors.
|
||||
defp to_iso_days(%{
|
||||
calendar: calendar,
|
||||
|
||||
+11
-101
@@ -46,7 +46,6 @@ defmodule Time do
|
||||
}
|
||||
|
||||
@parts_per_day 86_400_000_000
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current time in UTC.
|
||||
@@ -140,44 +139,6 @@ defmodule Time do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new time.
|
||||
|
||||
Expects all values to be integers. Returns `time` if each
|
||||
entry fits its appropriate range, raises if the time is invalid.
|
||||
|
||||
Microseconds can also be given with a precision, which must be an
|
||||
integer between 0 and 6.
|
||||
|
||||
The built-in calendar does not support leap seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.new!(0, 0, 0, 0)
|
||||
~T[00:00:00.000000]
|
||||
iex> Time.new!(23, 59, 59, 999_999)
|
||||
~T[23:59:59.999999]
|
||||
iex> Time.new!(24, 59, 59, 999_999)
|
||||
** (ArgumentError) cannot build time, reason: :invalid_time
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||
case new(hour, minute, second, microsecond, calendar) do
|
||||
{:ok, time} ->
|
||||
time
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build time, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `time` to a string.
|
||||
|
||||
@@ -211,7 +172,7 @@ defmodule Time do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Local time" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Time zone offset may be included in the string but they will be
|
||||
simply discarded as such information is not included in times.
|
||||
@@ -219,6 +180,12 @@ defmodule Time do
|
||||
As specified in the standard, the separator "T" may be omitted if
|
||||
desired as there is no ambiguity within this function.
|
||||
|
||||
Time representations with reduced accuracy are not supported.
|
||||
|
||||
Note that while ISO 8601 allows times to specify 24:00:00 as the
|
||||
zero hour of the next day, this notation is not supported by Elixir.
|
||||
Leap seconds are not supported as well by the built-in Calendar.ISO.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_iso8601("23:50:07")
|
||||
@@ -257,7 +224,7 @@ defmodule Time do
|
||||
|
||||
@doc """
|
||||
Parses the extended "Local time" format described by
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
Raises if the format is invalid.
|
||||
|
||||
@@ -284,7 +251,7 @@ defmodule Time do
|
||||
|
||||
@doc """
|
||||
Converts the given time to
|
||||
[ISO 8601:2019](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
|
||||
|
||||
By default, `Time.to_iso8601/2` returns times formatted in the "extended"
|
||||
format, for human readability. It also supports the "basic" format through
|
||||
@@ -391,63 +358,6 @@ defmodule Time do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of seconds after midnight to a `Time` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_seconds_after_midnight(10_000)
|
||||
~T[02:46:40]
|
||||
iex> Time.from_seconds_after_midnight(30_000, {5000, 3})
|
||||
~T[08:20:00.005]
|
||||
iex> Time.from_seconds_after_midnight(-1)
|
||||
~T[23:59:59]
|
||||
iex> Time.from_seconds_after_midnight(100_000)
|
||||
~T[03:46:40]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_seconds_after_midnight(
|
||||
integer(),
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
calendar.time_from_day_fraction({seconds_in_day, @seconds_per_day})
|
||||
|
||||
%Time{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `Time` struct to a number of seconds after midnight.
|
||||
|
||||
The returned value is a two-element tuple with the number of seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.to_seconds_after_midnight(~T[23:30:15])
|
||||
{84615, 0}
|
||||
iex> Time.to_seconds_after_midnight(~N[2010-04-17 23:30:15.999])
|
||||
{84615, 999000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds the `number` of `unit`s to the given `time`.
|
||||
|
||||
@@ -646,12 +556,12 @@ defmodule Time do
|
||||
|
||||
As with the `compare/2` function both `Time` structs and other structures
|
||||
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
|
||||
is passed, only the hour, minute, second, and microsecond is considered. Any
|
||||
is passed, only the hour, month, second, and microsecond is considered. Any
|
||||
additional information about a date or time zone is ignored when calculating
|
||||
the difference.
|
||||
|
||||
The answer can be returned in any `unit` available from
|
||||
`t:System.time_unit/0`. If the first time value is earlier than
|
||||
`t:System.time_unit/0`. If the first unit is smaller than
|
||||
the second, a negative number is returned.
|
||||
|
||||
This function returns the difference in seconds where seconds
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule Calendar.TimeZoneDatabase do
|
||||
with a limit for when the period ends (wall time). The second nested two-tuple is the period
|
||||
just after the gap and a datetime (wall time) for when the period begins just after the gap.
|
||||
|
||||
If there is only a single possible period for the provided `datetime`, then a tuple with `:ok`
|
||||
If there is only a single possible period for the provided `datetime`, the a tuple with `:single`
|
||||
and the `time_zone_period` is returned.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
|
||||
+129
-539
@@ -2,7 +2,7 @@ defmodule Code do
|
||||
@moduledoc ~S"""
|
||||
Utilities for managing code compilation, code evaluation, and code loading.
|
||||
|
||||
This module complements Erlang's [`:code` module](`:code`)
|
||||
This module complements Erlang's [`:code` module](http://www.erlang.org/doc/man/code.html)
|
||||
to add behaviour which is specific to Elixir. Almost all of the functions in this module
|
||||
have global side effects on the behaviour of Elixir.
|
||||
|
||||
@@ -30,56 +30,6 @@ defmodule Code do
|
||||
file, without tracking. `eval_file/2` should be used when you are interested in
|
||||
the result of evaluating the file rather than the modules it defines.
|
||||
|
||||
## Code loading on the Erlang VM
|
||||
|
||||
Erlang has two modes to load code: interactive and embedded.
|
||||
|
||||
By default, the Erlang VM runs in interactive mode, where modules
|
||||
are loaded as needed. In embedded mode the opposite happens, as all
|
||||
modules need to be loaded upfront or explicitly.
|
||||
|
||||
You can use `ensure_loaded/1` (as well as `ensure_lodead?/1` and
|
||||
`ensure_lodead!/1`) to check if a module is loaded before using it and
|
||||
act.
|
||||
|
||||
## `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
|
||||
Elixir also includes `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
functions that are a superset of `ensure_loaded/1`.
|
||||
|
||||
Since Elixir's compilation happens in parallel, in some situations
|
||||
you may need to use a module that was not yet compiled, therefore
|
||||
it can't even be loaded.
|
||||
|
||||
When invoked, `ensure_compiled/1` and `ensure_compiled!/1` halt the
|
||||
compilation of the caller until the module becomes available. Note
|
||||
the distinction between `ensure_compiled/1` and `ensure_compiled!/1`
|
||||
is important: if you are using `ensure_compiled!/1`, you are
|
||||
indicating to the compiler that you can only continue if said module
|
||||
is available.
|
||||
|
||||
If you are using `Code.ensure_compiled/1`, you are implying you may
|
||||
continue without the module and therefore Elixir may return
|
||||
`{:error, :unavailable}` for cases where the module is not yet available
|
||||
(but may be available later on).
|
||||
|
||||
For those reasons, developers must typically use `Code.ensure_compiled!/1`.
|
||||
In particular, do not do this:
|
||||
|
||||
case Code.ensure_compiled(module) do
|
||||
{:module, _} -> module
|
||||
{:error, _} -> raise ...
|
||||
end
|
||||
|
||||
Finally, note you only need `ensure_compiled!/1` to check for modules
|
||||
being defined within the same project. It does not apply to modules from
|
||||
dependencies as dependencies are always compiled upfront.
|
||||
|
||||
In most cases, `ensure_loaded/1` is enough. `ensure_compiled!/1`
|
||||
must be used in rare cases, usually involving macros that need to
|
||||
invoke a module for callback information. The use of `ensure_compiled/1`
|
||||
is even less likely.
|
||||
|
||||
## Compilation tracers
|
||||
|
||||
Elixir supports compilation tracers, which allows modules to observe constructs
|
||||
@@ -136,12 +86,12 @@ defmodule Code do
|
||||
|
||||
* `{:local_function, meta, name, arity}` and
|
||||
`{:local_macro, meta, name, arity}` - traced whenever a local
|
||||
function or macro is referenced. `meta` is the call AST metadata, followed by
|
||||
the `name` and `arity`.
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
* `{:compile_env, app, path, return}` - traced whenever `Application.compile_env/3`
|
||||
or `Application.compile_env!/2` are called. `app` is an atom, `path` is a list
|
||||
of keys to traverse in the application environment and `return` is either
|
||||
of keys to traverse in the application environemnt and `return` is either
|
||||
`{:ok, value}` or `:error`.
|
||||
|
||||
The `:tracers` compiler option can be combined with the `:parser_options`
|
||||
@@ -206,259 +156,6 @@ defmodule Code do
|
||||
required_files()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives a string and returns the cursor context.
|
||||
|
||||
This function receives a string with incomplete Elixir code,
|
||||
representing a cursor position, and based on the string, it
|
||||
provides contextual information about said position. The
|
||||
return of this function can then be used to provide tips,
|
||||
suggestions, and autocompletion functionality.
|
||||
|
||||
This function provides a best-effort detection and may not be
|
||||
accurate under certain circumstances. See the "Limitations"
|
||||
section below.
|
||||
|
||||
Consider adding a catch-all clause when handling the return
|
||||
type of this function as new cursor information may be added
|
||||
in future releases.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Code.cursor_context("")
|
||||
:expr
|
||||
|
||||
iex> Code.cursor_context("hello_wor")
|
||||
{:local_or_var, 'hello_wor'}
|
||||
|
||||
## Return values
|
||||
|
||||
* `{:alias, charlist}` - the context is an alias, potentially
|
||||
a nested one, such as `Hello.Wor` or `HelloWor`
|
||||
|
||||
* `{: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`
|
||||
itself. If a var is given, this may either be a remote call or a map
|
||||
field access. Examples are `Hello.wor`, `:hello.wor`, `hello.wor`,
|
||||
`Hello.nested.wor`, `hello.nested.wor`, and `@hello.world`
|
||||
|
||||
* `{:dot_arity, inside_dot, charlist}` - the context is a dot arity
|
||||
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
|
||||
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
|
||||
itself. If a var is given, it must be a remote arity. Examples are
|
||||
`Hello.world/`, `:hello.world/`, `hello.world/2`, and `@hello.world/2`
|
||||
|
||||
* `{:dot_call, inside_dot, charlist}` - the context is a dot
|
||||
call. This means parentheses or space have been added after the expression.
|
||||
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
|
||||
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
|
||||
itself. If a var is given, it must be a remote call. Examples are
|
||||
`Hello.world(`, `:hello.world(`, `Hello.world `, `hello.world(`, `hello.world `,
|
||||
and `@hello.world(`
|
||||
|
||||
* `:expr` - may be any expression. Autocompletion may suggest an alias,
|
||||
local or var
|
||||
|
||||
* `{:local_or_var, charlist}` - the context is a variable or a local
|
||||
(import or local) call, such as `hello_wor`
|
||||
|
||||
* `{:local_arity, charlist}` - the context is a local (import or local)
|
||||
call, such as `hello_world/`
|
||||
|
||||
* `{:local_call, charlist}` - the context is a local (import or local)
|
||||
call, such as `hello_world(` and `hello_world `
|
||||
|
||||
* `{:module_attribute, charlist}` - the context is a module attribute, such
|
||||
as `@hello_wor`
|
||||
|
||||
* `:none` - no context possible
|
||||
|
||||
* `:unquoted_atom` - the context is an unquoted atom. This can be either
|
||||
previous atoms or all available `:erlang` modules
|
||||
|
||||
## Limitations
|
||||
|
||||
* There is no context for operators
|
||||
* The current algorithm only considers the last line of the input
|
||||
* Context does not yet track strings, sigils, etc.
|
||||
* Arguments of functions calls are not currently recognized
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec cursor_context(List.Chars.t(), keyword()) ::
|
||||
{:alias, charlist}
|
||||
| {:dot, inside_dot, charlist}
|
||||
| {:dot_arity, inside_dot, charlist}
|
||||
| {:dot_call, inside_dot, charlist}
|
||||
| :expr
|
||||
| {:local_or_var, charlist}
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| :none
|
||||
| {:unquoted_atom, charlist}
|
||||
when inside_dot:
|
||||
{:alias, charlist}
|
||||
| {:dot, inside_dot, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:unquoted_atom, charlist}
|
||||
| {:var, charlist}
|
||||
def cursor_context(string, opts \\ [])
|
||||
|
||||
def cursor_context(binary, opts) when is_binary(binary) and is_list(opts) do
|
||||
binary =
|
||||
case :binary.matches(binary, "\n") do
|
||||
[] ->
|
||||
binary
|
||||
|
||||
matches ->
|
||||
{position, _} = List.last(matches)
|
||||
binary_part(binary, position + 1, byte_size(binary) - position - 1)
|
||||
end
|
||||
|
||||
do_cursor_context(String.to_charlist(binary), opts)
|
||||
end
|
||||
|
||||
def cursor_context(charlist, opts) when is_list(charlist) and is_list(opts) do
|
||||
chunked = Enum.chunk_by(charlist, &(&1 == ?\n))
|
||||
|
||||
case List.last(chunked, []) do
|
||||
[?\n | _] -> do_cursor_context([], opts)
|
||||
rest -> do_cursor_context(rest, opts)
|
||||
end
|
||||
end
|
||||
|
||||
def cursor_context(other, opts) do
|
||||
cursor_context(to_charlist(other), opts)
|
||||
end
|
||||
|
||||
@operators '\\<>+-*/:=|&~^@%'
|
||||
@non_closing_punctuation '.,([{;'
|
||||
@closing_punctuation ')]}'
|
||||
@space '\t\s'
|
||||
@closing_identifier '?!'
|
||||
|
||||
@operators_and_non_closing_puctuation @operators ++ @non_closing_punctuation
|
||||
@non_identifier @closing_identifier ++
|
||||
@operators ++ @non_closing_punctuation ++ @closing_punctuation ++ @space
|
||||
|
||||
defp do_cursor_context(list, _opts) do
|
||||
reverse = Enum.reverse(list)
|
||||
|
||||
case strip_spaces(reverse, 0) do
|
||||
# It is empty
|
||||
{[], _} ->
|
||||
:expr
|
||||
|
||||
{[?: | _], 0} ->
|
||||
{:unquoted_atom, ''}
|
||||
|
||||
{[?@ | _], 0} ->
|
||||
{:module_attribute, ''}
|
||||
|
||||
{[?. | rest], _} ->
|
||||
dot(rest, '')
|
||||
|
||||
# It is a local or remote call with parens
|
||||
{[?( | rest], _} ->
|
||||
call_to_cursor_context(rest)
|
||||
|
||||
# A local arity definition
|
||||
{[?/ | rest], _} ->
|
||||
case identifier_to_cursor_context(rest) do
|
||||
{:local_or_var, acc} -> {:local_arity, acc}
|
||||
{:dot, base, acc} -> {:dot_arity, base, acc}
|
||||
_ -> :none
|
||||
end
|
||||
|
||||
# Starting a new expression
|
||||
{[h | _], _} when h in @operators_and_non_closing_puctuation ->
|
||||
:expr
|
||||
|
||||
# It is a local or remote call without parens
|
||||
{rest, spaces} when spaces > 0 ->
|
||||
call_to_cursor_context(rest)
|
||||
|
||||
# It is an identifier
|
||||
_ ->
|
||||
identifier_to_cursor_context(reverse)
|
||||
end
|
||||
end
|
||||
|
||||
defp strip_spaces([h | rest], count) when h in @space, do: strip_spaces(rest, count + 1)
|
||||
defp strip_spaces(rest, count), do: {rest, count}
|
||||
|
||||
defp call_to_cursor_context(reverse) do
|
||||
case identifier_to_cursor_context(reverse) do
|
||||
{:local_or_var, acc} -> {:local_call, acc}
|
||||
{:dot, base, acc} -> {:dot_call, base, acc}
|
||||
_ -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp identifier_to_cursor_context(reverse) do
|
||||
case identifier(reverse) do
|
||||
# Parse :: first to avoid ambiguity with atoms
|
||||
{:alias, false, '::' ++ _, _} -> :none
|
||||
{kind, _, '::' ++ _, acc} -> alias_or_local_or_var(kind, acc)
|
||||
# Now handle atoms, any other atom is unexpected
|
||||
{_kind, _, ':' ++ _, acc} -> {:unquoted_atom, acc}
|
||||
{:atom, _, _, _} -> :none
|
||||
# Parse .. first to avoid ambiguity with dots
|
||||
{:alias, false, _, _} -> :none
|
||||
{kind, _, '..' ++ _, acc} -> alias_or_local_or_var(kind, acc)
|
||||
# Module attributes
|
||||
{:alias, _, '@' ++ _, _} -> :none
|
||||
{:identifier, _, '@' ++ _, acc} -> {:module_attribute, acc}
|
||||
# Everything else
|
||||
{:alias, _, '.' ++ rest, acc} -> nested_alias(rest, acc)
|
||||
{:identifier, _, '.' ++ rest, acc} -> dot(rest, acc)
|
||||
{kind, _, _, acc} -> alias_or_local_or_var(kind, acc)
|
||||
:none -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp nested_alias(rest, acc) do
|
||||
case identifier_to_cursor_context(rest) do
|
||||
{:alias, prev} -> {:alias, prev ++ '.' ++ acc}
|
||||
_ -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp dot(rest, acc) do
|
||||
case identifier_to_cursor_context(rest) do
|
||||
{:local_or_var, prev} -> {:dot, {:var, prev}, acc}
|
||||
{:unquoted_atom, _} = prev -> {:dot, prev, acc}
|
||||
{:alias, _} = prev -> {:dot, prev, acc}
|
||||
{:dot, _, _} = prev -> {:dot, prev, acc}
|
||||
{:module_attribute, _} = prev -> {:dot, prev, acc}
|
||||
_ -> :none
|
||||
end
|
||||
end
|
||||
|
||||
defp alias_or_local_or_var(:alias, acc), do: {:alias, acc}
|
||||
defp alias_or_local_or_var(:identifier, acc), do: {:local_or_var, acc}
|
||||
defp alias_or_local_or_var(_, _), do: :none
|
||||
|
||||
defp identifier([?? | rest]), do: check_identifier(rest, [??])
|
||||
defp identifier([?! | rest]), do: check_identifier(rest, [?!])
|
||||
defp identifier(rest), do: check_identifier(rest, [])
|
||||
|
||||
defp check_identifier([h | _], _acc) when h in @non_identifier, do: :none
|
||||
defp check_identifier(rest, acc), do: rest_identifier(rest, acc)
|
||||
|
||||
defp rest_identifier([h | rest], acc) when h not in @non_identifier do
|
||||
rest_identifier(rest, [h | acc])
|
||||
end
|
||||
|
||||
defp rest_identifier(rest, acc) do
|
||||
case String.Tokenizer.tokenize(acc) do
|
||||
{kind, _, [], _, ascii_only?, _} -> {kind, ascii_only?, rest, acc}
|
||||
_ -> :none
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes files from the required files list.
|
||||
|
||||
@@ -474,14 +171,14 @@ defmodule Code do
|
||||
# Now unrequire all files
|
||||
Code.unrequire_files(Code.required_files())
|
||||
|
||||
# Note that modules are still available
|
||||
# Notice modules are still available
|
||||
function_exported?(EExTest.Compiled, :before_compile, 0)
|
||||
#=> true
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec unrequire_files([binary]) :: :ok
|
||||
def unrequire_files(files) when is_list(files) do
|
||||
def unrequire_files(files) do
|
||||
:elixir_code_server.cast({:unrequire_files, files})
|
||||
end
|
||||
|
||||
@@ -593,7 +290,7 @@ defmodule Code do
|
||||
and the second a list of imported macro names and arity; the list
|
||||
of function names and arity must be sorted
|
||||
|
||||
Note that setting any of the values above overrides Elixir's default
|
||||
Notice that setting any of the values above overrides Elixir's default
|
||||
values. For example, setting `:requires` to `[]` will no longer
|
||||
automatically require the `Kernel` module. In the same way setting
|
||||
`:macros` will no longer auto-import `Kernel` macros like `Kernel.if/2`,
|
||||
@@ -609,33 +306,21 @@ defmodule Code do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 1, b: 2]
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
iex> {result, binding} = Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 1, b: 2, c: 3]
|
||||
iex> Code.eval_string("c = a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2, c: 3]}
|
||||
|
||||
iex> {result, binding} = Code.eval_string("a = a + b", [a: 1, b: 2])
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 3, b: 2]
|
||||
iex> Code.eval_string("a = a + b", [a: 1, b: 2])
|
||||
{3, [a: 3, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__/0` as the `opts` argument and
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 1, b: 2]
|
||||
iex> Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
@@ -652,7 +337,7 @@ defmodule Code do
|
||||
|
||||
defp eval_string_with_error_handling(string, binding, opts) do
|
||||
%{line: line, file: file} = env = :elixir.env_for_eval(opts)
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, file, [])
|
||||
{value, binding, _env} = :elixir.eval_forms(forms, binding, env)
|
||||
{value, binding}
|
||||
end
|
||||
@@ -673,8 +358,9 @@ defmodule Code do
|
||||
|
||||
* `:line_length` - the line length to aim for when formatting
|
||||
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
|
||||
reference but it is not enforced by the formatter as sometimes
|
||||
user intervention is required. See "Running the formatter"
|
||||
section
|
||||
|
||||
* `:locals_without_parens` - a keyword list of name and arity
|
||||
pairs that should be kept without parens whenever possible.
|
||||
@@ -682,12 +368,17 @@ defmodule Code do
|
||||
that name. The formatter already includes a list of functions
|
||||
and this option augments this list.
|
||||
|
||||
* `:rename_deprecated_at` - rename all known deprecated functions
|
||||
at the given version to their non-deprecated equivalent. It
|
||||
expects a valid `Version` which is usually the minimum Elixir
|
||||
version supported by the project.
|
||||
|
||||
* `:force_do_end_blocks` (since v1.9.0) - when `true`, converts all
|
||||
inline usages of `do: ...`, `else: ...` and friends into `do/end`
|
||||
blocks. Defaults to `false`. Note that this option is convergent:
|
||||
once you set it to `true`, **all keywords** will be converted.
|
||||
If you set it to `false` later on, `do/end` blocks won't be
|
||||
converted back to keywords.
|
||||
blocks. Defaults to `false`. Notice this option is convergent:
|
||||
once you set it to `true`, all keywords will be converted. If you
|
||||
set it to `false` later on, `do/end` blocks won't be converted
|
||||
back to keywords.
|
||||
|
||||
## Design principles
|
||||
|
||||
@@ -695,6 +386,8 @@ defmodule Code do
|
||||
|
||||
First, the formatter never changes the semantics of the code by
|
||||
default. This means the input AST and the output AST are equivalent.
|
||||
Optional behaviour, such as `:rename_deprecated_at`, is allowed to
|
||||
break this guarantee.
|
||||
|
||||
The second principle is to provide as little configuration as possible.
|
||||
This eases the formatter adoption by removing contention points while
|
||||
@@ -720,8 +413,29 @@ defmodule Code do
|
||||
do not recommend to run the formatter blindly in an existing codebase.
|
||||
Instead you should format and sanity check each formatted file.
|
||||
|
||||
For example, the formatter may break a long function definition over
|
||||
multiple clauses:
|
||||
Let's see some examples. The code below:
|
||||
|
||||
"this is a very long string ... #{inspect(some_value)}"
|
||||
|
||||
may be formatted as:
|
||||
|
||||
"this is a very long string ... #{
|
||||
inspect(some_value)
|
||||
}"
|
||||
|
||||
This happens because the only place the formatter can introduce a
|
||||
new line without changing the code semantics is in the interpolation.
|
||||
In those scenarios, we recommend developers to directly adjust the
|
||||
code. Here we can use the binary concatenation operator `<>/2`:
|
||||
|
||||
"this is a very long string " <>
|
||||
"... #{inspect(some_value)}"
|
||||
|
||||
The string concatenation makes the code fit on a single line and also
|
||||
gives more options to the formatter.
|
||||
|
||||
A similar example is when the formatter breaks a function definition
|
||||
over multiple clauses:
|
||||
|
||||
def my_function(
|
||||
%User{name: name, age: age, ...},
|
||||
@@ -786,40 +500,6 @@ defmodule Code do
|
||||
we describe in the next sections the cases where the formatter keeps the
|
||||
user encoding and how to control multiline expressions.
|
||||
|
||||
## Line length
|
||||
|
||||
Another point about the formatter is that the `:line_length` configuration
|
||||
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"
|
||||
|
||||
The formatter doesn't know how to break it apart without changing the
|
||||
code underlying syntax representation, so it is up to you to step in:
|
||||
|
||||
"this is a very long string " <>
|
||||
"that will go over the line length"
|
||||
|
||||
The string concatenation makes the code fit on a single line and also
|
||||
gives more options to the formatter.
|
||||
|
||||
This may also appear in do/end blocks, where the `do` keyword (or `->`)
|
||||
may go over the line lenth 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
|
||||
|
||||
And only the `do` keyword is above the line length, Elixir **will not**
|
||||
emit this:
|
||||
|
||||
case very_long_expression()
|
||||
do
|
||||
|
||||
So it prefers to not touch the line at all and leave `do` above the
|
||||
line limit.
|
||||
|
||||
## Keeping user's formatting
|
||||
|
||||
The formatter respects the input format in some cases. Those are
|
||||
@@ -966,10 +646,6 @@ defmodule Code do
|
||||
are considered equivalent (the nesting is discarded alongside most of
|
||||
user formatting). In such cases, the code formatter will always format to
|
||||
the latter.
|
||||
|
||||
## Newlines
|
||||
|
||||
The formatter converts all newlines in code from `\r\n` to `\n`.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, keyword) :: iodata
|
||||
@@ -1006,21 +682,15 @@ defmodule Code do
|
||||
## Examples
|
||||
|
||||
iex> contents = quote(do: var!(a) + var!(b))
|
||||
iex> {result, binding} = Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 1, b: 2]
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
For convenience, you can pass `__ENV__/0` as the `opts` argument and
|
||||
all options will be automatically extracted from the current environment:
|
||||
|
||||
iex> contents = quote(do: var!(a) + var!(b))
|
||||
iex> {result, binding} = Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
iex> Enum.sort(binding)
|
||||
[a: 1, b: 2]
|
||||
iex> Code.eval_quoted(contents, [a: 1, b: 2], __ENV__)
|
||||
{3, [a: 1, b: 2]}
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
@@ -1089,9 +759,6 @@ defmodule Code do
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
|
||||
* `:column` - (since v1.11.0) the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
|
||||
@@ -1159,13 +826,12 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@spec string_to_quoted(List.Chars.t(), keyword) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, term, term}}
|
||||
{:ok, Macro.t()} | {:error, {line :: pos_integer, term, term}}
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, file, opts) do
|
||||
{:ok, tokens} ->
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
@@ -1188,8 +854,7 @@ defmodule Code do
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
:elixir.string_to_quoted!(to_charlist(string), line, column, file, opts)
|
||||
:elixir.string_to_quoted!(to_charlist(string), line, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1224,7 +889,7 @@ defmodule Code do
|
||||
If the file was already required, `require_file/2` doesn't do anything and
|
||||
returns `nil`.
|
||||
|
||||
Note that if `require_file/2` is invoked by different processes concurrently,
|
||||
Notice that if `require_file/2` is invoked by different processes concurrently,
|
||||
the first process to invoke `require_file/2` acquires a lock and the remaining
|
||||
ones will block until the file is available. This means that if `require_file/2`
|
||||
is called more than once with a given file, that file will be compiled only once.
|
||||
@@ -1267,7 +932,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Gets all compilation options from the code server.
|
||||
|
||||
To get individual options, see `get_compiler_option/1`.
|
||||
To get invidual options, see `get_compiler_option/1`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
@@ -1286,7 +951,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores all given compilation options.
|
||||
|
||||
To store individual options, see `put_compiler_option/2`.
|
||||
To store invidual options, see `put_compiler_option/2`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
@@ -1298,9 +963,8 @@ defmodule Code do
|
||||
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
|
||||
def compiler_options(opts) do
|
||||
for {key, value} <- opts, into: %{} do
|
||||
previous = get_compiler_option(key)
|
||||
put_compiler_option(key, value)
|
||||
{key, previous}
|
||||
{key, value}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1512,7 +1176,39 @@ defmodule Code do
|
||||
If it succeeds in loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
See the module documentation for more information on code loading.
|
||||
## Code loading on the Erlang VM
|
||||
|
||||
Erlang has two modes to load code: interactive and embedded.
|
||||
|
||||
By default, the Erlang VM runs in interactive mode, where modules
|
||||
are loaded as needed. In embedded mode the opposite happens, as all
|
||||
modules need to be loaded upfront or explicitly.
|
||||
|
||||
Therefore, this function is used to check if a module is loaded
|
||||
before using it and allows one to react accordingly. For example, the `URI`
|
||||
module uses this function to check if a specific parser exists for a given
|
||||
URI scheme.
|
||||
|
||||
## `ensure_compiled/1`
|
||||
|
||||
Elixir also contains an `ensure_compiled/1` function that is a
|
||||
superset of `ensure_loaded/1`.
|
||||
|
||||
Since Elixir's compilation happens in parallel, in some situations
|
||||
you may need to use a module that was not yet compiled, therefore
|
||||
it can't even be loaded.
|
||||
|
||||
When invoked, `ensure_compiled/1` halts the compilation of the caller
|
||||
until the module given to `ensure_compiled/1` becomes available or
|
||||
all files for the current project have been compiled. If compilation
|
||||
finishes and the module is not available, an error tuple is returned.
|
||||
|
||||
`ensure_compiled/1` does not apply to dependencies, as dependencies
|
||||
must be compiled upfront.
|
||||
|
||||
In most cases, `ensure_loaded/1` is enough. `ensure_compiled/1`
|
||||
must be used in rare cases, usually involving macros that need to
|
||||
invoke a module for callback information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1547,62 +1243,14 @@ defmodule Code do
|
||||
match?({:module, ^module}, ensure_loaded(module))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `ensure_loaded/1` but raises if the module cannot be loaded.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec ensure_loaded!(module) :: module
|
||||
def ensure_loaded!(module) do
|
||||
case ensure_loaded(module) do
|
||||
{:module, module} ->
|
||||
module
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"could not load module #{inspect(module)} due to reason #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Similar to `ensure_compiled!/1` but indicates you can continue without said module.
|
||||
|
||||
While `ensure_compiled!/1` indicates to the Elixir compiler you can
|
||||
only continue when said module is available, this function indicates
|
||||
you may continue compilation without said module.
|
||||
|
||||
If it succeeds in loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this function returns `{:error, :unavailable}`. Unavailable doesn't
|
||||
necessarily mean the module doesn't exist, just that it is not currently
|
||||
available, but it (or may not) become available in the future.
|
||||
|
||||
Therefore, if you can only continue if the module is available, use
|
||||
`ensure_compiled!/1` instead. In particular, do not do this:
|
||||
|
||||
case Code.ensure_compiled(module) do
|
||||
{:module, _} -> module
|
||||
{:error, _} -> raise ...
|
||||
end
|
||||
|
||||
See the module documentation for more information on code loading.
|
||||
"""
|
||||
@spec ensure_compiled(module) ::
|
||||
{:module, module}
|
||||
| {:error, :embedded | :badfile | :nofile | :on_load_failure | :unavailable}
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
ensure_compiled(module, :soft)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given module is compiled and loaded.
|
||||
|
||||
If the module is already loaded, it works as no-op. If the module was
|
||||
not compiled yet, `ensure_compiled!/1` halts the compilation of the caller
|
||||
until the module given to `ensure_compiled!/1` becomes available or
|
||||
not compiled yet, `ensure_compiled/1` halts the compilation of the caller
|
||||
until the module given to `ensure_compiled/1` becomes available or
|
||||
all files for the current project have been compiled. If compilation
|
||||
finishes and the module is not available or is in a deadlock, an error
|
||||
is raised.
|
||||
finishes and the module is not available, an error tuple is returned.
|
||||
|
||||
Given this function halts compilation, use it carefully. In particular,
|
||||
avoid using it to guess which modules are in the system. Overuse of this
|
||||
@@ -1610,26 +1258,25 @@ defmodule Code do
|
||||
if the other is compiled. This returns a specific unavailable error code,
|
||||
where we cannot successfully verify a module is available or not.
|
||||
|
||||
See the module documentation for more information on code loading.
|
||||
If it succeeds in loading the module, it returns `{:module, module}`.
|
||||
If not, returns `{:error, reason}` with the error reason.
|
||||
|
||||
If the module being checked is currently in a compiler deadlock,
|
||||
this function returns `{:error, :unavailable}`. Unavailable doesn't
|
||||
necessarily mean the module doesn't exist, just that it is not currently
|
||||
available, but it (or may not) become available in the future.
|
||||
|
||||
Check `ensure_loaded/1` for more information on module loading
|
||||
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec ensure_compiled!(module) :: module
|
||||
def ensure_compiled!(module) do
|
||||
case ensure_compiled(module, :hard) do
|
||||
{:module, module} ->
|
||||
module
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"could not load module #{inspect(module)} due to reason #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_compiled(module, mode) do
|
||||
@spec ensure_compiled(module) ::
|
||||
{:module, module}
|
||||
| {:error, :embedded | :badfile | :nofile | :on_load_failure | :unavailable}
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
case :code.ensure_loaded(module) do
|
||||
{:error, :nofile} = error ->
|
||||
if can_await_module_compilation?() do
|
||||
case Kernel.ErrorHandler.ensure_compiled(module, :module, mode) do
|
||||
if is_pid(:erlang.get(:elixir_compiler_pid)) do
|
||||
case Kernel.ErrorHandler.ensure_compiled(module, :module, :soft) do
|
||||
:found -> {:module, module}
|
||||
:deadlock -> {:error, :unavailable}
|
||||
:not_found -> {:error, :nofile}
|
||||
@@ -1643,21 +1290,6 @@ defmodule Code do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the current process can await for module compilation.
|
||||
|
||||
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
|
||||
used by Mix and `elixirc`, calling a module that has not yet been
|
||||
compiled will block the caller until the module becomes available.
|
||||
Executing Elixir scripts, such as passing a filename to `elixir`,
|
||||
does not await.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec can_await_module_compilation? :: boolean
|
||||
def can_await_module_compilation? do
|
||||
:erlang.process_info(self(), :error_handler) == {:error_handler, Kernel.ErrorHandler}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Code.ensure_compiled/1 instead (see the proper disclaimers in its docs)"
|
||||
def ensure_compiled?(module) when is_atom(module) do
|
||||
@@ -1673,7 +1305,7 @@ defmodule Code do
|
||||
file.
|
||||
|
||||
It returns the term stored in the documentation chunk in the format defined by
|
||||
[EEP 48](https://erlang.org/eep/eeps/eep-0048.html) or `{:error, reason}` if
|
||||
[EEP 48](http://erlang.org/eep/eeps/eep-0048.html) or `{:error, reason}` if
|
||||
the chunk is not available.
|
||||
|
||||
## Examples
|
||||
@@ -1694,8 +1326,8 @@ defmodule Code do
|
||||
docs :: [doc_element]}
|
||||
| {: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,
|
||||
beam_language: :elixir | :erlang | :lfe | :alpaca | atom(),
|
||||
doc_content: %{required(binary) => binary} | :none | :hidden,
|
||||
doc_element:
|
||||
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
|
||||
metadata},
|
||||
@@ -1706,77 +1338,35 @@ defmodule Code do
|
||||
|
||||
def fetch_docs(module) when is_atom(module) do
|
||||
case :code.get_object_code(module) do
|
||||
{_module, bin, beam_path} ->
|
||||
case fetch_docs_from_beam(bin) do
|
||||
{:error, :chunk_not_found} ->
|
||||
app_root = Path.expand(Path.join(["..", ".."]), beam_path)
|
||||
path = Path.join([app_root, "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
|
||||
:error ->
|
||||
case :code.which(module) do
|
||||
:preloaded ->
|
||||
# The erts directory is not necessarily included in releases
|
||||
# unless it is listed as an extra application.
|
||||
case :code.lib_dir(:erts) do
|
||||
path when is_list(path) ->
|
||||
path = Path.join([path, "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :chunk_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :module_not_found}
|
||||
end
|
||||
{_module, bin, _beam_path} -> do_fetch_docs(bin)
|
||||
:error -> {:error, :module_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_docs(path) when is_binary(path) do
|
||||
fetch_docs_from_beam(String.to_charlist(path))
|
||||
do_fetch_docs(String.to_charlist(path))
|
||||
end
|
||||
|
||||
@docs_chunk 'Docs'
|
||||
|
||||
defp fetch_docs_from_beam(bin_or_path) do
|
||||
defp do_fetch_docs(bin_or_path) do
|
||||
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
|
||||
{:ok, {_module, [{@docs_chunk, bin}]}} ->
|
||||
load_docs_chunk(bin)
|
||||
try do
|
||||
:erlang.binary_to_term(bin)
|
||||
rescue
|
||||
_ -> {:error, {:invalid_chunk, bin}}
|
||||
end
|
||||
|
||||
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
|
||||
{:error, :chunk_not_found}
|
||||
|
||||
{:error, :beam_lib, {:file_error, _, :enoent}} ->
|
||||
{:error, :module_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_docs_from_chunk(path) do
|
||||
case File.read(path) do
|
||||
{:ok, bin} ->
|
||||
load_docs_chunk(bin)
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :chunk_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_docs_chunk(bin) do
|
||||
:erlang.binary_to_term(bin)
|
||||
rescue
|
||||
_ ->
|
||||
{:error, {:invalid_chunk, bin}}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Deprecated function to retrieve old documentation format.
|
||||
|
||||
Elixir v1.7 adopts [EEP 48](https://erlang.org/eep/eeps/eep-0048.html)
|
||||
Elixir v1.7 adopts [EEP 48](http://erlang.org/eep/eeps/eep-0048.html)
|
||||
which is a new documentation format meant to be shared across all
|
||||
BEAM languages. The old format, used by `Code.get_docs/2`, is no
|
||||
longer available, and therefore this function always returns `nil`.
|
||||
|
||||
+119
-132
@@ -12,11 +12,8 @@ defmodule Code.Formatter do
|
||||
@empty empty()
|
||||
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
|
||||
|
||||
# Operators that are composed of multiple binary operators
|
||||
@multi_binary_operators [:"..//"]
|
||||
|
||||
# Operators that do not have space between operands
|
||||
@no_space_binary_operators [:.., :"//"]
|
||||
@no_space_binary_operators [:..]
|
||||
|
||||
# Operators that do not have newline between operands (as well as => and keywords)
|
||||
@no_newline_binary_operators [:\\, :in]
|
||||
@@ -45,8 +42,6 @@ defmodule Code.Formatter do
|
||||
:<~>,
|
||||
:<|>,
|
||||
:^^^,
|
||||
:+++,
|
||||
:---,
|
||||
:in,
|
||||
:++,
|
||||
:--,
|
||||
@@ -145,8 +140,8 @@ defmodule Code.Formatter do
|
||||
Checks if two strings are equivalent.
|
||||
"""
|
||||
def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, 1, "nofile", [])
|
||||
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, 1, "nofile", [])
|
||||
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, "nofile", [])
|
||||
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, "nofile", [])
|
||||
|
||||
case not_equivalent(quoted1, quoted2) do
|
||||
{left, right} -> {:error, left, right}
|
||||
@@ -215,7 +210,7 @@ defmodule Code.Formatter do
|
||||
token_metadata: true
|
||||
]
|
||||
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, 1, file, tokenizer_options),
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options),
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do
|
||||
state =
|
||||
Process.get(:code_formatter_comments)
|
||||
@@ -242,14 +237,26 @@ defmodule Code.Formatter do
|
||||
{:ok, doc} ->
|
||||
doc
|
||||
|
||||
{:error, {location, error, token}} ->
|
||||
:elixir_errors.parse_error(location, Keyword.get(opts, :file, "nofile"), error, token)
|
||||
{:error, {line, error, token}} ->
|
||||
:elixir_errors.parse_error(line, Keyword.get(opts, :file, "nofile"), error, token)
|
||||
end
|
||||
end
|
||||
|
||||
defp state(comments, opts) do
|
||||
force_do_end_blocks = Keyword.get(opts, :force_do_end_blocks, false)
|
||||
|
||||
rename_deprecated_at =
|
||||
if version = opts[:rename_deprecated_at] do
|
||||
case Version.parse(version) do
|
||||
{:ok, parsed} ->
|
||||
parsed
|
||||
|
||||
:error ->
|
||||
raise ArgumentError,
|
||||
"invalid version #{inspect(version)} given to :rename_deprecated_at"
|
||||
end
|
||||
end
|
||||
|
||||
locals_without_parens =
|
||||
Keyword.get(opts, :locals_without_parens, []) ++ @locals_without_parens
|
||||
|
||||
@@ -257,7 +264,7 @@ defmodule Code.Formatter do
|
||||
force_do_end_blocks: force_do_end_blocks,
|
||||
locals_without_parens: locals_without_parens,
|
||||
operand_nesting: 2,
|
||||
skip_eol: false,
|
||||
rename_deprecated_at: rename_deprecated_at,
|
||||
comments: comments
|
||||
}
|
||||
end
|
||||
@@ -323,7 +330,7 @@ defmodule Code.Formatter do
|
||||
{next_eol, comments, doc}
|
||||
end
|
||||
|
||||
# Special AST nodes from compiler feedback
|
||||
# Special AST nodes from compiler feedback.
|
||||
|
||||
defp quoted_to_algebra({{:special, :clause_args}, _meta, [args]}, _context, state) do
|
||||
{doc, state} = clause_args_to_algebra(args, state)
|
||||
@@ -394,17 +401,10 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
# foo[bar]
|
||||
defp quoted_to_algebra({{:., _, [Access, :get]}, meta, [target, arg]}, _context, state) do
|
||||
defp quoted_to_algebra({{:., _, [Access, :get]}, meta, [target | args]}, _context, state) do
|
||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||
|
||||
{access_doc, state} =
|
||||
if keyword?(arg) do
|
||||
list_to_algebra(meta, arg, state)
|
||||
else
|
||||
list_to_algebra(meta, [arg], state)
|
||||
end
|
||||
|
||||
{concat(target_doc, access_doc), state}
|
||||
{call_doc, state} = list_to_algebra(meta, args, state)
|
||||
{concat(target_doc, call_doc), state}
|
||||
end
|
||||
|
||||
# %Foo{}
|
||||
@@ -483,10 +483,6 @@ defmodule Code.Formatter do
|
||||
quoted_to_algebra(arg, context, state)
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
|
||||
{"nil", state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do
|
||||
{block, state} = block_to_algebra(block, line(meta), closing_line(meta), state)
|
||||
{surround("(", block, ")"), state}
|
||||
@@ -519,17 +515,19 @@ defmodule Code.Formatter do
|
||||
|
||||
# not(left in right)
|
||||
# left not in right
|
||||
defp quoted_to_algebra({:not, meta, [{:in, _, [left, right]}]}, context, state) do
|
||||
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
||||
end
|
||||
defp quoted_to_algebra({:not, meta, [{:in, _, [left, right]} = arg]}, context, state) do
|
||||
%{rename_deprecated_at: since} = state
|
||||
|
||||
# 1..2//3
|
||||
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
|
||||
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
|
||||
# TODO: Remove metadata and always rewrite to left not in right in Elixir v2.0.
|
||||
if meta[:operator] == :"not in" || (since && Version.match?(since, "~> 1.5")) do
|
||||
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
||||
else
|
||||
unary_op_to_algebra(:not, meta, arg, context, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do
|
||||
anon_fun_to_algebra(clauses, line(meta), closing_line(meta), state, eol?(meta, state))
|
||||
anon_fun_to_algebra(clauses, line(meta), closing_line(meta), state, eol?(meta))
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({fun, meta, args}, context, state) when is_atom(fun) and is_list(args) do
|
||||
@@ -561,10 +559,6 @@ defmodule Code.Formatter do
|
||||
if keyword_key?(left_arg) do
|
||||
{left, state} =
|
||||
case left_arg do
|
||||
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||
{:__block__, _, [:"..//"]} ->
|
||||
{string(~S{"..//":}), state}
|
||||
|
||||
{:__block__, _, [atom]} when is_atom(atom) ->
|
||||
key =
|
||||
case Code.Identifier.classify(atom) do
|
||||
@@ -711,7 +705,7 @@ defmodule Code.Formatter do
|
||||
{operands, max_line} =
|
||||
unwrap_right(right_arg, op, meta, right_context, [{{:root, left_context}, left_arg}])
|
||||
|
||||
fun = fn
|
||||
operand_to_algebra = fn
|
||||
{{:root, context}, arg}, _args, state ->
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2)
|
||||
{{doc, @empty, 1}, state}
|
||||
@@ -723,7 +717,14 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
{doc, state} =
|
||||
operand_to_algebra_with_comments(operands, meta, min_line, max_line, context, state, fun)
|
||||
operand_to_algebra_with_comments(
|
||||
operands,
|
||||
meta,
|
||||
min_line,
|
||||
max_line,
|
||||
state,
|
||||
operand_to_algebra
|
||||
)
|
||||
|
||||
if keyword?(right_arg) and context in [:parens_arg, :no_parens_arg] do
|
||||
{wrap_in_parens(doc), state}
|
||||
@@ -742,7 +743,7 @@ defmodule Code.Formatter do
|
||||
{pipes, min_line} =
|
||||
unwrap_pipes(left_arg, meta, left_context, [{{op, right_context}, right_arg}])
|
||||
|
||||
fun = fn
|
||||
operand_to_algebra = fn
|
||||
{{:root, context}, arg}, _args, state ->
|
||||
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :left, 2)
|
||||
{{doc, @empty, 1}, state}
|
||||
@@ -754,7 +755,7 @@ defmodule Code.Formatter do
|
||||
{{concat(op_string, doc), @empty, 1}, state}
|
||||
end
|
||||
|
||||
operand_to_algebra_with_comments(pipes, meta, min_line, max_line, context, state, fun)
|
||||
operand_to_algebra_with_comments(pipes, meta, min_line, max_line, state, operand_to_algebra)
|
||||
end
|
||||
|
||||
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
|
||||
@@ -778,7 +779,7 @@ defmodule Code.Formatter do
|
||||
concat(concat(group(left), op_string), group(right))
|
||||
|
||||
true ->
|
||||
eol? = eol?(meta, state)
|
||||
eol? = eol?(meta)
|
||||
|
||||
next_break_fits? =
|
||||
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
|
||||
@@ -882,45 +883,11 @@ defmodule Code.Formatter do
|
||||
{Enum.reverse(acc), line(meta)}
|
||||
end
|
||||
|
||||
defp operand_to_algebra_with_comments(operands, meta, min_line, max_line, context, state, fun) do
|
||||
# If we are in a no_parens_one_arg expression, we actually cannot
|
||||
# extract comments from the first operand, because it would rewrite:
|
||||
#
|
||||
# @spec function(x) ::
|
||||
# # Comment
|
||||
# any
|
||||
# when x: any
|
||||
#
|
||||
# to:
|
||||
#
|
||||
# @spec # Comment
|
||||
# function(x) ::
|
||||
# any
|
||||
# when x: any
|
||||
#
|
||||
# Instead we get:
|
||||
#
|
||||
# @spec function(x) ::
|
||||
# any
|
||||
# # Comment
|
||||
# when x: any
|
||||
#
|
||||
# Which may look counter-intuitive but it actually makes sense,
|
||||
# as the closest possible location for the comment is the when
|
||||
# operator.
|
||||
{operands, acc, state} =
|
||||
if context == :no_parens_one_arg do
|
||||
[operand | operands] = operands
|
||||
{doc_triplet, state} = fun.(operand, :unused, state)
|
||||
{operands, [doc_triplet], state}
|
||||
else
|
||||
{operands, [], state}
|
||||
end
|
||||
|
||||
defp operand_to_algebra_with_comments(operands, meta, min_line, max_line, state, fun) do
|
||||
{docs, comments?, state} =
|
||||
quoted_to_algebra_with_comments(operands, acc, min_line, max_line, state, fun)
|
||||
quoted_to_algebra_with_comments(operands, [], min_line, max_line, state, fun)
|
||||
|
||||
if comments? or eol?(meta, state) do
|
||||
if comments? or eol?(meta) do
|
||||
{docs |> Enum.reduce(&line(&2, &1)) |> force_unfit(), state}
|
||||
else
|
||||
{docs |> Enum.reduce(&glue(&2, &1)), state}
|
||||
@@ -985,7 +952,7 @@ defmodule Code.Formatter do
|
||||
)
|
||||
when is_atom(fun) and is_integer(arity) do
|
||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||
fun = Code.Identifier.inspect_as_function(fun)
|
||||
fun = remote_fun_to_algebra(target, fun, arity, state)
|
||||
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
|
||||
end
|
||||
|
||||
@@ -1030,7 +997,7 @@ defmodule Code.Formatter do
|
||||
defp remote_to_algebra({{:., _, [target, fun]}, meta, args}, context, state)
|
||||
when is_atom(fun) do
|
||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||
fun = Code.Identifier.inspect_as_function(fun)
|
||||
fun = remote_fun_to_algebra(target, fun, length(args), state)
|
||||
remote_doc = target_doc |> concat(".") |> concat(string(fun))
|
||||
|
||||
if args == [] and not remote_target_is_a_module?(target) and not meta?(meta, :closing) do
|
||||
@@ -1066,6 +1033,38 @@ defmodule Code.Formatter do
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_fun_to_algebra(target, fun, arity, state) do
|
||||
%{rename_deprecated_at: since} = state
|
||||
|
||||
atom_target =
|
||||
case since && target do
|
||||
{:__aliases__, _, [alias | _] = aliases} when is_atom(alias) ->
|
||||
Module.concat(aliases)
|
||||
|
||||
{:__block__, _, [atom]} when is_atom(atom) ->
|
||||
atom
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
with {fun, requirement} <- deprecated(atom_target, fun, arity),
|
||||
true <- Version.match?(since, requirement) do
|
||||
fun
|
||||
else
|
||||
_ -> Code.Identifier.inspect_as_function(fun)
|
||||
end
|
||||
end
|
||||
|
||||
# We can only rename functions in the same module because
|
||||
# introducing a new module may be wrong due to aliases.
|
||||
defp deprecated(Enum, :partition, 2), do: {"split_with", "~> 1.4"}
|
||||
defp deprecated(Code, :unload_files, 2), do: {"unrequire_files", "~> 1.7"}
|
||||
defp deprecated(Code, :loaded_files, 2), do: {"required_files", "~> 1.7"}
|
||||
defp deprecated(Kernel.ParallelCompiler, :files, 2), do: {"compile", "~> 1.6"}
|
||||
defp deprecated(Kernel.ParallelCompiler, :files_to_path, 2), do: {"compile_to_path", "~> 1.6"}
|
||||
defp deprecated(_, _, _), do: :error
|
||||
|
||||
defp remote_target_to_algebra({:fn, _, [_ | _]} = quoted, state) do
|
||||
# This change is not semantically required but for beautification.
|
||||
{doc, state} = quoted_to_algebra(quoted, :no_parens_arg, state)
|
||||
@@ -1156,7 +1155,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
args = if keyword?, do: left ++ right, else: left ++ [right]
|
||||
many_eol? = match?([_, _ | _], args) and eol?(meta, state)
|
||||
many_eol? = match?([_, _ | _], args) and eol?(meta)
|
||||
no_generators? = no_generators?(args)
|
||||
to_algebra_fun = "ed_to_algebra(&1, context, &2)
|
||||
|
||||
@@ -1346,8 +1345,9 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp list_interpolation_to_algebra([entry | entries], escape, state, acc, last) do
|
||||
{{:., _, [Kernel, :to_string]}, _meta, [quoted]} = entry
|
||||
{doc, state} = interpolation_to_string(quoted, state)
|
||||
{{:., _, [Kernel, :to_string]}, meta, [quoted]} = entry
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), closing_line(meta), state)
|
||||
doc = surround("\#{", doc, "}")
|
||||
list_interpolation_to_algebra(entries, escape, state, concat(acc, doc), last)
|
||||
end
|
||||
|
||||
@@ -1362,8 +1362,9 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp interpolation_to_algebra([entry | entries], escape, state, acc, last) do
|
||||
{:"::", _, [{{:., _, [Kernel, :to_string]}, _meta, [quoted]}, {:binary, _, _}]} = entry
|
||||
{doc, state} = interpolation_to_string(quoted, state)
|
||||
{:"::", _, [{{:., _, [Kernel, :to_string]}, meta, [quoted]}, {:binary, _, _}]} = entry
|
||||
{doc, state} = block_to_algebra(quoted, line(meta), closing_line(meta), state)
|
||||
doc = surround("\#{", doc, "}")
|
||||
interpolation_to_algebra(entries, escape, state, concat(acc, doc), last)
|
||||
end
|
||||
|
||||
@@ -1371,23 +1372,6 @@ defmodule Code.Formatter do
|
||||
{concat(acc, last), state}
|
||||
end
|
||||
|
||||
defp interpolation_to_string(quoted, %{skip_eol: skip_eol} = state) do
|
||||
{doc, state} = block_to_algebra(quoted, @max_line, @min_line, %{state | skip_eol: true})
|
||||
doc = interpolation_to_string(surround("\#{", doc, "}"))
|
||||
{doc, %{state | skip_eol: skip_eol}}
|
||||
end
|
||||
|
||||
defp interpolation_to_string(doc) do
|
||||
[head | tail] =
|
||||
doc
|
||||
|> format_to_string()
|
||||
|> String.split("\n")
|
||||
|
||||
Enum.reduce(tail, string(head), fn line, acc ->
|
||||
concat([acc, line(), string(line)])
|
||||
end)
|
||||
end
|
||||
|
||||
## Sigils
|
||||
|
||||
defp maybe_sigil_to_algebra(fun, meta, args, state) do
|
||||
@@ -1426,7 +1410,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp bitstring_to_algebra(meta, args, state) do
|
||||
last = length(args) - 1
|
||||
join = if eol?(meta, state), do: :line, else: :flex_break
|
||||
join = if eol?(meta), do: :line, else: :flex_break
|
||||
to_algebra_fun = &bitstring_segment_to_algebra(&1, &2, last)
|
||||
|
||||
{args_doc, join, state} =
|
||||
@@ -1494,7 +1478,7 @@ defmodule Code.Formatter do
|
||||
## Literals
|
||||
|
||||
defp list_to_algebra(meta, args, state) do
|
||||
join = if eol?(meta, state), do: :line, else: :break
|
||||
join = if eol?(meta), do: :line, else: :break
|
||||
fun = "ed_to_algebra(&1, :parens_arg, &2)
|
||||
|
||||
{args_doc, _join, state} =
|
||||
@@ -1504,7 +1488,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp map_to_algebra(meta, name_doc, [{:|, _, [left, right]}], state) do
|
||||
join = if eol?(meta, state), do: :line, else: :break
|
||||
join = if eol?(meta), do: :line, else: :break
|
||||
fun = "ed_to_algebra(&1, :parens_arg, &2)
|
||||
{left_doc, state} = fun.(left, state)
|
||||
|
||||
@@ -1521,7 +1505,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp map_to_algebra(meta, name_doc, args, state) do
|
||||
join = if eol?(meta, state), do: :line, else: :break
|
||||
join = if eol?(meta), do: :line, else: :break
|
||||
fun = "ed_to_algebra(&1, :parens_arg, &2)
|
||||
|
||||
{args_doc, _join, state} =
|
||||
@@ -1532,7 +1516,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp tuple_to_algebra(meta, args, join, state) do
|
||||
join = if eol?(meta, state), do: :line, else: join
|
||||
join = if eol?(meta), do: :line, else: join
|
||||
fun = "ed_to_algebra(&1, :parens_arg, &2)
|
||||
|
||||
{args_doc, join, state} =
|
||||
@@ -1549,11 +1533,6 @@ defmodule Code.Formatter do
|
||||
Atom.to_string(atom)
|
||||
end
|
||||
|
||||
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||
defp atom_to_algebra(:"..//") do
|
||||
string(":\"..//\"")
|
||||
end
|
||||
|
||||
defp atom_to_algebra(atom) do
|
||||
string = Atom.to_string(atom)
|
||||
|
||||
@@ -1714,7 +1693,7 @@ defmodule Code.Formatter do
|
||||
|> glue(body_doc)
|
||||
|> nest(2)
|
||||
|> glue("end")
|
||||
|> maybe_force_clauses(clauses, state)
|
||||
|> maybe_force_clauses(clauses)
|
||||
|> group()
|
||||
|
||||
{doc, state}
|
||||
@@ -1748,7 +1727,7 @@ defmodule Code.Formatter do
|
||||
|> glue(body_doc)
|
||||
|> nest(2)
|
||||
|> glue("end")
|
||||
|> maybe_force_clauses(clauses, state)
|
||||
|> maybe_force_clauses(clauses)
|
||||
|> group()
|
||||
|
||||
{doc, state}
|
||||
@@ -1776,7 +1755,7 @@ defmodule Code.Formatter do
|
||||
"(() -> "
|
||||
|> concat(nest(body_doc, :cursor))
|
||||
|> concat(")")
|
||||
|> maybe_force_clauses(clauses, state)
|
||||
|> maybe_force_clauses(clauses)
|
||||
|> group()
|
||||
|
||||
{doc, state}
|
||||
@@ -1797,7 +1776,7 @@ defmodule Code.Formatter do
|
||||
|> group()
|
||||
|> concat(break() |> concat(body_doc) |> nest(2))
|
||||
|> wrap_in_parens()
|
||||
|> maybe_force_clauses(clauses, state)
|
||||
|> maybe_force_clauses(clauses)
|
||||
|> group()
|
||||
|
||||
{doc, state}
|
||||
@@ -1816,8 +1795,8 @@ defmodule Code.Formatter do
|
||||
|
||||
## Clauses
|
||||
|
||||
defp maybe_force_clauses(doc, clauses, state) do
|
||||
if Enum.any?(clauses, fn {:->, meta, _} -> eol?(meta, state) end) do
|
||||
defp maybe_force_clauses(doc, clauses) do
|
||||
if Enum.any?(clauses, fn {:->, meta, _} -> eol?(meta) end) do
|
||||
force_unfit(doc)
|
||||
else
|
||||
doc
|
||||
@@ -1840,7 +1819,7 @@ defmodule Code.Formatter do
|
||||
{doc_acc, state_acc}
|
||||
end)
|
||||
|
||||
{clauses_doc |> maybe_force_clauses([clause | clauses], state) |> group(), state}
|
||||
{clauses_doc |> maybe_force_clauses([clause | clauses]) |> group(), state}
|
||||
end
|
||||
|
||||
defp clauses_to_algebra(other, min_line, max_line, state) do
|
||||
@@ -1978,7 +1957,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
# If the document is immediately followed by comment which is followed by newlines,
|
||||
# its newlines wouldn't have considered the comment, so we need to adjust it.
|
||||
# its newlines wouldn't have considerd the comment, so we need to adjust it.
|
||||
defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _])
|
||||
when newlines > 1 and line == doc_end + 1 do
|
||||
{doc, next_line, 1}
|
||||
@@ -2056,6 +2035,8 @@ defmodule Code.Formatter do
|
||||
{wrap_in_parens_if_operator(doc, ast), state}
|
||||
end
|
||||
|
||||
# TODO: We can remove this workaround once we remove
|
||||
# ?rearrange_uop from the parser on v2.0.
|
||||
defp wrap_in_parens_if_operator(doc, {:__block__, _, [expr]}) do
|
||||
wrap_in_parens_if_operator(doc, expr)
|
||||
end
|
||||
@@ -2111,16 +2092,21 @@ defmodule Code.Formatter do
|
||||
|
||||
defp binary_operator?(quoted) do
|
||||
case quoted do
|
||||
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
|
||||
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
|
||||
_ -> false
|
||||
{op, _, [_, _]} when is_atom(op) ->
|
||||
Code.Identifier.binary_op(op) != :error
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp unary_operator?(quoted) do
|
||||
case quoted do
|
||||
{op, _, [_]} when is_atom(op) -> Code.Identifier.unary_op(op) != :error
|
||||
_ -> false
|
||||
{op, _, [_]} when is_atom(op) ->
|
||||
Code.Identifier.unary_op(op) != :error
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2161,7 +2147,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do
|
||||
meta[:delimiter] != ~s[']
|
||||
meta[:delimeter] != ~s[']
|
||||
end
|
||||
|
||||
defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do
|
||||
@@ -2181,8 +2167,8 @@ defmodule Code.Formatter do
|
||||
false
|
||||
end
|
||||
|
||||
defp eol_or_comments?(meta, %{comments: comments} = state) do
|
||||
eol?(meta, state) or
|
||||
defp eol_or_comments?(meta, %{comments: comments}) do
|
||||
eol?(meta) or
|
||||
(
|
||||
min_line = line(meta)
|
||||
max_line = closing_line(meta)
|
||||
@@ -2269,8 +2255,9 @@ defmodule Code.Formatter do
|
||||
defp keyword_key?(_),
|
||||
do: false
|
||||
|
||||
defp eol?(_meta, %{skip_eol: true}), do: false
|
||||
defp eol?(meta, _state), do: Keyword.get(meta, :newlines, 0) > 0
|
||||
defp eol?(meta) do
|
||||
Keyword.get(meta, :newlines, 0) > 0
|
||||
end
|
||||
|
||||
defp meta?(meta, key) do
|
||||
is_list(meta[key])
|
||||
|
||||
@@ -37,15 +37,14 @@ defmodule Code.Identifier do
|
||||
op in [:"::"] -> {:right, 60}
|
||||
op in [:|] -> {:right, 70}
|
||||
op in [:=] -> {:right, 100}
|
||||
op in [:||, :|||, :or] -> {:left, 120}
|
||||
op in [:&&, :&&&, :and] -> {:left, 130}
|
||||
op in [:==, :!=, :=~, :===, :!==] -> {:left, 140}
|
||||
op in [:<, :<=, :>=, :>] -> {:left, 150}
|
||||
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 160}
|
||||
op in [:in] -> {:left, 170}
|
||||
op in [:^^^] -> {:left, 180}
|
||||
op in [:"//"] -> {:right, 190}
|
||||
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
|
||||
op in [:||, :|||, :or] -> {:left, 130}
|
||||
op in [:&&, :&&&, :and] -> {:left, 140}
|
||||
op in [:==, :!=, :=~, :===, :!==] -> {:left, 150}
|
||||
op in [:<, :<=, :>=, :>] -> {:left, 160}
|
||||
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
|
||||
op in [:in] -> {:left, 180}
|
||||
op in [:^^^] -> {:left, 190}
|
||||
op in [:++, :--, :.., :<>] -> {:right, 200}
|
||||
op in [:+, :-] -> {:left, 210}
|
||||
op in [:*, :/] -> {:left, 220}
|
||||
op in [:.] -> {:left, 310}
|
||||
@@ -69,11 +68,10 @@ defmodule Code.Identifier do
|
||||
the ambiguity between the atom and the keyword identifier
|
||||
|
||||
* `:not_callable` - an atom that cannot be used as a function call after the
|
||||
`.` operator. Those are typically AST nodes that are special forms (such as
|
||||
`:%{}` and `:<<>>>`) as well as nodes that are ambiguous in calls (such as
|
||||
`:..` and `:...`). This category also includes atoms like `:Foo`, since
|
||||
they are valid identifiers but they need quotes to be used in function
|
||||
calls (`Foo."Bar"`)
|
||||
`.` operator (for example, `:<<>>` is not callable because `Foo.<<>>` is a
|
||||
syntax error); this category includes atoms like `:Foo`, since they are
|
||||
valid identifiers but they need quotes to be used in function calls
|
||||
(`Foo."Bar"`)
|
||||
|
||||
* `:other` - any other atom (these are usually escaped when inspected, like
|
||||
`:"foo and bar"`)
|
||||
@@ -83,10 +81,10 @@ defmodule Code.Identifier do
|
||||
charlist = Atom.to_charlist(atom)
|
||||
|
||||
cond do
|
||||
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
|
||||
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :->] ->
|
||||
:not_callable
|
||||
|
||||
atom in [:"::", :"//"] ->
|
||||
atom in [:"::"] ->
|
||||
:not_atomable
|
||||
|
||||
unary_op(atom) != :error or binary_op(atom) != :error ->
|
||||
|
||||
@@ -273,13 +273,13 @@ defmodule Code.Typespec do
|
||||
{{:optional, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
|
||||
end)
|
||||
|
||||
case List.keytake(fields, :__struct__, 0) do
|
||||
{{:__struct__, struct}, fields_pruned} when is_atom(struct) and struct != nil ->
|
||||
map_pruned = {:%{}, [line: line], fields_pruned}
|
||||
{:%, [line: line], [struct, map_pruned]}
|
||||
{struct, fields} = Keyword.pop(fields, :__struct__)
|
||||
map = {:%{}, [line: line], fields}
|
||||
|
||||
_ ->
|
||||
{:%{}, [line: line], fields}
|
||||
if struct do
|
||||
{:%, [line: line], [struct, map]}
|
||||
else
|
||||
map
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ defprotocol Collectable do
|
||||
|
||||
## Examples
|
||||
|
||||
To show how to manually use the `Collectable` protocol, let's play with a
|
||||
simplified implementation for `MapSet`.
|
||||
To show how to manually use the `Collectable` protocol, let's play with its
|
||||
implementation for `MapSet`.
|
||||
|
||||
iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new())
|
||||
iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc ->
|
||||
@@ -38,33 +38,21 @@ defprotocol Collectable do
|
||||
#MapSet<[1, 2, 3]>
|
||||
|
||||
To show how the protocol can be implemented, we can again look at the
|
||||
simplified implementation for `MapSet`. In this implementation "collecting" elements
|
||||
implementation for `MapSet`. In this implementation "collecting" elements
|
||||
simply means inserting them in the set through `MapSet.put/2`.
|
||||
|
||||
defimpl Collectable, for: MapSet do
|
||||
def into(map_set) do
|
||||
def into(original) do
|
||||
collector_fun = fn
|
||||
map_set_acc, {:cont, elem} ->
|
||||
MapSet.put(map_set_acc, elem)
|
||||
|
||||
map_set_acc, :done ->
|
||||
map_set_acc
|
||||
|
||||
_map_set_acc, :halt ->
|
||||
:ok
|
||||
set, {:cont, elem} -> MapSet.put(set, elem)
|
||||
set, :done -> set
|
||||
_set, :halt -> :ok
|
||||
end
|
||||
|
||||
initial_acc = map_set
|
||||
|
||||
{initial_acc, collector_fun}
|
||||
{original, collector_fun}
|
||||
end
|
||||
end
|
||||
|
||||
So now we can call `Enum.into/2`:
|
||||
|
||||
iex> Enum.into([1, 2, 3], MapSet.new())
|
||||
#MapSet<[1, 2, 3]>
|
||||
|
||||
"""
|
||||
|
||||
@type command :: {:cont, term} | :done | :halt
|
||||
@@ -72,11 +60,8 @@ defprotocol Collectable do
|
||||
@doc """
|
||||
Returns an initial accumulator and a "collector" function.
|
||||
|
||||
Receives a `collectable` which can be used as the initial accumulator that will
|
||||
be passed to the function.
|
||||
|
||||
The collector function receives a term and a command and injects the term into
|
||||
the collectable accumulator on every `{:cont, term}` command.
|
||||
The returned function receives a term and a command and injects the term into
|
||||
the collectable on every `{:cont, term}` command.
|
||||
|
||||
`:done` is passed as a command when no further values will be injected. This
|
||||
is useful when there's a need to close resources or normalizing values. A
|
||||
@@ -88,13 +73,13 @@ defprotocol Collectable do
|
||||
For examples on how to use the `Collectable` protocol and `into/1` see the
|
||||
module documentation.
|
||||
"""
|
||||
@spec into(t) :: {initial_acc :: term, collector :: (term, command -> t | term)}
|
||||
@spec into(t) :: {term, (term, command -> t | term)}
|
||||
def into(collectable)
|
||||
end
|
||||
|
||||
defimpl Collectable, for: List do
|
||||
def into(list) do
|
||||
if list != [] do
|
||||
def into(original) do
|
||||
if original != [] do
|
||||
IO.warn(
|
||||
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
|
||||
"things like Enum.into/2 or \"for\" comprehensions with an :into option is incorrect " <>
|
||||
@@ -105,14 +90,9 @@ defimpl Collectable, for: List do
|
||||
end
|
||||
|
||||
fun = fn
|
||||
list_acc, {:cont, elem} ->
|
||||
[elem | list_acc]
|
||||
|
||||
list_acc, :done ->
|
||||
list ++ :lists.reverse(list_acc)
|
||||
|
||||
_list_acc, :halt ->
|
||||
:ok
|
||||
list, {:cont, x} -> [x | list]
|
||||
list, :done -> original ++ :lists.reverse(list)
|
||||
_, :halt -> :ok
|
||||
end
|
||||
|
||||
{[], fun}
|
||||
@@ -120,7 +100,7 @@ defimpl Collectable, for: List do
|
||||
end
|
||||
|
||||
defimpl Collectable, for: BitString do
|
||||
def into(binary) when is_binary(binary) do
|
||||
def into(original) when is_binary(original) do
|
||||
fun = fn
|
||||
acc, {:cont, x} when is_binary(x) and is_list(acc) ->
|
||||
[acc | x]
|
||||
@@ -137,14 +117,14 @@ defimpl Collectable, for: BitString do
|
||||
acc, :done ->
|
||||
IO.iodata_to_binary(acc)
|
||||
|
||||
__acc, :halt ->
|
||||
_, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{[binary], fun}
|
||||
{[original], fun}
|
||||
end
|
||||
|
||||
def into(bitstring) do
|
||||
def into(original) when is_bitstring(original) do
|
||||
fun = fn
|
||||
acc, {:cont, x} when is_bitstring(x) ->
|
||||
<<acc::bitstring, x::bitstring>>
|
||||
@@ -152,27 +132,22 @@ defimpl Collectable, for: BitString do
|
||||
acc, :done ->
|
||||
acc
|
||||
|
||||
_acc, :halt ->
|
||||
_, :halt ->
|
||||
:ok
|
||||
end
|
||||
|
||||
{bitstring, fun}
|
||||
{original, fun}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Collectable, for: Map do
|
||||
def into(map) do
|
||||
def into(original) do
|
||||
fun = fn
|
||||
map_acc, {:cont, {key, value}} ->
|
||||
Map.put(map_acc, key, value)
|
||||
|
||||
map_acc, :done ->
|
||||
map_acc
|
||||
|
||||
_map_acc, :halt ->
|
||||
:ok
|
||||
map, {:cont, {k, v}} -> Map.put(map, k, v)
|
||||
map, :done -> map
|
||||
_, :halt -> :ok
|
||||
end
|
||||
|
||||
{map, fun}
|
||||
{original, fun}
|
||||
end
|
||||
end
|
||||
|
||||
+55
-120
@@ -13,11 +13,10 @@ defmodule Config do
|
||||
key1: "value1",
|
||||
key2: "value2"
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
||||
`import Config` will import the functions `config/2`, `config/3`
|
||||
`config_env/0`, `config_target/0`, and `import_config/1`
|
||||
to help you manage your configuration.
|
||||
and `import_config/1` to help you manage your configuration.
|
||||
|
||||
`config/2` and `config/3` are used to define key-value configuration
|
||||
for a given application. Once Mix starts, it will automatically
|
||||
@@ -27,26 +26,24 @@ defmodule Config do
|
||||
|
||||
"value1" = Application.fetch_env!(:some_app, :key1)
|
||||
|
||||
Finally, the line `import_config "#{config_env()}.exs"` will import
|
||||
other config files based on the current configuration environment,
|
||||
such as `config/dev.exs` and `config/test.exs`.
|
||||
Finally, the line `import_config "#{Mix.env()}.exs"` will import other
|
||||
config files, based on the current Mix environment, such as
|
||||
`config/dev.exs` and `config/test.exs`.
|
||||
|
||||
`Config` also provides a low-level API for evaluating and reading
|
||||
configuration, under the `Config.Reader` module.
|
||||
|
||||
**Important:** 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. Also note that
|
||||
the `config/config.exs` of a library is not evaluated when the library is
|
||||
used as a dependency, as configuration is always meant to configure the
|
||||
current project. For more information, read our [library guidelines](library-guidelines.md).
|
||||
application environment is effectively a global storage. For more information,
|
||||
read our [library guidelines](library-guidelines.html).
|
||||
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
|
||||
You can leverage `Config` instead of `Mix.Config` in three steps. The first
|
||||
You can leverage `Config` instead of `Mix.Config` in two steps. The first
|
||||
step is to replace `use Mix.Config` at the top of your config files by
|
||||
`import Config`.
|
||||
|
||||
@@ -62,30 +59,43 @@ defmodule Config do
|
||||
import_config config
|
||||
end
|
||||
|
||||
The last step is to replace all `Mix.env()` calls by `config_env()`.
|
||||
## config/releases.exs
|
||||
|
||||
## config/runtime.exs
|
||||
|
||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||
It is executed right before applications start in both Mix and releases
|
||||
(assembled with `mix release`).
|
||||
If you are using releases, see `mix release`, there is another configuration
|
||||
file called `config/releases.exs`. While `config/config.exs` and friends
|
||||
mentioned in the previous section are executed whenever you run a Mix
|
||||
command, including when you assemble a release, `config/releases.exs` is
|
||||
executed every time your production system boots. Since Mix is not available
|
||||
in a production system, `config/releases.exs` must not use any of the
|
||||
functions from Mix.
|
||||
"""
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@config_key {__MODULE__, :config}
|
||||
@imports_key {__MODULE__, :imports}
|
||||
@files_key {__MODULE__, :files}
|
||||
|
||||
defp get_opts!(), do: Process.get(@opts_key)
|
||||
defp put_opts(value), do: Process.put(@opts_key, value)
|
||||
defp delete_opts(), do: Process.delete(@opts_key)
|
||||
defp get_config!() do
|
||||
Process.get(@config_key) || raise_improper_use!()
|
||||
end
|
||||
|
||||
defp get_config!(), do: Process.get(@config_key) || raise_improper_use!()
|
||||
defp put_config(value), do: Process.put(@config_key, value)
|
||||
defp delete_config(), do: Process.delete(@config_key)
|
||||
defp put_config(value) do
|
||||
Process.put(@config_key, value)
|
||||
end
|
||||
|
||||
defp get_imports!(), do: Process.get(@imports_key) || raise_improper_use!()
|
||||
defp put_imports(value), do: Process.put(@imports_key, value)
|
||||
defp delete_imports(), do: Process.delete(@imports_key)
|
||||
defp delete_config() do
|
||||
Process.delete(@config_key)
|
||||
end
|
||||
|
||||
defp get_files!() do
|
||||
Process.get(@files_key) || raise_improper_use!()
|
||||
end
|
||||
|
||||
defp put_files(value) do
|
||||
Process.put(@files_key, value)
|
||||
end
|
||||
|
||||
defp delete_files() do
|
||||
Process.delete(@files_key)
|
||||
end
|
||||
|
||||
defp raise_improper_use!() do
|
||||
raise "could not set configuration via Config. " <>
|
||||
@@ -162,55 +172,6 @@ defmodule Config do
|
||||
|> put_config()
|
||||
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, the environment when `mix release` ran.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_env() == :prod do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_env() do
|
||||
quote do
|
||||
Config.__env__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __env__!() :: atom()
|
||||
def __env__!() do
|
||||
elem(get_opts!(), 0) || raise "no :env key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the target this configuration file is executed on.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_target() == :host do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_target() do
|
||||
quote do
|
||||
Config.__target__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __target__!() :: atom()
|
||||
def __target__!() do
|
||||
elem(get_opts!(), 1) || raise "no :target key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Imports configuration from the given file.
|
||||
|
||||
@@ -223,11 +184,8 @@ defmodule Config do
|
||||
|
||||
This is often used to emulate configuration across environments:
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
||||
Note, however, some configuration files, such as `config/runtime.exs`
|
||||
does not support imports, as they are meant to be copied across
|
||||
systems.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
defmacro import_config(file) do
|
||||
@@ -240,62 +198,39 @@ defmodule Config do
|
||||
@doc false
|
||||
@spec __import__!(Path.t()) :: {term, Code.binding()}
|
||||
def __import__!(file) when is_binary(file) do
|
||||
import_config!(file, File.read!(file), true)
|
||||
current_files = get_files!()
|
||||
|
||||
if file in current_files do
|
||||
raise ArgumentError,
|
||||
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
|
||||
end
|
||||
|
||||
put_files([file | current_files])
|
||||
Code.eval_file(file)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@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)
|
||||
imports = Keyword.get(opts, :imports, [])
|
||||
|
||||
previous_opts = put_opts({env, target})
|
||||
@spec __eval__!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
|
||||
def __eval__!(file, imported_paths \\ []) when is_binary(file) and is_list(imported_paths) do
|
||||
previous_config = put_config([])
|
||||
previous_imports = put_imports(imports)
|
||||
previous_files = put_files(imported_paths)
|
||||
|
||||
try do
|
||||
{eval_config, _} = import_config!(file, content, false)
|
||||
{eval_config, _} = __import__!(Path.expand(file))
|
||||
|
||||
case get_config!() do
|
||||
[] when is_list(eval_config) ->
|
||||
{validate!(eval_config, file), get_imports!()}
|
||||
{validate!(eval_config, file), get_files!()}
|
||||
|
||||
pdict_config ->
|
||||
{pdict_config, get_imports!()}
|
||||
{pdict_config, get_files!()}
|
||||
end
|
||||
after
|
||||
if previous_opts, do: put_opts(previous_opts), else: delete_opts()
|
||||
if previous_config, do: put_config(previous_config), else: delete_config()
|
||||
if previous_imports, do: put_imports(previous_imports), else: delete_imports()
|
||||
if previous_files, do: put_files(previous_files), else: delete_files()
|
||||
end
|
||||
end
|
||||
|
||||
defp import_config!(file, contents, raise_when_disabled?) do
|
||||
current_imports = get_imports!()
|
||||
|
||||
cond do
|
||||
current_imports == :disabled ->
|
||||
if raise_when_disabled? do
|
||||
raise "import_config/1 is not enabled for this configuration file. " <>
|
||||
"Some configuration files do not allow importing other files " <>
|
||||
"as they are often copied to external systems"
|
||||
end
|
||||
|
||||
file in current_imports ->
|
||||
raise ArgumentError,
|
||||
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
|
||||
|
||||
true ->
|
||||
put_imports([file | current_imports])
|
||||
:ok
|
||||
end
|
||||
|
||||
# TODO: Emit a warning if Mix.env() is found in said files in Elixir v1.15.
|
||||
# Note this won't be a deprecation warning as it will always be emitted.
|
||||
Code.eval_string(contents, [], file: file)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __merge__(config1, config2) when is_list(config1) and is_list(config2) do
|
||||
Keyword.merge(config1, config2, fn _, app1, app2 ->
|
||||
|
||||
@@ -11,45 +11,11 @@ defmodule Config.Provider do
|
||||
the file system. For more information on runtime configuration,
|
||||
see `mix release`.
|
||||
|
||||
## Multiple config files
|
||||
## Sample config provider
|
||||
|
||||
One common use of config providers is to specify multiple
|
||||
configuration files in a release. Elixir ships with one provider,
|
||||
called `Config.Reader`, which is capable of handling Elixir's
|
||||
built-in config files.
|
||||
|
||||
For example, imagine you want to list some basic configuration
|
||||
on Mix's built-in `config/runtime.exs` file, but you also want
|
||||
some additional configuration files. To do so, you can do this
|
||||
in your `mix.exs`:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
config_providers: [
|
||||
{Config.Reader, {:system, "RELEASE_ROOT", "/extra_config.exs"}}
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
You can place this `extra_config.exs` file in your release in
|
||||
multiple ways:
|
||||
|
||||
1. If it is available on the host when assembling the release,
|
||||
you can place it on "rel/overlays/extra_config.exs" and it
|
||||
will be automatically copied to the release root
|
||||
|
||||
2. If it is available on the target during deployment, you can
|
||||
simply copy it to the release root as a step in your deployment
|
||||
|
||||
Now once the system boots, it will load both `config/runtime.exs`
|
||||
and `extra_config.exs` early in the boot process.
|
||||
|
||||
## Custom config provider
|
||||
|
||||
You can also implement custom config providers, similar to how
|
||||
`Config.Reader` works. For example, imagine you need to load
|
||||
some configuration from a JSON file and load that into the system.
|
||||
Said configuration provider would look like:
|
||||
For example, imagine you need to load some configuration from
|
||||
a JSON file and load that into the system. Said configuration
|
||||
provider would look like:
|
||||
|
||||
defmodule JSONConfigProvider do
|
||||
@behaviour Config.Provider
|
||||
@@ -73,17 +39,19 @@ defmodule Config.Provider do
|
||||
end
|
||||
end
|
||||
|
||||
Then, when specifying your release, you can specify the provider in
|
||||
Then when specifying your release, you can specify the provider in
|
||||
the release configuration:
|
||||
|
||||
releases: [
|
||||
demo: [
|
||||
config_providers: [
|
||||
{JSONConfigProvider, "/etc/config.json"}
|
||||
]
|
||||
# ...,
|
||||
config_providers: [{JSONConfigProvider, "/etc/config.json"}]
|
||||
]
|
||||
]
|
||||
|
||||
Now once the system boots, it will invoke the provider early in
|
||||
the boot process, save the merged configuration to the disk, and
|
||||
reboot the system with the new values in place.
|
||||
"""
|
||||
|
||||
@type config :: keyword
|
||||
@@ -125,7 +93,7 @@ defmodule Config.Provider do
|
||||
Loads configuration (typically during system boot).
|
||||
|
||||
It receives the current `config` and the `state` returned by
|
||||
`c:init/1`. Then, you typically read the extra configuration
|
||||
`c:init/1`. Then you typically read the extra configuration
|
||||
from an external source and merge it into the received `config`.
|
||||
Merging should be done with `Config.Reader.merge/2`, as it
|
||||
performs deep merge. It should return the updated config.
|
||||
@@ -141,13 +109,11 @@ defmodule Config.Provider do
|
||||
:providers,
|
||||
:config_path,
|
||||
extra_config: [],
|
||||
prune_runtime_sys_config_after_boot: false,
|
||||
reboot_system_after_config: false,
|
||||
prune_after_boot: false,
|
||||
reboot_after_config: true,
|
||||
validate_compile_env: false
|
||||
]
|
||||
|
||||
@reserved_apps [:kernel, :stdlib]
|
||||
|
||||
@doc """
|
||||
Validates a `t:config_path/0`.
|
||||
"""
|
||||
@@ -181,44 +147,44 @@ defmodule Config.Provider do
|
||||
def resolve_config_path!(path) when is_binary(path), do: path
|
||||
def resolve_config_path!({:system, name, path}), do: System.fetch_env!(name) <> path
|
||||
|
||||
# Private keys
|
||||
@init_key :config_provider_init
|
||||
@booted_key :config_provider_booted
|
||||
|
||||
# Public keys
|
||||
@reboot_mode_key :config_provider_reboot_mode
|
||||
|
||||
@doc false
|
||||
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)}
|
||||
init = struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
|
||||
[elixir: [{@init_key, init}]]
|
||||
struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def boot(reboot_fun \\ &restart_and_sleep/0) do
|
||||
def boot(app, key, restart_fun \\ &restart_and_sleep/0) do
|
||||
# The app with the config provider settings may not
|
||||
# have been loaded at this point, so make sure we load
|
||||
# its environment before querying it.
|
||||
_ = :application.load(app)
|
||||
|
||||
# The config provider typically runs very early in the
|
||||
# release process, so we need to make sure Elixir is started
|
||||
# before we go around running Elixir code.
|
||||
{:ok, _} = :application.ensure_all_started(:elixir)
|
||||
|
||||
case Application.fetch_env(:elixir, @booted_key) do
|
||||
# The key we store if the system already booted
|
||||
booted_key = :"#{key}_booted"
|
||||
|
||||
case :application.get_env(app, booted_key) do
|
||||
{:ok, {:booted, path}} ->
|
||||
path && File.rm(path)
|
||||
|
||||
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
|
||||
with {:ok, %Config.Provider{} = provider} <- :application.get_env(app, key) do
|
||||
maybe_validate_compile_env(provider)
|
||||
end
|
||||
|
||||
:booted
|
||||
|
||||
_ ->
|
||||
case Application.fetch_env(:elixir, @init_key) do
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
reboot_config = [elixir: [{@booted_key, booted_value(provider, path)}]]
|
||||
boot_providers(path, provider, reboot_config, reboot_fun)
|
||||
reboot_config = [{app, [{booted_key, booted_value(provider, path)}]}]
|
||||
boot_providers(path, provider, reboot_config, restart_fun)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
@@ -226,8 +192,9 @@ defmodule Config.Provider do
|
||||
end
|
||||
end
|
||||
|
||||
defp boot_providers(path, provider, reboot_config, reboot_fun) do
|
||||
defp boot_providers(path, provider, reboot_config, restart_fun) do
|
||||
validate_no_cyclic_boot!(path)
|
||||
loaded_applications = :application.loaded_applications()
|
||||
original_config = read_config!(path)
|
||||
|
||||
config =
|
||||
@@ -235,16 +202,16 @@ defmodule Config.Provider do
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_system_after_config do
|
||||
if provider.reboot_after_config do
|
||||
config
|
||||
|> Config.__merge__(reboot_config)
|
||||
|> write_config!(path)
|
||||
|
||||
reboot_fun.()
|
||||
restart_fun.()
|
||||
else
|
||||
for app <- @reserved_apps, config[app] != original_config[app] do
|
||||
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
|
||||
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
@@ -271,9 +238,9 @@ defmodule Config.Provider do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
|
||||
def validate_compile_env(compile_env) do
|
||||
for {app, [key | path], compile_return} <- compile_env,
|
||||
ensure_app_loaded?(app, ensure_loaded?) do
|
||||
Application.ensure_loaded(app) == :ok do
|
||||
try do
|
||||
traverse_env(Application.fetch_env(app, key), path)
|
||||
rescue
|
||||
@@ -308,9 +275,6 @@ defmodule Config.Provider do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_app_loaded?(app, true), do: Application.ensure_loaded(app) == :ok
|
||||
defp ensure_app_loaded?(app, false), do: Application.spec(app, :vsn) != nil
|
||||
|
||||
defp path(key, []), do: "for key #{inspect(key)}"
|
||||
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
|
||||
|
||||
@@ -335,22 +299,13 @@ defmodule Config.Provider do
|
||||
defp traverse_env(:error, _paths), do: :error
|
||||
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
|
||||
|
||||
@compile {:no_warn_undefined, {:init, :restart, 1}}
|
||||
defp restart_and_sleep() do
|
||||
mode = Application.get_env(:elixir, @reboot_mode_key)
|
||||
|
||||
# TODO: Remove otp_release check once we require Erlang/OTP 23+
|
||||
if :erlang.system_info(:otp_release) >= '23' and mode in [:embedded, :interactive] do
|
||||
:init.restart(mode: mode)
|
||||
else
|
||||
:init.restart()
|
||||
end
|
||||
|
||||
defp restart_and_sleep do
|
||||
:init.restart()
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: false}, _path), do: {:booted, nil}
|
||||
defp booted_value(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_after_boot: false}, _path), do: {:booted, nil}
|
||||
|
||||
defp validate_no_cyclic_boot!(path) do
|
||||
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
|
||||
|
||||
@@ -16,20 +16,10 @@ defmodule Config.Reader do
|
||||
|
||||
Or if you want to read a custom path inside the release:
|
||||
|
||||
config_providers: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
|
||||
|
||||
You can also pass a keyword list of options to the reader,
|
||||
where the `:path` is a required key:
|
||||
|
||||
config_providers: [
|
||||
{Config.Reader,
|
||||
path: "/etc/config.exs",
|
||||
env: :prod,
|
||||
imports: :disabled}
|
||||
]
|
||||
config_provider: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
|
||||
|
||||
Note by default Mix releases supports runtime configuration via
|
||||
a `config/runtime.exs`. If a `config/runtime.exs` exists in your
|
||||
a `config/releases.exs`. If a `config/releases.exs` exists in your
|
||||
application, it is automatically copied inside the release and
|
||||
automatically set as a config provider.
|
||||
"""
|
||||
@@ -37,71 +27,49 @@ defmodule Config.Reader do
|
||||
@behaviour Config.Provider
|
||||
|
||||
@impl true
|
||||
def init(opts) when is_list(opts) do
|
||||
{path, opts} = Keyword.pop!(opts, :path)
|
||||
Config.Provider.validate_config_path!(path)
|
||||
{path, opts}
|
||||
end
|
||||
|
||||
def init(path) do
|
||||
init(path: path)
|
||||
Config.Provider.validate_config_path!(path)
|
||||
path
|
||||
end
|
||||
|
||||
@impl true
|
||||
def load(config, {path, opts}) do
|
||||
merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the configuration `contents` for the given `file`.
|
||||
|
||||
Accepts the same options as `read!/2`.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@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)
|
||||
def load(config, path) do
|
||||
merge(config, path |> Config.Provider.resolve_config_path!() |> read!())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the configuration file.
|
||||
|
||||
## Options
|
||||
The same as `read_imports!/2` but only returns the configuration
|
||||
in the given file, without returning the imported paths.
|
||||
|
||||
* `:imports` - a list of already imported paths or `:disabled`
|
||||
to disable imports
|
||||
It exists for convenience purposes. For example, you could
|
||||
invoke it inside your `mix.exs` to read some external data
|
||||
you decided to move to a configuration file:
|
||||
|
||||
* `:env` - the environment the configuration file runs on.
|
||||
See `Config.config_env/0` for sample usage
|
||||
|
||||
* `:target` - the target the configuration file runs on.
|
||||
See `Config.config_target/0` for sample usage
|
||||
releases: Config.Reader.read!("rel/releases.exs")
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@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)
|
||||
@spec read!(Path.t(), [Path.t()]) :: keyword
|
||||
def read!(file, imported_paths \\ [])
|
||||
when is_binary(file) and is_list(imported_paths) do
|
||||
Config.__eval__!(file, imported_paths) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the given configuration file and returns the configuration
|
||||
with its imports.
|
||||
Reads the given configuration file alongside its imports.
|
||||
|
||||
Accepts the same options as `read!/2`. Although note the `:imports`
|
||||
option cannot be disabled in `read_imports!/2`.
|
||||
It accepts a list of `imported_paths` that should raise if attempted
|
||||
to be imported again (to avoid recursive imports).
|
||||
|
||||
It returns a tuple with the configuration and the imported paths.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@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"
|
||||
end
|
||||
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts)
|
||||
@spec read_imports!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, imported_paths \\ [])
|
||||
when is_binary(file) and is_list(imported_paths) do
|
||||
Config.__eval__!(file, imported_paths)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -18,14 +18,11 @@ defmodule Dict do
|
||||
message =
|
||||
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
|
||||
|
||||
@deprecated message
|
||||
defmacro __using__(_) do
|
||||
# Use this import to guarantee proper code expansion
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
if __CALLER__.module != HashDict do
|
||||
IO.warn("use Dict is deprecated. " <> unquote(message), Macro.Env.stacktrace(__CALLER__))
|
||||
end
|
||||
|
||||
quote do
|
||||
message = "Use maps and the Map module instead"
|
||||
|
||||
@@ -155,13 +152,13 @@ defmodule Dict do
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def update(dict, key, default, fun) do
|
||||
def update(dict, key, initial, fun) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
put(dict, key, fun.(value))
|
||||
|
||||
:error ->
|
||||
put(dict, key, default)
|
||||
put(dict, key, initial)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -378,8 +375,8 @@ defmodule Dict do
|
||||
|
||||
@deprecated message
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(dict, key, default, fun) do
|
||||
target(dict).update(dict, key, default, fun)
|
||||
def update(dict, key, initial, fun) do
|
||||
target(dict).update(dict, key, initial, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
|
||||
@@ -147,10 +147,10 @@ defmodule DynamicSupervisor do
|
||||
extra_arguments: [term()]
|
||||
}
|
||||
|
||||
@typedoc "Options given to `start_link` functions"
|
||||
@typedoc "Option values used by the `start*` functions"
|
||||
@type option :: GenServer.option()
|
||||
|
||||
@typedoc "Options given to `start_link` and `init/1` functions"
|
||||
@typedoc "Options given to `start_link/1` and `init/1`"
|
||||
@type init_option ::
|
||||
{:strategy, strategy()}
|
||||
| {:max_restarts, non_neg_integer()}
|
||||
@@ -418,8 +418,7 @@ defmodule DynamicSupervisor do
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec which_children(Supervisor.supervisor()) :: [
|
||||
# module() | :dynamic here because :supervisor.modules() is not exported
|
||||
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
{:undefined, pid | :restarting, :worker | :supervisor, :supervisor.modules()}
|
||||
]
|
||||
def which_children(supervisor) do
|
||||
call(supervisor, :which_children)
|
||||
|
||||
+182
-776
File diff suppressed because it is too large
Load Diff
+65
-146
@@ -21,15 +21,15 @@ defmodule Exception do
|
||||
|
||||
@typedoc "The kind handled by formatting functions"
|
||||
@type kind :: :error | non_error_kind
|
||||
@type non_error_kind :: :exit | :throw | {:EXIT, pid}
|
||||
@typep non_error_kind :: :exit | :throw | {:EXIT, pid}
|
||||
|
||||
@type stacktrace :: [stacktrace_entry]
|
||||
@type stacktrace_entry ::
|
||||
{module, atom, arity_or_args, location}
|
||||
| {(... -> any), arity_or_args, location}
|
||||
|
||||
@type arity_or_args :: non_neg_integer | list
|
||||
@type location :: keyword
|
||||
@typep arity_or_args :: non_neg_integer | list
|
||||
@typep location :: keyword
|
||||
|
||||
@callback exception(term) :: t
|
||||
@callback message(t) :: String.t()
|
||||
@@ -46,8 +46,6 @@ defmodule Exception do
|
||||
@doc """
|
||||
Returns `true` if the given `term` is an exception.
|
||||
"""
|
||||
# TODO: Remove this on Elixir v1.15
|
||||
@doc deprecated: "Use Kernel.is_exception/1 instead"
|
||||
def exception?(term)
|
||||
def exception?(%_{__exception__: true}), do: true
|
||||
def exception?(_), do: false
|
||||
@@ -190,7 +188,7 @@ defmodule Exception do
|
||||
Where `definition` is `:def`, `:defp`, `:defmacro` or `:defmacrop`.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec blame_mfa(module, function :: atom, args :: [term]) ::
|
||||
@spec blame_mfa(module, function, args :: [term]) ::
|
||||
{:ok, :def | :defp | :defmacro | :defmacrop, [{args :: [term], guards :: [term]}]}
|
||||
| :error
|
||||
def blame_mfa(module, function, args)
|
||||
@@ -239,11 +237,7 @@ defmodule Exception do
|
||||
binding = :orddict.store(:VAR, call_arg, binding)
|
||||
|
||||
try do
|
||||
ann = :erl_anno.new(0)
|
||||
|
||||
{:value, _, binding} =
|
||||
:erl_eval.expr({:match, ann, erl_arg, {:var, ann, :VAR}}, binding, :none)
|
||||
|
||||
{:value, _, binding} = :erl_eval.expr({:match, 0, erl_arg, {:var, 0, :VAR}}, binding, :none)
|
||||
{true, binding}
|
||||
rescue
|
||||
_ -> {false, binding}
|
||||
@@ -252,8 +246,8 @@ defmodule Exception do
|
||||
|
||||
defp rewrite_arg(arg) do
|
||||
Macro.prewalk(arg, fn
|
||||
{:%{}, meta, [__struct__: Range, first: first, last: last, step: step]} ->
|
||||
{:"..//", meta, [first, last, step]}
|
||||
{:%{}, meta, [__struct__: Range, first: first, last: last]} ->
|
||||
{:.., meta, [first, last]}
|
||||
|
||||
other ->
|
||||
other
|
||||
@@ -267,13 +261,7 @@ defmodule Exception do
|
||||
blame_guard(right, scope, binding)
|
||||
]
|
||||
|
||||
kernel_op =
|
||||
case op do
|
||||
:orelse -> :or
|
||||
:andalso -> :and
|
||||
end
|
||||
|
||||
{kernel_op, meta, guards}
|
||||
{rewrite_guard_call(op), meta, guards}
|
||||
end
|
||||
|
||||
defp blame_guard(ex_guard, scope, binding) do
|
||||
@@ -292,17 +280,32 @@ defmodule Exception do
|
||||
|
||||
defp rewrite_guard(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
|
||||
other -> other
|
||||
{{:., _, [:erlang, :element]}, _, [{{:., _, [:erlang, :+]}, _, [int, 1]}, arg]} ->
|
||||
{:elem, [], [arg, int]}
|
||||
|
||||
{{:., _, [:erlang, :element]}, _, [int, arg]} when is_integer(int) ->
|
||||
{:elem, [], [arg, int - 1]}
|
||||
|
||||
{:., _, [:erlang, call]} ->
|
||||
rewrite_guard_call(call)
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
end
|
||||
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
defp rewrite_guard_call(:orelse), do: :or
|
||||
defp rewrite_guard_call(:andalso), do: :and
|
||||
defp rewrite_guard_call(:"=<"), do: :<=
|
||||
defp rewrite_guard_call(:"/="), do: :!=
|
||||
defp rewrite_guard_call(:"=:="), do: :===
|
||||
defp rewrite_guard_call(:"=/="), do: :!==
|
||||
|
||||
defp rewrite_guard_call(op) when op in [:band, :bor, :bnot, :bsl, :bsr, :bxor],
|
||||
do: {:., [], [Bitwise, op]}
|
||||
|
||||
defp rewrite_guard_call(op) when op in [:xor, :element, :size], do: {:., [], [:erlang, op]}
|
||||
defp rewrite_guard_call(op), do: op
|
||||
|
||||
defp blame_wrap(match?, ast), do: %{match?: match?, node: ast}
|
||||
|
||||
@@ -635,7 +638,6 @@ defmodule Exception do
|
||||
|
||||
@doc """
|
||||
Formats the given `file` and `line` as shown in stacktraces.
|
||||
|
||||
If any of the values are `nil`, they are omitted.
|
||||
|
||||
## Examples
|
||||
@@ -651,42 +653,14 @@ defmodule Exception do
|
||||
|
||||
"""
|
||||
def format_file_line(file, line, suffix \\ "") do
|
||||
cond do
|
||||
is_nil(file) -> ""
|
||||
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
|
||||
true -> "#{file}:#{line}:#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the given `file`, `line`, and `column` as shown in stacktraces.
|
||||
|
||||
If any of the values are `nil`, they are omitted.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_file_line_column("foo", 1, 2)
|
||||
"foo:1:2:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", 1, nil)
|
||||
"foo:1:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", nil, nil)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", nil, 2)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line_column(nil, nil, nil)
|
||||
if file do
|
||||
if line && line != 0 do
|
||||
"#{file}:#{line}:#{suffix}"
|
||||
else
|
||||
"#{file}:#{suffix}"
|
||||
end
|
||||
else
|
||||
""
|
||||
|
||||
"""
|
||||
def format_file_line_column(file, line, column, suffix \\ "") do
|
||||
cond do
|
||||
is_nil(file) -> ""
|
||||
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
|
||||
is_nil(column) or column == 0 -> "#{file}:#{line}:#{suffix}"
|
||||
true -> "#{file}:#{line}:#{column}:#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -709,7 +683,7 @@ defmodule ArgumentError do
|
||||
|
||||
@impl true
|
||||
def blame(
|
||||
exception,
|
||||
%{message: "argument error"} = exception,
|
||||
[{:erlang, :apply, [module, function, args], _} | _] = stacktrace
|
||||
) do
|
||||
message =
|
||||
@@ -718,7 +692,7 @@ defmodule ArgumentError do
|
||||
not is_atom(module) and is_atom(function) and args == [] ->
|
||||
"you attempted to apply #{inspect(function)} on #{inspect(module)}. " <>
|
||||
"If you are using apply/3, make sure the module is an atom. " <>
|
||||
"If you are using the dot syntax, such as map.field or module.function(), " <>
|
||||
"If you are using the dot syntax, such as map.field or module.function, " <>
|
||||
"make sure the left side of the dot is an atom or a map"
|
||||
|
||||
not is_atom(module) ->
|
||||
@@ -783,26 +757,30 @@ defmodule ArithmeticError do
|
||||
end
|
||||
|
||||
defmodule SystemLimitError do
|
||||
defexception message: "a system limit has been reached"
|
||||
defexception []
|
||||
|
||||
@impl true
|
||||
def message(_) do
|
||||
"a system limit has been reached"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule SyntaxError do
|
||||
defexception [:file, :line, :column, description: "syntax error"]
|
||||
defexception [:file, :line, description: "syntax error"]
|
||||
|
||||
@impl true
|
||||
def message(%{file: file, line: line, column: column, description: description}) do
|
||||
Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <>
|
||||
" " <> description
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
end
|
||||
end
|
||||
|
||||
defmodule TokenMissingError do
|
||||
defexception [:file, :line, :column, description: "expression is incomplete"]
|
||||
defexception [:file, :line, description: "expression is incomplete"]
|
||||
|
||||
@impl true
|
||||
def message(%{file: file, line: line, column: column, description: description}) do
|
||||
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column) <>
|
||||
" " <> description
|
||||
def message(%{file: file, line: line, description: description}) do
|
||||
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <> " " <> description
|
||||
end
|
||||
end
|
||||
|
||||
@@ -972,7 +950,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp hint(nil, _function, 0, _loaded?) do
|
||||
". If you are using the dot syntax, such as map.field or module.function(), " <>
|
||||
". If you are using the dot syntax, such as map.field or module.function, " <>
|
||||
"make sure the left side of the dot is an atom or a map"
|
||||
end
|
||||
|
||||
@@ -987,23 +965,11 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
@doc false
|
||||
def hint_for_loaded_module(module, function, arity, exports) do
|
||||
cond do
|
||||
macro_exported?(module, function, arity) ->
|
||||
". However there is a macro with the same name and arity. " <>
|
||||
"Be sure to require #{inspect(module)} if you intend to invoke this macro"
|
||||
|
||||
message = otp_obsolete(module, function, arity) ->
|
||||
", #{message}"
|
||||
|
||||
true ->
|
||||
IO.iodata_to_binary(did_you_mean(module, function, exports))
|
||||
end
|
||||
end
|
||||
|
||||
defp otp_obsolete(module, function, arity) do
|
||||
case :otp_internal.obsolete(module, function, arity) do
|
||||
{:removed, [_ | _] = string} -> string
|
||||
_ -> nil
|
||||
if macro_exported?(module, function, arity) do
|
||||
". However there is a macro with the same name and arity. " <>
|
||||
"Be sure to require #{inspect(module)} if you intend to invoke this macro"
|
||||
else
|
||||
IO.iodata_to_binary(did_you_mean(module, function, exports))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1255,10 +1221,6 @@ defmodule KeyError do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def blame(exception = %{message: message}, stacktrace) when is_binary(message) do
|
||||
{exception, stacktrace}
|
||||
end
|
||||
|
||||
def blame(exception = %{term: nil}, stacktrace) do
|
||||
message = message(exception.key, exception.term)
|
||||
{%{exception | message: message}, stacktrace}
|
||||
@@ -1367,7 +1329,7 @@ defmodule File.CopyError do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
|
||||
location =
|
||||
case exception.on do
|
||||
case exception.on() do
|
||||
"" -> ""
|
||||
on -> ". #{on}"
|
||||
end
|
||||
@@ -1385,7 +1347,7 @@ defmodule File.RenameError do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
|
||||
location =
|
||||
case exception.on do
|
||||
case exception.on() do
|
||||
"" -> ""
|
||||
on -> ". #{on}"
|
||||
end
|
||||
@@ -1416,32 +1378,16 @@ defmodule ErlangError do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def normalize(:badarg, stacktrace) do
|
||||
case error_info(:badarg, stacktrace) do
|
||||
{:ok, args} ->
|
||||
message = "errors were found at the given arguments:\n\n#{args}"
|
||||
%ArgumentError{message: message}
|
||||
|
||||
:error ->
|
||||
%ArgumentError{}
|
||||
end
|
||||
def normalize(:badarg, _stacktrace) do
|
||||
%ArgumentError{}
|
||||
end
|
||||
|
||||
def normalize(:badarith, _stacktrace) do
|
||||
%ArithmeticError{}
|
||||
end
|
||||
|
||||
def normalize(:system_limit, stacktrace) do
|
||||
case error_info(:system_limit, stacktrace) do
|
||||
{:ok, args} ->
|
||||
message =
|
||||
"a system limit has been reached due to errors at the given arguments:\n\n#{args}"
|
||||
|
||||
%SystemLimitError{message: message}
|
||||
|
||||
:error ->
|
||||
%SystemLimitError{}
|
||||
end
|
||||
def normalize(:system_limit, _stacktrace) do
|
||||
%SystemLimitError{}
|
||||
end
|
||||
|
||||
def normalize(:cond_clause, _stacktrace) do
|
||||
@@ -1531,31 +1477,4 @@ defmodule ErlangError do
|
||||
defp from_stacktrace(_) do
|
||||
{nil, nil, nil}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def error_info(erl_exception, stacktrace) do
|
||||
with [{module, _, args_or_arity, opts} | _] <- stacktrace,
|
||||
%{} = error_info <- opts[:error_info] do
|
||||
module = Map.get(error_info, :module, module)
|
||||
function = Map.get(error_info, :function, :format_error)
|
||||
arity = if is_integer(args_or_arity), do: args_or_arity, else: length(args_or_arity)
|
||||
extra = apply(module, function, [erl_exception, stacktrace])
|
||||
args_errors = Map.take(extra, Enum.to_list(1..arity//1))
|
||||
|
||||
if map_size(args_errors) > 0 do
|
||||
{:ok, IO.iodata_to_binary(Enum.map(args_errors, &arg_error/1))}
|
||||
else
|
||||
:error
|
||||
end
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp arg_error({n, message}), do: " * #{nth(n)} argument: #{message}\n"
|
||||
|
||||
defp nth(1), do: "1st"
|
||||
defp nth(2), do: "2nd"
|
||||
defp nth(3), do: "3rd"
|
||||
defp nth(n), do: "#{n}th"
|
||||
end
|
||||
|
||||
+9
-27
@@ -110,8 +110,6 @@ defmodule File do
|
||||
|
||||
@type stream_mode ::
|
||||
encoding_mode()
|
||||
| :append
|
||||
| :compressed
|
||||
| :trim_bom
|
||||
| {:read_ahead, pos_integer | false}
|
||||
| {:delayed_write, non_neg_integer, non_neg_integer}
|
||||
@@ -737,7 +735,6 @@ defmodule File do
|
||||
File.rename("samples", "tmp")
|
||||
|
||||
"""
|
||||
@doc since: "1.1.0"
|
||||
@spec rename(Path.t(), Path.t()) :: :ok | {:error, posix}
|
||||
def rename(source, destination) do
|
||||
:file.rename(source, destination)
|
||||
@@ -764,16 +761,15 @@ defmodule File do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Copies the contents of `source_file` to `destination_file` preserving its modes.
|
||||
Copies the contents in `source_file` to `destination_file` preserving its modes.
|
||||
|
||||
`source_file` must be a file or a symbolic link to one. `destination_file` must
|
||||
be a path to a non-existent file. If either is a directory, `{:error, :eisdir}`
|
||||
will be returned.
|
||||
`source_file` and `destination_file` must be a file or a symbolic link to one,
|
||||
or in the case of destination, a path to a non-existent file. If either one of
|
||||
them is a directory, `{:error, :eisdir}` will be returned.
|
||||
|
||||
The `callback` function is invoked if the `destination_file` already exists.
|
||||
The function receives arguments for `source_file` and `destination_file`;
|
||||
it should return `true` if the existing file should be overwritten, `false` if
|
||||
otherwise. The default callback returns `true`.
|
||||
If a file already exists in the destination, it invokes a
|
||||
callback which should return `true` if the existing file
|
||||
should be overwritten, `false` otherwise. The callback defaults to return `true`.
|
||||
|
||||
The function returns `:ok` in case of success. Otherwise, it returns
|
||||
`{:error, reason}`.
|
||||
@@ -1507,12 +1503,6 @@ defmodule File do
|
||||
@doc """
|
||||
Sets the current working directory.
|
||||
|
||||
The current working directory is set for the BEAM globally. This can lead to
|
||||
race conditions if multiple processes are changing the current working
|
||||
directory concurrently. To run an external command in a given directory
|
||||
without changing the global current working directory, use the `:cd` option
|
||||
of `System.cmd/3` and `Port.open/2`.
|
||||
|
||||
Returns `:ok` if successful, `{:error, reason}` otherwise.
|
||||
"""
|
||||
@spec cd(Path.t()) :: :ok | {:error, posix}
|
||||
@@ -1542,12 +1532,6 @@ defmodule File do
|
||||
executes the given function and then reverts back
|
||||
to the previous path regardless of whether there is an exception.
|
||||
|
||||
The current working directory is temporarily set for the BEAM globally. This
|
||||
can lead to race conditions if multiple processes are changing the current
|
||||
working directory concurrently. To run an external command in a given
|
||||
directory without changing the global current working directory, use the
|
||||
`:cd` option of `System.cmd/3` and `Port.open/2`.
|
||||
|
||||
Raises an error if retrieving or changing the current
|
||||
directory fails.
|
||||
"""
|
||||
@@ -1614,9 +1598,7 @@ defmodule File do
|
||||
which means it can be used both for read and write.
|
||||
|
||||
The `line_or_bytes` argument configures how the file is read when
|
||||
streaming, by `:line` (default) or by a given number of bytes. When
|
||||
using the `:line` option, CRLF line breaks (`"\r\n"`) are normalized
|
||||
to LF (`"\n"`).
|
||||
streaming, by `:line` (default) or by a given number of bytes.
|
||||
|
||||
Operating the stream can fail on open for the same reasons as
|
||||
`File.open!/2`. Note that the file is automatically opened each time streaming
|
||||
@@ -1655,7 +1637,7 @@ defmodule File do
|
||||
|
||||
See `Stream.run/1` for an example of streaming into a file.
|
||||
"""
|
||||
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
|
||||
@spec stream!(Path.t(), stream_mode, :line | pos_integer) :: File.Stream.t()
|
||||
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
|
||||
modes = normalize_modes(modes, true)
|
||||
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
|
||||
|
||||
+40
-96
@@ -46,46 +46,6 @@ defmodule Float do
|
||||
@precision_range 0..15
|
||||
@type precision_range :: 0..15
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
|
||||
`base` must be a float and `exponent` can be any number.
|
||||
However, if a negative base and a fractional exponent
|
||||
are given, it raises `ArithmeticError`.
|
||||
|
||||
It always returns a float. See `Integer.pow/2` for
|
||||
exponentiation that returns integers.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.pow(2.0, 0)
|
||||
1.0
|
||||
iex> Float.pow(2.0, 1)
|
||||
2.0
|
||||
iex> Float.pow(2.0, 10)
|
||||
1024.0
|
||||
iex> Float.pow(2.0, -1)
|
||||
0.5
|
||||
iex> Float.pow(2.0, -3)
|
||||
0.125
|
||||
|
||||
iex> Float.pow(3.0, 1.5)
|
||||
5.196152422706632
|
||||
|
||||
iex> Float.pow(-2.0, 3)
|
||||
-8.0
|
||||
iex> Float.pow(-2.0, 4)
|
||||
16.0
|
||||
|
||||
iex> Float.pow(-1.0, 0.5)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec pow(float, number) :: float
|
||||
def pow(base, exponent) when is_float(base) and is_number(exponent),
|
||||
do: :math.pow(base, exponent)
|
||||
|
||||
@doc """
|
||||
Parses a binary into a float.
|
||||
|
||||
@@ -308,11 +268,11 @@ defmodule Float do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
defp round(0.0 = num, _precision, _rounding), do: num
|
||||
defp round(0.0, _precision, _rounding), do: 0.0
|
||||
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
{num, count} = decompose(significant, 1)
|
||||
{num, count, _} = decompose(significant, 1)
|
||||
count = count - exp + 1023
|
||||
|
||||
cond do
|
||||
@@ -364,22 +324,6 @@ defmodule Float do
|
||||
end
|
||||
end
|
||||
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, initial)
|
||||
end
|
||||
|
||||
defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
|
||||
end
|
||||
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
|
||||
decompose(bits, count + 1, last_count, acc)
|
||||
end
|
||||
|
||||
defp decompose(<<>>, _count, last_count, acc) do
|
||||
{acc, last_count}
|
||||
end
|
||||
|
||||
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
|
||||
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
|
||||
|
||||
@@ -459,55 +403,55 @@ defmodule Float do
|
||||
def ratio(0.0), do: {0, 1}
|
||||
|
||||
def ratio(float) when is_float(float) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
case <<float::float>> do
|
||||
<<sign::1, 0::11, significant::52-bitstring>> ->
|
||||
{num, _, den} = decompose(significant, 0)
|
||||
{sign(sign, num), shift_left(den, 1022)}
|
||||
|
||||
{num, den_exp} =
|
||||
if exp != 0 do
|
||||
# Floats are expressed like this:
|
||||
# (2**52 + mantissa) * 2**(-52 + exp - 1023)
|
||||
#
|
||||
# We compute the root factors of the mantissa so we have this:
|
||||
# (2**52 + mantissa * 2**count) * 2**(-52 + exp - 1023)
|
||||
{mantissa, count} = root_factors(mantissa, 0)
|
||||
<<sign::1, exp::11, significant::52-bitstring>> ->
|
||||
{num, _, den} = decompose(significant, 1)
|
||||
num = sign(sign, num)
|
||||
|
||||
# Now we can move the count around so we have this:
|
||||
# (2**(52-count) + mantissa) * 2**(count + -52 + exp - 1023)
|
||||
if mantissa == 0 do
|
||||
{1, exp - 1023}
|
||||
else
|
||||
num = (1 <<< (52 - count)) + mantissa
|
||||
den_exp = count - 52 + exp - 1023
|
||||
{num, den_exp}
|
||||
case exp - 1023 do
|
||||
exp when exp > 0 ->
|
||||
{den, exp} = shift_right(den, exp)
|
||||
{shift_left(num, exp), den}
|
||||
|
||||
exp when exp < 0 ->
|
||||
{num, shift_left(den, -exp)}
|
||||
|
||||
0 ->
|
||||
{num, den}
|
||||
end
|
||||
else
|
||||
# Subnormals are expressed like this:
|
||||
# (mantissa) * 2**(-52 + 1 - 1023)
|
||||
#
|
||||
# So we compute it to this:
|
||||
# (mantissa * 2**(count)) * 2**(-52 + 1 - 1023)
|
||||
#
|
||||
# Which becomes:
|
||||
# mantissa * 2**(count-1074)
|
||||
root_factors(mantissa, -1074)
|
||||
end
|
||||
|
||||
if den_exp > 0 do
|
||||
{sign(sign, num <<< den_exp), 1}
|
||||
else
|
||||
{sign(sign, num), 1 <<< -den_exp}
|
||||
end
|
||||
end
|
||||
|
||||
defp root_factors(mantissa, count) when mantissa != 0 and (mantissa &&& 1) == 0,
|
||||
do: root_factors(mantissa >>> 1, count + 1)
|
||||
defp decompose(significant, initial) do
|
||||
decompose(significant, 1, 0, 2, 1, initial)
|
||||
end
|
||||
|
||||
defp root_factors(mantissa, count),
|
||||
do: {mantissa, count}
|
||||
defp decompose(<<1::1, bits::bitstring>>, count, last_count, power, _last_power, acc) do
|
||||
decompose(bits, count + 1, count, power <<< 1, power, shift_left(acc, count - last_count) + 1)
|
||||
end
|
||||
|
||||
@compile {:inline, sign: 2}
|
||||
defp decompose(<<0::1, bits::bitstring>>, count, last_count, power, last_power, acc) do
|
||||
decompose(bits, count + 1, last_count, power <<< 1, last_power, acc)
|
||||
end
|
||||
|
||||
defp decompose(<<>>, _count, last_count, _power, last_power, acc) do
|
||||
{acc, last_count, last_power}
|
||||
end
|
||||
|
||||
@compile {:inline, sign: 2, shift_left: 2}
|
||||
defp sign(0, num), do: num
|
||||
defp sign(1, num), do: -num
|
||||
|
||||
defp shift_left(num, times), do: num <<< times
|
||||
|
||||
defp shift_right(num, 0), do: {num, 0}
|
||||
defp shift_right(1, times), do: {1, times}
|
||||
defp shift_right(num, times), do: shift_right(num >>> 1, times - 1)
|
||||
|
||||
@doc """
|
||||
Returns a charlist which corresponds to the text representation
|
||||
of the given float.
|
||||
|
||||
@@ -8,21 +8,8 @@ defmodule Function do
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
Anonymous functions can also have multiple clauses. All clauses
|
||||
should expect the same number of arguments:
|
||||
|
||||
iex> negate = fn
|
||||
...> true -> false
|
||||
...> false -> true
|
||||
...> end
|
||||
iex> negate.(false)
|
||||
true
|
||||
|
||||
## The capture operator
|
||||
|
||||
It is also possible to capture public module functions and pass them
|
||||
around as if they were anonymous functions by using the capture
|
||||
operator `Kernel.SpecialForms.&/1`:
|
||||
It is also possible to capture module functions and pass them around
|
||||
as if they were anonymous functions by using the capture operator `&/1`:
|
||||
|
||||
iex> add = &Kernel.+/2
|
||||
iex> add.(1, 2)
|
||||
@@ -32,9 +19,8 @@ defmodule Function do
|
||||
iex> length.("hello")
|
||||
5
|
||||
|
||||
To capture a definition within the current module, you can skip the
|
||||
module prefix, such as `&my_fun/2`. In those cases, the captured
|
||||
function can be public (`def`) or private (`defp`).
|
||||
It is also possible to capture a definition in the current module by
|
||||
skipping the module prefix, such as `&my_fun/2`.
|
||||
|
||||
The capture operator can also be used to create anonymous functions
|
||||
that expect at least one argument:
|
||||
@@ -45,8 +31,6 @@ defmodule Function do
|
||||
|
||||
In such cases, using the capture operator is no different than using `fn`.
|
||||
|
||||
## Internal and external functions
|
||||
|
||||
We say that functions that point to definitions residing in modules, such
|
||||
as `&String.length/1`, are **external** functions. All other functions are
|
||||
**local** and they are always bound to the file or module that defined them.
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule GenEvent do
|
||||
If you are interested in implementing an event manager, please read the
|
||||
"Alternatives" section below. If you have to implement an event handler to
|
||||
integrate with an existing system, such as Elixir's Logger, please use
|
||||
[`:gen_event`](`:gen_event`) instead.
|
||||
`:gen_event` instead.
|
||||
|
||||
## Alternatives
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule GenEvent do
|
||||
|
||||
If your use case requires exactly what GenEvent provided, or you have to
|
||||
integrate with an existing `:gen_event`-based system, you can still use the
|
||||
[`:gen_event`](`:gen_event`) Erlang module.
|
||||
[`:gen_event`](http://erlang.org/doc/man/gen_event.html) Erlang module.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use Erlang/OTP's :gen_event module instead"
|
||||
|
||||
@@ -61,7 +61,7 @@ defmodule GenServer do
|
||||
|
||||
Every time you do a `GenServer.call/3`, the client will send a message
|
||||
that must be handled by the `c:handle_call/3` callback in the GenServer.
|
||||
A `cast/2` message must be handled by `c:handle_cast/2`. There are 8 possible
|
||||
A `cast/2` message must be handled by `c:handle_cast/2`. There are 7 possible
|
||||
callbacks to be implemented when you use a `GenServer`. The only required
|
||||
callback is `c:init/1`.
|
||||
|
||||
@@ -163,12 +163,12 @@ defmodule GenServer do
|
||||
using `Process.register/2`.
|
||||
|
||||
* `{:global, term}` - the GenServer is registered globally with the given
|
||||
term using the functions in the [`:global` module](`:global`).
|
||||
term using the functions in the [`:global` module](http://www.erlang.org/doc/man/global.html).
|
||||
|
||||
* `{:via, module, term}` - the GenServer is registered with the given
|
||||
mechanism and name. The `:via` option expects a module that exports
|
||||
`register_name/2`, `unregister_name/1`, `whereis_name/1` and `send/2`.
|
||||
One such example is the [`:global` module](`:global`) which uses these functions
|
||||
One such example is the [`:global` module](http://www.erlang.org/doc/man/global.html) which uses these functions
|
||||
for keeping the list of names of processes and their associated PIDs
|
||||
that are available globally for a network of Elixir nodes. Elixir also
|
||||
ships with a local, decentralized and scalable registry called `Registry`
|
||||
@@ -242,8 +242,7 @@ defmodule GenServer do
|
||||
end
|
||||
|
||||
defp schedule_work do
|
||||
# We schedule the work to happen in 2 hours (written in milliseconds).
|
||||
# Alternatively, one might write :timer.hours(2)
|
||||
# In 2 hours
|
||||
Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
|
||||
end
|
||||
end
|
||||
@@ -281,10 +280,6 @@ defmodule GenServer do
|
||||
GenServer.call(__MODULE__, {:add, a, b})
|
||||
end
|
||||
|
||||
def subtract(a, b) do
|
||||
GenServer.call(__MODULE__, {:subtract, a, b})
|
||||
end
|
||||
|
||||
def handle_call({:add, a, b}, _from, state) do
|
||||
{:reply, a + b, state}
|
||||
end
|
||||
@@ -312,14 +307,14 @@ defmodule GenServer do
|
||||
|
||||
## Debugging with the :sys module
|
||||
|
||||
GenServers, as [special processes](https://erlang.org/doc/design_principles/spec_proc.html),
|
||||
can be debugged using the [`:sys` module](`:sys`).
|
||||
GenServers, as [special processes](http://erlang.org/doc/design_principles/spec_proc.html),
|
||||
can be debugged using the [`:sys` module](http://www.erlang.org/doc/man/sys.html).
|
||||
Through various hooks, this module allows developers to introspect the state of
|
||||
the process and trace system events that happen during its execution, such as
|
||||
received messages, sent replies and state changes.
|
||||
|
||||
Let's explore the basic functions from the
|
||||
[`:sys` module](`:sys`) used for debugging:
|
||||
[`:sys` module](http://www.erlang.org/doc/man/sys.html) used for debugging:
|
||||
|
||||
* `:sys.get_state/2` - allows retrieval of the state of the process.
|
||||
In the case of a GenServer process, it will be the callback module state,
|
||||
@@ -401,8 +396,8 @@ defmodule GenServer do
|
||||
in Erlang can also provide extra insight.
|
||||
|
||||
* [GenServer - Elixir's Getting Started Guide](https://elixir-lang.org/getting-started/mix-otp/genserver.html)
|
||||
* [`:gen_server` module documentation](`:gen_server`)
|
||||
* [gen_server Behaviour - OTP Design Principles](https://erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [`:gen_server` module documentation](http://www.erlang.org/doc/man/gen_server.html)
|
||||
* [gen_server Behaviour - OTP Design Principles](http://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||
|
||||
"""
|
||||
@@ -425,7 +420,7 @@ defmodule GenServer do
|
||||
`c:handle_call/3` for more information on hibernation.
|
||||
|
||||
Returning `{:ok, state, {:continue, continue}}` is similar to
|
||||
`{:ok, state}` except that immediately after entering the loop,
|
||||
`{:ok, state}` except that immediately after entering the loop
|
||||
the `c:handle_continue/2` callback will be invoked with the value
|
||||
`continue` as first argument.
|
||||
|
||||
@@ -472,8 +467,8 @@ defmodule GenServer do
|
||||
|
||||
Returning `{:reply, reply, new_state, :hibernate}` is similar to
|
||||
`{:reply, reply, new_state}` except the process is hibernated and will
|
||||
continue the loop once a message is in its message queue. However, if a message is
|
||||
already in the message queue, the process will continue the loop immediately. Hibernating a
|
||||
continue the loop once a message is in its message queue. If a message is
|
||||
already in the message queue this will be immediately. Hibernating a
|
||||
`GenServer` causes garbage collection and leaves a continuous heap that
|
||||
minimises the memory used by the process.
|
||||
|
||||
@@ -507,7 +502,7 @@ defmodule GenServer do
|
||||
occurs as with a `:reply` tuple.
|
||||
|
||||
Returning `{:stop, reason, reply, new_state}` stops the loop and `c:terminate/2`
|
||||
is called with reason `reason` and state `new_state`. Then, the `reply` is sent
|
||||
is called with reason `reason` and state `new_state`. Then the `reply` is sent
|
||||
as the response to call and the process exits with reason `reason`.
|
||||
|
||||
Returning `{:stop, reason, new_state}` is similar to
|
||||
@@ -585,6 +580,8 @@ defmodule GenServer do
|
||||
|
||||
This callback is optional. If one is not implemented, the server will fail
|
||||
if a continue instruction is used.
|
||||
|
||||
This callback is only supported on Erlang/OTP 21+.
|
||||
"""
|
||||
@callback handle_continue(continue :: term, state :: term) ::
|
||||
{:noreply, new_state}
|
||||
@@ -598,13 +595,15 @@ defmodule GenServer do
|
||||
`reason` is exit reason and `state` is the current state of the `GenServer`.
|
||||
The return value is ignored.
|
||||
|
||||
`c:terminate/2` is called if the `GenServer` traps exits (using `Process.flag/2`)
|
||||
*and* the parent process sends an exit signal, or a callback (except `c:init/1`)
|
||||
does one of the following:
|
||||
`c:terminate/2` is called if a callback (except `c:init/1`) does one of the
|
||||
following:
|
||||
|
||||
* returns a `:stop` tuple
|
||||
* raises (via `Kernel.raise/2`) or exits (via `Kernel.exit/1`)
|
||||
* raises
|
||||
* calls `Kernel.exit/1`
|
||||
* returns an invalid value
|
||||
* the `GenServer` traps exits (using `Process.flag/2`) *and* the parent
|
||||
process sends an exit signal
|
||||
|
||||
If part of a supervision tree, a `GenServer` will receive an exit
|
||||
signal when the tree is shutting down. The exit signal is based on
|
||||
@@ -711,7 +710,7 @@ defmodule GenServer do
|
||||
{:debug, debug}
|
||||
| {:name, name}
|
||||
| {:timeout, timeout}
|
||||
| {:spawn_opt, [Process.spawn_opt()]}
|
||||
| {:spawn_opt, Process.spawn_opt()}
|
||||
| {:hibernate_after, timeout}
|
||||
|
||||
@typedoc "Debug options supported by the `start*` functions"
|
||||
@@ -890,7 +889,7 @@ defmodule GenServer do
|
||||
milliseconds initializing or it will be terminated and the start function
|
||||
will return `{:error, :timeout}`
|
||||
|
||||
* `:debug` - if present, the corresponding function in the [`:sys` module](`:sys`) is invoked
|
||||
* `:debug` - if present, the corresponding function in the [`:sys` module](http://www.erlang.org/doc/man/sys.html) is invoked
|
||||
|
||||
* `:spawn_opt` - if present, its value is passed as options to the
|
||||
underlying process as in `Process.spawn/4`
|
||||
@@ -1036,8 +1035,18 @@ defmodule GenServer do
|
||||
is unknown whether the destination `server` successfully
|
||||
handled the message.
|
||||
|
||||
`c:handle_cast/2` will be called on the server to handle
|
||||
the request. In case the `server` is on a node which is
|
||||
not yet connected to the caller one, the semantics differ
|
||||
depending on the used Erlang/OTP version.
|
||||
|
||||
`server` can be any of the values described in the "Name registration"
|
||||
section of the documentation for this module.
|
||||
|
||||
Before Erlang/OTP 21, the call is going to block until a
|
||||
connection happens. This was done to guarantee ordering.
|
||||
Starting with Erlang/OTP 21, both Erlang and Elixir do
|
||||
not block the call.
|
||||
"""
|
||||
@spec cast(server, term) :: :ok
|
||||
def cast(server, request)
|
||||
@@ -1168,11 +1177,8 @@ defmodule GenServer do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `pid` or `{name, node}` of a GenServer process, `nil` otherwise.
|
||||
|
||||
To be precise, `nil` is returned whenever a `pid` or `{name, node}` cannot
|
||||
be returned. Note there is no guarantee the returned `pid` or `{name, node}`
|
||||
is alive, as a process could terminate immediately after it is looked up.
|
||||
Returns the `pid` or `{name, node}` of a GenServer process, or `nil` if
|
||||
no process is associated with the given `server`.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ defmodule HashDict do
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def update(%HashDict{root: root, size: size}, key, default, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> default end, fun, key_hash(key))
|
||||
def update(%HashDict{root: root, size: size}, key, initial, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> initial end, fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
@@ -135,25 +135,25 @@ defmodule HashDict do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(node, key, default, fun, hash) do
|
||||
defp do_update(node, key, initial, fun, hash) do
|
||||
index = key_mask(hash)
|
||||
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [key | default.()]), 1}
|
||||
{put_elem(node, index, [key | initial.()]), 1}
|
||||
|
||||
[^key | value] ->
|
||||
{put_elem(node, index, [key | fun.(value)]), 0}
|
||||
|
||||
[k | v] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | default.()])
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | initial.()])
|
||||
{put_elem(node, index, {k, v, n}), 1}
|
||||
|
||||
{^key, value, n} ->
|
||||
{put_elem(node, index, {key, fun.(value), n}), 0}
|
||||
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_update(n, key, default, fun, key_shift(hash))
|
||||
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
|
||||
{put_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
+17
-20
@@ -26,22 +26,20 @@ defprotocol Inspect do
|
||||
defimpl Inspect, for: MapSet do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(map_set, opts) do
|
||||
concat(["#MapSet<", to_doc(MapSet.to_list(map_set), opts), ">"])
|
||||
def inspect(dict, opts) do
|
||||
concat(["#MapSet<", to_doc(MapSet.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
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<"`,
|
||||
the document returned by `Inspect.Algebra.to_doc/2`, and the final
|
||||
string `">"`. We prefix the module name `#` to denote the inspect
|
||||
presentation is not actually valid Elixir syntax.
|
||||
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<"`, the document returned by
|
||||
`Inspect.Algebra.to_doc/2`, and the final string `">"`. All strings are
|
||||
valid algebra documents that keep their formatting when pretty printed.
|
||||
|
||||
Finally, note strings themselves are valid algebra documents that
|
||||
keep their formatting when pretty printed. This means your `Inspect`
|
||||
implementation may simply return a string, although that will devoid
|
||||
it of any pretty-printing.
|
||||
Since regular strings are valid entities in an algebra document,
|
||||
an implementation of the `Inspect` protocol may simply return a
|
||||
string, although that will devoid it of any pretty-printing.
|
||||
|
||||
## Error handling
|
||||
|
||||
@@ -439,7 +437,7 @@ defimpl Inspect, for: Any do
|
||||
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
module.__struct__()
|
||||
module.__struct__
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect(struct, opts)
|
||||
else
|
||||
@@ -454,17 +452,16 @@ defimpl Inspect, for: Any do
|
||||
end
|
||||
|
||||
def inspect(map, name, opts) do
|
||||
map = Map.to_list(map) ++ [:...]
|
||||
# Use the :limit option and an extra element to force
|
||||
# `container_doc/6` to append "...".
|
||||
opts = %{opts | limit: min(opts.limit, map_size(map))}
|
||||
map = Map.to_list(map) ++ ["..."]
|
||||
|
||||
open = color("#" <> name <> "<", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color(">", :map, opts)
|
||||
|
||||
fun = fn
|
||||
{key, value}, opts -> Inspect.List.keyword({key, value}, opts)
|
||||
:..., _opts -> "..."
|
||||
end
|
||||
|
||||
container_doc(open, map, close, opts, fun, separator: sep, break: :strict)
|
||||
container_doc(open, map, close, opts, &Inspect.List.keyword/2, separator: sep, break: :strict)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -31,24 +31,22 @@ defmodule Inspect.Opts do
|
||||
options. Useful when implementing the `Inspect` protocol for nested structs
|
||||
to pass the custom options through.
|
||||
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents.
|
||||
Defaults to `Inspect.inspect/2`.
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
|
||||
defaults to `Inspect.inspect/2`.
|
||||
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
bitstrings, maps, lists and any other collection of items. It does not
|
||||
apply to printable strings nor printable charlists and defaults to 50.
|
||||
If you don't want to limit the number of items to a particular number,
|
||||
use `:infinity`. It accepts a positive integer or `:infinity`.
|
||||
Defaults to `50`.
|
||||
use `:infinity`.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
|
||||
|
||||
* `:printable_limit` - limits the number of characters that are inspected
|
||||
on printable strings and printable charlists. You can use `String.printable?/1`
|
||||
and `List.ascii_printable?/1` to check if a given string or charlist is
|
||||
printable. If you don't want to limit the number of characters to a particular
|
||||
number, use `:infinity`. It accepts a positive integer or `:infinity`.
|
||||
Defaults to `4096`.
|
||||
printable. Defaults to 4096. If you don't want to limit the number of
|
||||
characters to a particular number, use `:infinity`.
|
||||
|
||||
* `:safe` - when `false`, failures while inspecting structs will be raised
|
||||
as errors instead of being wrapped in the `Inspect.Error` exception. This
|
||||
@@ -56,7 +54,7 @@ defmodule Inspect.Opts do
|
||||
implementations.
|
||||
|
||||
* `:structs` - when `false`, structs are not formatted by the inspect
|
||||
protocol, they are instead printed as maps. Defaults to `true`.
|
||||
protocol, they are instead printed as maps, defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - when set to a keyword list of colors the output is
|
||||
colorized. The keys are types and the values are the colors to use for
|
||||
@@ -65,10 +63,10 @@ defmodule Inspect.Opts do
|
||||
`:string`, and `:tuple`. Custom data types may provide their own options.
|
||||
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
|
||||
|
||||
* `:width` - number of characters per line used when pretty is `true` or when
|
||||
printing to IO devices. Set to `0` to force each item to be printed on its
|
||||
* `:width` - defaults to 80 characters, used when pretty is `true` or when
|
||||
printing to IO devices. Set to 0 to force each item to be printed on its
|
||||
own line. If you don't want to limit the number of items to a particular
|
||||
number, use `:infinity`. Defaults to `80`.
|
||||
number, use `:infinity`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -97,13 +95,13 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer | :as_lists | :as_charlists,
|
||||
custom_options: keyword,
|
||||
inspect_fun: (any, t -> Inspect.Algebra.t()),
|
||||
limit: non_neg_integer | :infinity,
|
||||
limit: pos_integer | :infinity,
|
||||
pretty: boolean,
|
||||
printable_limit: non_neg_integer | :infinity,
|
||||
printable_limit: pos_integer | :infinity,
|
||||
safe: boolean,
|
||||
structs: boolean,
|
||||
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
|
||||
width: non_neg_integer | :infinity
|
||||
width: pos_integer | :infinity
|
||||
}
|
||||
end
|
||||
|
||||
@@ -150,7 +148,7 @@ defmodule Inspect.Algebra do
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", " ", "b"]
|
||||
|
||||
Note that the break was represented as is, because we haven't reached
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
@@ -268,9 +266,6 @@ defmodule Inspect.Algebra do
|
||||
when is_binary(doc) or doc in [:doc_nil, :doc_line] or
|
||||
(is_tuple(doc) and elem(doc, 0) in @docs)
|
||||
|
||||
defguardp is_limit(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
|
||||
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
|
||||
|
||||
# Elixir + Inspect.Opts conveniences
|
||||
|
||||
@doc """
|
||||
@@ -402,14 +397,13 @@ defmodule Inspect.Algebra do
|
||||
{:lists.reverse(["..." | acc]), simple?}
|
||||
end
|
||||
|
||||
defp container_each([term | terms], limit, opts, fun, acc, simple?)
|
||||
when is_list(terms) and is_limit(limit) do
|
||||
defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
|
||||
limit = decrement(limit)
|
||||
doc = fun.(term, %{opts | limit: limit})
|
||||
container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
|
||||
end
|
||||
|
||||
defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do
|
||||
defp container_each([left | right], limit, opts, fun, acc, simple?) do
|
||||
limit = decrement(limit)
|
||||
left = fun.(left, %{opts | limit: limit})
|
||||
right = fun.(right, %{opts | limit: limit})
|
||||
@@ -601,7 +595,7 @@ defmodule Inspect.Algebra do
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", "\t", "b"]
|
||||
|
||||
Note that the break was represented with the given string, because we didn't
|
||||
Notice the break was represented with the given string, because we didn't
|
||||
reach a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> break = Inspect.Algebra.break("\t")
|
||||
@@ -890,28 +884,23 @@ defmodule Inspect.Algebra do
|
||||
|
||||
"""
|
||||
@spec format(t, non_neg_integer | :infinity) :: iodata
|
||||
def format(doc, width) when is_doc(doc) and is_width(width) do
|
||||
def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
|
||||
format(width, 0, [{0, :flat, doc}])
|
||||
end
|
||||
|
||||
# Type representing the document mode to be rendered:
|
||||
# Type representing the document mode to be rendered
|
||||
#
|
||||
# * flat - represents a document with breaks as flats (a break may fit, as it may break)
|
||||
# * break - represents a document with breaks as breaks (a break always fits, since it breaks)
|
||||
#
|
||||
# The following modes are exclusive to fitting:
|
||||
# The following modes are exclusive to fitting
|
||||
#
|
||||
# * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode
|
||||
# * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode
|
||||
#
|
||||
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
|
||||
|
||||
@spec fits?(
|
||||
width :: non_neg_integer(),
|
||||
column :: non_neg_integer(),
|
||||
break? :: boolean(),
|
||||
entries
|
||||
) :: boolean()
|
||||
@spec fits?(width :: integer(), column :: integer(), break? :: boolean(), entries) :: boolean()
|
||||
when entries:
|
||||
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
|
||||
|
||||
@@ -972,9 +961,7 @@ defmodule Inspect.Algebra do
|
||||
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
|
||||
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
|
||||
|
||||
@spec format(width :: non_neg_integer() | :infinity, column :: non_neg_integer(), [
|
||||
{integer, mode, t}
|
||||
]) :: [binary]
|
||||
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
|
||||
defp format(_, _, []), do: []
|
||||
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
|
||||
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
|
||||
|
||||
+42
-113
@@ -64,55 +64,6 @@ defmodule Integer do
|
||||
"""
|
||||
defguard is_even(integer) when is_integer(integer) and (integer &&& 1) == 0
|
||||
|
||||
@doc """
|
||||
Computes `base` raised to power of `exponent`.
|
||||
|
||||
Both `base` and `exponent` must be integers.
|
||||
The exponent must be zero or positive.
|
||||
|
||||
See `Float.pow/2` for exponentiation of negative
|
||||
exponents as well as floats.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.pow(2, 0)
|
||||
1
|
||||
iex> Integer.pow(2, 1)
|
||||
2
|
||||
iex> Integer.pow(2, 10)
|
||||
1024
|
||||
iex> Integer.pow(2, 11)
|
||||
2048
|
||||
iex> Integer.pow(2, 64)
|
||||
0x10000000000000000
|
||||
|
||||
iex> Integer.pow(3, 4)
|
||||
81
|
||||
iex> Integer.pow(4, 3)
|
||||
64
|
||||
|
||||
iex> Integer.pow(-2, 3)
|
||||
-8
|
||||
iex> Integer.pow(-2, 4)
|
||||
16
|
||||
|
||||
iex> Integer.pow(2, -2)
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec pow(integer, non_neg_integer) :: integer
|
||||
def pow(base, exponent) when is_integer(base) and is_integer(exponent) do
|
||||
if exponent < 0, do: :erlang.error(:badarith, [base, exponent])
|
||||
guarded_pow(base, exponent)
|
||||
end
|
||||
|
||||
# https://en.wikipedia.org/wiki/Exponentiation_by_squaring
|
||||
defp guarded_pow(_, 0), do: 1
|
||||
defp guarded_pow(b, 1), do: b
|
||||
defp guarded_pow(b, e) when (e &&& 1) == 0, do: guarded_pow(b * b, e >>> 1)
|
||||
defp guarded_pow(b, e), do: b * guarded_pow(b * b, e >>> 1)
|
||||
|
||||
@doc """
|
||||
Computes the modulo remainder of an integer division.
|
||||
|
||||
@@ -275,7 +226,7 @@ defmodule Integer do
|
||||
** (ArgumentError) invalid base 38
|
||||
|
||||
"""
|
||||
@spec parse(binary, 2..36) :: {integer, remainder_of_binary :: binary} | :error
|
||||
@spec parse(binary, 2..36) :: {integer, binary} | :error
|
||||
def parse(binary, base \\ 10)
|
||||
|
||||
def parse(_binary, base) when base not in 2..36 do
|
||||
@@ -318,12 +269,12 @@ defmodule Integer do
|
||||
|
||||
defp count_digits_nosign(<<_::bits>>, _, count), do: count
|
||||
|
||||
# TODO: Remove Integer.to_string/1 once the minimum supported version is
|
||||
# Erlang/OTP 22, since it is covered by the now BIF Integer.to_string/2.
|
||||
# Please reapply commit 2622fd6b0aa419a983a899a1fbdb5deefba3d85d.
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36. If no `base` is given,
|
||||
it defaults to `10`.
|
||||
of `integer`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -341,6 +292,22 @@ defmodule Integer do
|
||||
iex> Integer.to_string(0123)
|
||||
"123"
|
||||
|
||||
"""
|
||||
@spec to_string(integer) :: String.t()
|
||||
def to_string(integer) do
|
||||
:erlang.integer_to_binary(integer)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a binary which corresponds to the text representation
|
||||
of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_string(100, 16)
|
||||
"64"
|
||||
|
||||
@@ -352,16 +319,15 @@ defmodule Integer do
|
||||
|
||||
"""
|
||||
@spec to_string(integer, 2..36) :: String.t()
|
||||
def to_string(integer, base \\ 10) do
|
||||
def to_string(integer, base) do
|
||||
:erlang.integer_to_binary(integer, base)
|
||||
end
|
||||
|
||||
# TODO: Remove Integer.to_charlist/1 once the minimum supported version is
|
||||
# Erlang/OTP 22, since it is covered by the now BIF Integer.to_charlist/2.
|
||||
# Please reapply commit 2622fd6b0aa419a983a899a1fbdb5deefba3d85d.
|
||||
@doc """
|
||||
Returns a charlist which corresponds to the text representation
|
||||
of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36. If no `base` is given,
|
||||
it defaults to `10`.
|
||||
Returns a charlist which corresponds to the text representation of the given `integer`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -379,6 +345,21 @@ defmodule Integer do
|
||||
iex> Integer.to_charlist(0123)
|
||||
'123'
|
||||
|
||||
"""
|
||||
@spec to_charlist(integer) :: charlist
|
||||
def to_charlist(integer) do
|
||||
:erlang.integer_to_list(integer)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a charlist which corresponds to the text representation of `integer` in the given `base`.
|
||||
|
||||
`base` can be an integer between 2 and 36.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.to_charlist(100, 16)
|
||||
'64'
|
||||
|
||||
@@ -390,7 +371,7 @@ defmodule Integer do
|
||||
|
||||
"""
|
||||
@spec to_charlist(integer, 2..36) :: charlist
|
||||
def to_charlist(integer, base \\ 10) do
|
||||
def to_charlist(integer, base) do
|
||||
:erlang.integer_to_list(integer, base)
|
||||
end
|
||||
|
||||
@@ -433,58 +414,6 @@ defmodule Integer do
|
||||
defp gcd_positive(integer1, 0), do: integer1
|
||||
defp gcd_positive(integer1, integer2), do: gcd_positive(integer2, rem(integer1, integer2))
|
||||
|
||||
@doc """
|
||||
Returns the extended greatest common divisor of the two given integers.
|
||||
|
||||
It uses the Extended Euclidean algorithm to return a three-element tuple with the `gcd`
|
||||
and the coefficients `m` and `n` of Bézout's identity such that:
|
||||
|
||||
gcd(a, b) = m*a + n*b
|
||||
|
||||
By convention, `extended_gcd(0, 0)` returns `{0, 0, 0}`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.extended_gcd(240, 46)
|
||||
{2, -9, 47}
|
||||
iex> Integer.extended_gcd(46, 240)
|
||||
{2, 47, -9}
|
||||
iex> Integer.extended_gcd(-46, 240)
|
||||
{2, -47, -9}
|
||||
iex> Integer.extended_gcd(-46, -240)
|
||||
{2, -47, 9}
|
||||
|
||||
iex> Integer.extended_gcd(14, 21)
|
||||
{7, -1, 1}
|
||||
|
||||
iex> Integer.extended_gcd(10, 0)
|
||||
{10, 1, 0}
|
||||
iex> Integer.extended_gcd(0, 10)
|
||||
{10, 0, 1}
|
||||
iex> Integer.extended_gcd(0, 0)
|
||||
{0, 0, 0}
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec extended_gcd(integer, integer) :: {non_neg_integer, integer, integer}
|
||||
def extended_gcd(0, 0), do: {0, 0, 0}
|
||||
def extended_gcd(0, n), do: {n, 0, 1}
|
||||
def extended_gcd(n, 0), do: {n, 1, 0}
|
||||
|
||||
def extended_gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
|
||||
extended_gcd(integer2, integer1, 0, 1, 1, 0)
|
||||
end
|
||||
|
||||
defp extended_gcd(r1, r0, s1, s0, t1, t0) do
|
||||
div = div(r0, r1)
|
||||
|
||||
case r0 - div * r1 do
|
||||
0 when r1 > 0 -> {r1, s1, t1}
|
||||
0 when r1 < 0 -> {-r1, -s1, -t1}
|
||||
r2 -> extended_gcd(r2, r1, s0 - div * s1, s1, t0 - div * t1, t1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Integer.to_charlist/1 instead"
|
||||
def to_char_list(integer), do: Integer.to_charlist(integer)
|
||||
|
||||
+12
-55
@@ -34,7 +34,7 @@ defmodule IO do
|
||||
IO data is a data type that can be used as a more efficient alternative to binaries
|
||||
in certain situations.
|
||||
|
||||
A term of type **IO data** is a binary or a list containing bytes (integers within the `0..255` range)
|
||||
A term of type **IO data** is a binary or a list containing bytes (integers in `0..255`)
|
||||
or nested IO data. The type is recursive. Let's see an example of one of
|
||||
the possible IO data representing the binary `"hello"`:
|
||||
|
||||
@@ -99,8 +99,8 @@ defmodule IO do
|
||||
Erlang and Elixir also have the idea of `t:chardata/0`. Chardata is very
|
||||
similar to IO data: the only difference is that integers in IO data represent
|
||||
bytes while integers in chardata represent Unicode code points. Bytes
|
||||
(`t:byte/0`) are integers within the `0..255` range, while Unicode code points
|
||||
(`t:char/0`) are integers within the `0..0x10FFFF` range. The `IO` module provides
|
||||
(`t:byte/0`) are integers in the `0..255` range, while Unicode code points
|
||||
(`t:char/0`) are integers in the range `0..0x10FFFF`. The `IO` module provides
|
||||
the `chardata_to_string/1` function for chardata as the "counter-part" of the
|
||||
`iodata_to_binary/1` function for IO data.
|
||||
|
||||
@@ -108,8 +108,8 @@ defmodule IO do
|
||||
argument error. For example, let's try to put a code point that is not
|
||||
representable with one byte, like `?π`, inside IO data:
|
||||
|
||||
IO.iodata_to_binary(["The symbol for pi is: ", ?π])
|
||||
#=> ** (ArgumentError) argument error
|
||||
iex> IO.iodata_to_binary(["The symbol for pi is: ", ?π])
|
||||
** (ArgumentError) argument error
|
||||
|
||||
If we use chardata instead, it will work as expected:
|
||||
|
||||
@@ -334,28 +334,11 @@ defmodule IO do
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def warn_once(key, message, stacktrace_drop_levels) do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
|
||||
|
||||
if :elixir_config.warn(key, stacktrace) do
|
||||
warn(message, stacktrace)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes a `message` to stderr, along with the current stacktrace.
|
||||
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
Do not call this function at the tail of another function. Due to tail
|
||||
call optimization, a stacktrace entry would not be added and the
|
||||
stacktrace would be incorrectly trimmed. Therefore make sure at least
|
||||
one expression (or an atom such as `:ok`) follows the `IO.warn/1` call.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.warn("variable bar is unused")
|
||||
@@ -447,8 +430,8 @@ defmodule IO do
|
||||
See `IO.getn/3` for a description of return values.
|
||||
|
||||
"""
|
||||
@spec getn(device | chardata | String.Chars.t(), pos_integer | chardata | String.Chars.t()) ::
|
||||
chardata | nodata
|
||||
@spec getn(chardata | String.Chars.t(), pos_integer) :: chardata | nodata
|
||||
@spec getn(device, chardata | String.Chars.t()) :: chardata | nodata
|
||||
def getn(prompt, count \\ 1)
|
||||
|
||||
def getn(prompt, count) when is_integer(count) and count > 0 do
|
||||
@@ -508,17 +491,6 @@ defmodule IO do
|
||||
:io.get_line(map_dev(device), to_chardata(prompt))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a line-based `IO.Stream` on `:stdio`.
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
IO.stream(:stdio, :line)
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def stream, do: stream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
Converts the IO `device` into an `IO.Stream`.
|
||||
|
||||
@@ -535,9 +507,6 @@ defmodule IO do
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
|
||||
`stream/1` has been introduced in Elixir v1.12.0,
|
||||
while `stream/2` has been available since v1.0.0.
|
||||
|
||||
## Examples
|
||||
|
||||
Here is an example on how we mimic an echo server
|
||||
@@ -547,23 +516,12 @@ defmodule IO do
|
||||
|
||||
"""
|
||||
@spec stream(device, :line | pos_integer) :: Enumerable.t()
|
||||
def stream(device \\ :stdio, line_or_codepoints)
|
||||
def stream(device, line_or_codepoints)
|
||||
when line_or_codepoints == :line
|
||||
when is_integer(line_or_codepoints) and line_or_codepoints > 0 do
|
||||
IO.Stream.__build__(map_dev(device), false, line_or_codepoints)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a raw, line-based `IO.Stream` on `:stdio`. The operation is Unicode unsafe.
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
IO.binstream(:stdio, :line)
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def binstream, do: binstream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
Converts the IO `device` into an `IO.Stream`. The operation is Unicode unsafe.
|
||||
|
||||
@@ -572,7 +530,8 @@ defmodule IO do
|
||||
and write.
|
||||
|
||||
The `device` is iterated by the given number of bytes or line by line if
|
||||
`:line` is given. This reads from the IO device as a raw binary.
|
||||
`:line` is given.
|
||||
This reads from the IO device as a raw binary.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
@@ -580,11 +539,9 @@ defmodule IO do
|
||||
Finally, do not use this function on IO devices in Unicode
|
||||
mode as it will return the wrong result.
|
||||
|
||||
`binstream/1` has been introduced in Elixir v1.12.0,
|
||||
while `binstream/2` has been available since v1.0.0.
|
||||
"""
|
||||
@spec binstream(device, :line | pos_integer) :: Enumerable.t()
|
||||
def binstream(device \\ :stdio, line_or_bytes)
|
||||
def binstream(device, line_or_bytes)
|
||||
when line_or_bytes == :line
|
||||
when is_integer(line_or_bytes) and line_or_bytes > 0 do
|
||||
IO.Stream.__build__(map_dev(device), true, line_or_bytes)
|
||||
@@ -625,7 +582,7 @@ defmodule IO do
|
||||
|
||||
The operation is Unicode unsafe.
|
||||
|
||||
Note that this function treats integers in the given IO data as
|
||||
Notice that this function treats integers in the given IO data as
|
||||
raw bytes and does not perform any kind of encoding conversion.
|
||||
If you want to convert from a charlist to a UTF-8-encoded string,
|
||||
use `chardata_to_string/1` instead. For more information about
|
||||
|
||||
@@ -21,31 +21,6 @@ defmodule IO.ANSI do
|
||||
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
are characters embedded in text used to control formatting, color, and
|
||||
other output options on video text terminals.
|
||||
|
||||
ANSI escapes are typically enabled on all Unix terminals. They are also
|
||||
available on Windows consoles from Windows 10, although it must be
|
||||
explicitly enabled for the current user in the registry by running the
|
||||
following command:
|
||||
|
||||
reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
|
||||
|
||||
After running the command above, you must restart your current console.
|
||||
|
||||
## Examples
|
||||
|
||||
Because the ANSI escape sequences are embedded in text, the normal usage of
|
||||
these functions is to concatenate their output with text.
|
||||
|
||||
formatted_text = IO.ANSI.blue_background() <> "Example" <> IO.ANSI.reset()
|
||||
IO.puts(formatted_text)
|
||||
|
||||
A higher level and more convenient API is also available via `IO.ANSI.format/1`,
|
||||
where you use atoms to represent each ANSI escape sequence and by default
|
||||
checks if ANSI is enabled:
|
||||
|
||||
IO.puts(IO.ANSI.format([:blue_background, "Example"]))
|
||||
|
||||
In case ANSI is disabled, the ANSI escape sequences are simply discarded.
|
||||
"""
|
||||
|
||||
import IO.ANSI.Sequence
|
||||
@@ -242,7 +217,7 @@ defmodule IO.ANSI do
|
||||
performed. If you don't want this behaviour, use `format_fragment/2`.
|
||||
|
||||
An optional boolean parameter can be passed to enable or disable
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will emitted.
|
||||
By default checks if ANSI is enabled using the `enabled?/0` function.
|
||||
|
||||
## Examples
|
||||
|
||||
+48
-269
@@ -1,8 +1,6 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullet_text_unicode "• "
|
||||
@bullet_text_ascii "* "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@spaces [" ", "\n", "\t"]
|
||||
|
||||
@@ -48,20 +46,15 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
@spec print_heading(String.t(), keyword) :: :ok
|
||||
def print_heading(heading, options \\ []) do
|
||||
IO.puts(IO.ANSI.reset())
|
||||
options = Keyword.merge(default_options(), options)
|
||||
newline_after_block(options)
|
||||
width = options[:width]
|
||||
|
||||
for heading <- headings do
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = String.pad_leading(heading, padding)
|
||||
heading = if options[:enabled], do: String.pad_trailing(heading, width), else: heading
|
||||
write(:doc_title, heading, options)
|
||||
end
|
||||
|
||||
newline_after_block(options)
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = heading |> String.pad_leading(padding) |> String.pad_trailing(width)
|
||||
write(:doc_title, heading, options)
|
||||
newline_after_block()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -85,11 +78,11 @@ defmodule IO.ANSI.Docs do
|
||||
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
|
||||
|
||||
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
|
||||
IO.puts([metadata_label(key, options), ?\s, to_string(value)])
|
||||
IO.puts([metadata_label(key, options), ' ', to_string(value)])
|
||||
|
||||
{:delegate_to, {m, f, a}}, _printed ->
|
||||
label = metadata_label(:delegate_to, options)
|
||||
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
|
||||
IO.puts([label, ' ', Exception.format_mfa(m, f, a)])
|
||||
|
||||
_metadata, printed ->
|
||||
printed
|
||||
@@ -97,210 +90,21 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp metadata_label(key, options) do
|
||||
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
|
||||
if options[:enabled] do
|
||||
"#{color(:doc_metadata, options)}#{key}:#{IO.ANSI.reset()}"
|
||||
else
|
||||
"#{key}:"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the documentation body `doc` according to `format`.
|
||||
Prints the documentation body.
|
||||
|
||||
It takes a set of `options` defined in `default_options/0`.
|
||||
In addition to the printing string, takes a set of `options`
|
||||
defined in `default_options/0`.
|
||||
"""
|
||||
@spec print(term(), String.t(), keyword) :: :ok
|
||||
def print(doc, format, options \\ [])
|
||||
|
||||
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
|
||||
print_markdown(doc, options)
|
||||
end
|
||||
|
||||
def print(doc, "application/erlang+html", options) when is_list(options) do
|
||||
print_erlang_html(doc, options)
|
||||
end
|
||||
|
||||
def print(_doc, format, options) when is_binary(format) and is_list(options) do
|
||||
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
|
||||
end
|
||||
|
||||
## Erlang+html
|
||||
|
||||
def print_erlang_html(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
IO.write(traverse_erlang_html(doc, "", options))
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
|
||||
text
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
|
||||
for node <- nodes do
|
||||
traverse_erlang_html(node, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
|
||||
prefix = indent <> quote_prefix(options)
|
||||
|
||||
content =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing()
|
||||
|
||||
[
|
||||
prefix,
|
||||
class |> to_string() |> String.upcase(),
|
||||
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:p, _, entries}, indent, options) do
|
||||
[indent | handle_erlang_html_text(entries, indent, options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:i, _, entries}, indent, options) do
|
||||
inline_text("_", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:em, _, entries}, indent, options) do
|
||||
inline_text("*", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:code, _, entries}, indent, options) do
|
||||
inline_text("`", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
|
||||
string =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
|
||||
if href = attributes[:href] do
|
||||
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
|
||||
else
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(entries, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
|
||||
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
|
||||
if attributes[:class] == "types" do
|
||||
types =
|
||||
for {:li, _, lines} <- entries,
|
||||
line <- lines,
|
||||
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
|
||||
|
||||
if types != [] do
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
for {:li, _, lines} <- entries do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
|
||||
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
|
||||
[
|
||||
"#{indent} ",
|
||||
Integer.to_string(i),
|
||||
". " | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) do
|
||||
[
|
||||
indent <> "<#{tag}>\n",
|
||||
traverse_erlang_html(entries, indent <> " ", options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing(),
|
||||
"\n" <> indent <> "</#{tag}>"
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp newline_cons(text) do
|
||||
[text | "\n\n"]
|
||||
end
|
||||
|
||||
defp handle_erlang_html_text(entries, indent, options) do
|
||||
if Enum.all?(entries, &inline_html?/1) do
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split(@spaces)
|
||||
|> wrap_text(options[:width], indent, true, "", [])
|
||||
|> tl()
|
||||
|> newline_cons()
|
||||
else
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_leading()
|
||||
end
|
||||
end
|
||||
|
||||
defp inline_html?(binary) when is_binary(binary), do: true
|
||||
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :br], do: true
|
||||
defp inline_html?(_), do: false
|
||||
|
||||
## Markdown
|
||||
|
||||
def print_markdown(doc, options) do
|
||||
@spec print(String.t(), keyword) :: :ok
|
||||
def print(doc, options \\ []) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
|
||||
doc
|
||||
@@ -377,16 +181,16 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
### Headings
|
||||
## Headings
|
||||
|
||||
defp write_heading(heading, rest, text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write(:doc_headings, heading, options)
|
||||
newline_after_block(options)
|
||||
newline_after_block()
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
### Quotes
|
||||
## Quotes
|
||||
|
||||
defp process_quote([], lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
@@ -419,19 +223,21 @@ defmodule IO.ANSI.Docs do
|
||||
)
|
||||
end
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
|
||||
|
||||
defp write_empty_quote_line(options) do
|
||||
options
|
||||
|> quote_prefix()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
### Lists
|
||||
## Lists
|
||||
|
||||
defp process_rest(stripped, rest, count, text, indent, options) do
|
||||
case stripped do
|
||||
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
|
||||
write_text(text, indent, options)
|
||||
process_list(bullet_text(options), item, rest, count, indent, options)
|
||||
process_list("• ", item, rest, count, indent, options)
|
||||
|
||||
<<d1, ?., ?\s, item::binary>> when d1 in ?0..?9 ->
|
||||
write_text(text, indent, options)
|
||||
@@ -451,12 +257,10 @@ defmodule IO.ANSI.Docs do
|
||||
entry = if indent == "", do: " " <> entry, else: entry
|
||||
new_indent = indent <> String.duplicate(" ", String.length(entry))
|
||||
|
||||
{contents, rest, done} =
|
||||
process_list_next(rest, count, byte_size(new_indent) - byte_size(indent), [])
|
||||
|
||||
{contents, rest, done} = process_list_next(rest, count, byte_size(new_indent), [])
|
||||
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
|
||||
|
||||
if done, do: newline_after_block(options)
|
||||
if done, do: newline_after_block()
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
@@ -497,7 +301,7 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
### Text
|
||||
## Text
|
||||
|
||||
defp write_text(text, indent, options) do
|
||||
case Enum.reverse(text) do
|
||||
@@ -521,7 +325,7 @@ defmodule IO.ANSI.Docs do
|
||||
|> String.split(@spaces)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
|
||||
|
||||
unless no_wrap, do: newline_after_block(options)
|
||||
unless no_wrap, do: newline_after_block()
|
||||
end
|
||||
|
||||
defp format_text(text, options) do
|
||||
@@ -530,7 +334,7 @@ defmodule IO.ANSI.Docs do
|
||||
|> handle_inline(options)
|
||||
end
|
||||
|
||||
### Code blocks
|
||||
## Code blocks
|
||||
|
||||
defp process_code([], code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
@@ -569,15 +373,15 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
defp write_code(code, indent, options) do
|
||||
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
|
||||
newline_after_block(options)
|
||||
newline_after_block()
|
||||
end
|
||||
|
||||
### Tables
|
||||
## Tables
|
||||
|
||||
defp process_table(lines, indent, options) do
|
||||
{table, rest} = Enum.split_while(lines, &table_line?/1)
|
||||
table_lines(table, options)
|
||||
newline_after_block(options)
|
||||
newline_after_block()
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
@@ -603,7 +407,7 @@ defmodule IO.ANSI.Docs do
|
||||
line
|
||||
|> String.trim(" ")
|
||||
|> String.trim("|")
|
||||
|> String.split(~r{(?<!\\)\|})
|
||||
|> String.split("|")
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
@@ -720,7 +524,7 @@ defmodule IO.ANSI.Docs do
|
||||
defp strip_spaces(rest, acc, _max), do: {rest, acc}
|
||||
|
||||
defp write(style, string, options) do
|
||||
IO.puts([color(style, options), string, maybe_reset(options)])
|
||||
IO.puts([color(style, options), string, IO.ANSI.reset()])
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first, _prefix) do
|
||||
@@ -730,7 +534,7 @@ defmodule IO.ANSI.Docs do
|
||||
defp write_with_wrap(words, available, indent, first, prefix) do
|
||||
words
|
||||
|> wrap_text(available, indent, first, prefix, [])
|
||||
|> tl()
|
||||
|> Enum.join("\n")
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
@@ -743,7 +547,7 @@ defmodule IO.ANSI.Docs do
|
||||
{words, rest} = take_words(words, available - prefix_length, [])
|
||||
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
|
||||
|
||||
wrap_text(rest, available, indent, false, prefix, [line, ?\n | wrapped_lines])
|
||||
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
|
||||
end
|
||||
|
||||
defp take_words([word | words], available, acc) do
|
||||
@@ -817,7 +621,7 @@ defmodule IO.ANSI.Docs do
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
### Inline start
|
||||
# Inline start
|
||||
|
||||
defp handle_inline(<<?*, ?*, rest::binary>>, options) do
|
||||
handle_inline(rest, ?d, ["**"], [], options)
|
||||
@@ -831,7 +635,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, nil, [], [], options)
|
||||
end
|
||||
|
||||
### Inline delimiters
|
||||
# Inline delimiters
|
||||
|
||||
defp handle_inline(<<delimiter, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" and delimiter in @delimiters do
|
||||
@@ -848,7 +652,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
|
||||
end
|
||||
|
||||
### Clauses for handling escape
|
||||
# Clauses for handling escape
|
||||
|
||||
defp handle_inline(<<?\\, ?\\, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" do
|
||||
@@ -869,7 +673,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, limit, [mark | buffer], acc, options)
|
||||
end
|
||||
|
||||
### Inline end
|
||||
# Inline end
|
||||
|
||||
defp handle_inline(<<?*, ?*, delimiter, rest::binary>>, ?d, buffer, acc, options)
|
||||
when delimiter in @delimiters do
|
||||
@@ -897,7 +701,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options) | acc], options)
|
||||
end
|
||||
|
||||
### Catch all
|
||||
# Catch all
|
||||
|
||||
defp handle_inline(<<char, rest::binary>>, mark, buffer, acc, options) do
|
||||
handle_inline(rest, mark, [char | buffer], acc, options)
|
||||
@@ -908,24 +712,8 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp inline_buffer(buffer, options) do
|
||||
[mark | t] = Enum.reverse(buffer)
|
||||
inline_text(mark, t, options)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
|
||||
|
||||
defp heading(text, n, options) do
|
||||
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
|
||||
end
|
||||
|
||||
defp inline_text(mark, text, options) do
|
||||
if options[:enabled] do
|
||||
[[color_for(mark, options) | text] | IO.ANSI.reset()]
|
||||
else
|
||||
[[mark | text] | mark]
|
||||
end
|
||||
[h | t] = Enum.reverse([IO.ANSI.reset() | buffer])
|
||||
[color_for(h, options) | t]
|
||||
end
|
||||
|
||||
defp color_for(mark, colors) do
|
||||
@@ -937,19 +725,10 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
defp bullet_text(options) do
|
||||
if options[:enabled], do: @bullet_text_unicode, else: @bullet_text_ascii
|
||||
end
|
||||
|
||||
defp color(style, colors) do
|
||||
IO.ANSI.format_fragment(colors[style], colors[:enabled])
|
||||
color = colors[style]
|
||||
IO.ANSI.format_fragment(color, colors[:enabled])
|
||||
end
|
||||
|
||||
defp newline_after_block(options) do
|
||||
IO.puts(maybe_reset(options))
|
||||
end
|
||||
|
||||
defp maybe_reset(options) do
|
||||
if options[:enabled], do: IO.ANSI.reset(), else: ""
|
||||
end
|
||||
defp newline_after_block, do: IO.puts(IO.ANSI.reset())
|
||||
end
|
||||
|
||||
+253
-793
File diff suppressed because it is too large
Load Diff
@@ -498,12 +498,9 @@ defmodule Kernel.CLI do
|
||||
|
||||
verbose_opts =
|
||||
if config.verbose_compile do
|
||||
[each_file: &IO.puts("Compiling #{Path.relative_to_cwd(&1)}")]
|
||||
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 15s)")]
|
||||
else
|
||||
[
|
||||
each_long_compilation:
|
||||
&IO.puts("Compiling #{Path.relative_to_cwd(&1)} (it's taking more than 10s)")
|
||||
]
|
||||
[]
|
||||
end
|
||||
|
||||
profile_opts =
|
||||
@@ -528,7 +525,6 @@ defmodule Kernel.CLI do
|
||||
|
||||
defp filter_patterns(pattern) do
|
||||
pattern
|
||||
|> Path.expand()
|
||||
|> Path.wildcard()
|
||||
|> :lists.usort()
|
||||
|> Enum.filter(&File.regular?/1)
|
||||
|
||||
@@ -29,11 +29,6 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :stop)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_require(pid, module) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_require, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, fas, line, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
|
||||
@@ -50,8 +45,13 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module, fa, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa, mode})
|
||||
def remote_struct(pid, module) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_struct, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module, fa) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -106,7 +106,7 @@ defmodule Kernel.LexicalTracker do
|
||||
state = %{
|
||||
directives: %{},
|
||||
references: %{},
|
||||
exports: %{},
|
||||
structs: %{},
|
||||
cache: %{},
|
||||
compile_env: :ordsets.new(),
|
||||
file: nil
|
||||
@@ -127,7 +127,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
|
||||
{:reply, {compile, Map.keys(state.structs), runtime, state.compile_env}, state}
|
||||
end
|
||||
|
||||
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
|
||||
@@ -142,13 +142,18 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, %{state | cache: Map.put(cache, key, value)}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_struct, module}, state) do
|
||||
structs = Map.put(state.structs, module, true)
|
||||
{:noreply, %{state | structs: structs}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_dispatch, module, mode}, state) do
|
||||
references = add_reference(state.references, module, mode)
|
||||
{:noreply, %{state | references: references}}
|
||||
end
|
||||
|
||||
def handle_cast({:import_dispatch, module, {function, arity}, mode}, state) do
|
||||
state = add_import_dispatch(state, module, function, arity, mode)
|
||||
def handle_cast({:import_dispatch, module, {function, arity}}, state) do
|
||||
state = add_import_dispatch(state, module, function, arity)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -168,16 +173,11 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
|
||||
end
|
||||
|
||||
def handle_cast({:add_require, module}, state) do
|
||||
{:noreply, put_in(state.exports[module], true)}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, fas, line, warn}, state) do
|
||||
to_remove = for {{:import, {^module, _, _}} = key, _} <- state.directives, do: key
|
||||
|
||||
directives =
|
||||
state.directives
|
||||
|> Map.drop(to_remove)
|
||||
|> Enum.reject(&match?({{:import, {^module, _, _}}, _}, &1))
|
||||
|> Map.new()
|
||||
|> add_directive(module, line, warn, :import)
|
||||
|
||||
directives =
|
||||
@@ -227,13 +227,14 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp add_import_dispatch(state, module, function, arity, mode) do
|
||||
defp add_import_dispatch(state, module, function, arity) do
|
||||
directives =
|
||||
state.directives
|
||||
|> add_dispatch(module, :import)
|
||||
add_dispatch(state.directives, module, :import)
|
||||
|> add_dispatch({module, function, arity}, :import)
|
||||
|
||||
references = add_reference(state.references, module, mode)
|
||||
# Always compile time because we depend
|
||||
# on the module at compile time
|
||||
references = add_reference(state.references, module, :compile)
|
||||
%{state | directives: directives, references: references}
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
always awaited on by calling `Task.await/1`
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
def async(fun) when is_function(fun) do
|
||||
if parent = :erlang.get(:elixir_compiler_pid) do
|
||||
file = :erlang.get(:elixir_compiler_file)
|
||||
dest = :erlang.get(:elixir_compiler_dest)
|
||||
@@ -63,16 +63,12 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that should return the following values:
|
||||
* `{:compile, modules, warnings}` - to continue compilation with a list of
|
||||
further modules to compile
|
||||
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
|
||||
of modules because dependent modules have changed
|
||||
* `{:compile, modules}` - to continue compilation with a list of further modules to compile
|
||||
* `{:runtime, modules}` - to stop compilation and verify the list of modules because
|
||||
dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) to check for modules
|
||||
taking too long to compile. For each file that exceeds the threshold, the
|
||||
`:each_long_compilation` callback is invoked. From Elixir v1.11, only the time
|
||||
spent compiling the actual module is taken into account by the threshold, the
|
||||
time spent waiting is not considered. Defaults to `10` seconds.
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) after the
|
||||
`:each_long_compilation` callback is invoked; defaults to `15`
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
@@ -90,11 +86,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
spawn_workers(files, :compile, options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given files and writes resulting BEAM files into path.
|
||||
|
||||
See `compile/2` for more information.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
|
||||
spawn_workers(files, {:compile, path}, options)
|
||||
@@ -145,45 +136,42 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
defp spawn_workers(files, output, options) do
|
||||
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
|
||||
compiler_pid = self()
|
||||
:elixir_code_server.cast({:reset_warnings, compiler_pid})
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
beam_timestamp = Keyword.get(options, :beam_timestamp)
|
||||
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
|
||||
timer_ref = Process.send_after(self(), :threshold_check, threshold)
|
||||
|
||||
{outcome, state} =
|
||||
outcome =
|
||||
spawn_workers(files, 0, [], [], %{}, [], %{
|
||||
dest: Keyword.get(options, :dest),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
|
||||
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
|
||||
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
|
||||
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
|
||||
profile: profile_init(Keyword.get(options, :profile)),
|
||||
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
|
||||
profile: Keyword.get(options, :profile),
|
||||
cycle_start: System.monotonic_time(),
|
||||
module_counter: 0,
|
||||
output: output,
|
||||
timer_ref: timer_ref,
|
||||
long_compilation_threshold: threshold,
|
||||
schedulers: schedulers
|
||||
})
|
||||
|
||||
Process.cancel_timer(state.timer_ref)
|
||||
# In case --warning-as-errors is enabled and there was a warning,
|
||||
# compilation status will be set to error.
|
||||
compilation_status = :elixir_code_server.call({:compilation_status, compiler_pid})
|
||||
|
||||
receive do
|
||||
:threshold_check -> :ok
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
|
||||
case {outcome, Code.get_compiler_option(:warnings_as_errors)} do
|
||||
{{:ok, _, [_ | _] = warnings}, true} ->
|
||||
case {outcome, compilation_status} do
|
||||
{{:ok, _, warnings}, :error} ->
|
||||
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
|
||||
IO.puts(:stderr, message)
|
||||
{:error, warnings, []}
|
||||
|
||||
{{:error, errors, warnings}, :error} ->
|
||||
{:error, errors ++ warnings, []}
|
||||
|
||||
{{:ok, outcome, warnings}, _} ->
|
||||
{:ok, write_module_binaries(outcome, output, beam_timestamp), warnings}
|
||||
|
||||
{{:error, errors, warnings}, true} ->
|
||||
{:error, errors ++ warnings, []}
|
||||
|
||||
{{:error, errors, warnings}, _} ->
|
||||
{:error, errors, warnings}
|
||||
end
|
||||
@@ -224,7 +212,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
defp verify_modules(result, warnings, dependent_modules, state) do
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{{:ok, result, warnings}, state}
|
||||
{:ok, result, warnings}
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
@@ -256,10 +244,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
|
||||
defp profile_init(nil), do: :none
|
||||
|
||||
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
|
||||
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
time = div(time, 1000)
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
@@ -267,7 +252,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
|
||||
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
|
||||
@@ -289,23 +274,23 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
# Release waiting processes
|
||||
defp spawn_workers([{ref, found} | t], spawned, waiting, files, result, warnings, state) do
|
||||
{files, waiting} =
|
||||
waiting =
|
||||
case List.keytake(waiting, ref, 2) do
|
||||
{{_kind, pid, ^ref, _on, _defining, _deadlock}, waiting} ->
|
||||
send(pid, {ref, found})
|
||||
{update_timing(files, pid, :waiting), waiting}
|
||||
waiting
|
||||
|
||||
nil ->
|
||||
# In case the waiting process died (for example, it was an async process),
|
||||
# it will no longer be on the list. So we need to take it into account here.
|
||||
{files, waiting}
|
||||
waiting
|
||||
end
|
||||
|
||||
spawn_workers(t, spawned, waiting, files, result, warnings, state)
|
||||
end
|
||||
|
||||
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, state) do
|
||||
%{output: output, dest: dest} = state
|
||||
%{output: output, long_compilation_threshold: threshold, dest: dest} = state
|
||||
parent = self()
|
||||
file = Path.expand(file)
|
||||
|
||||
@@ -328,34 +313,24 @@ defmodule Kernel.ParallelCompiler do
|
||||
exit(:shutdown)
|
||||
end)
|
||||
|
||||
file_data = %{
|
||||
pid: pid,
|
||||
ref: ref,
|
||||
file: file,
|
||||
timestamp: System.monotonic_time(),
|
||||
compiling: 0,
|
||||
waiting: 0,
|
||||
warned: false
|
||||
}
|
||||
|
||||
files = [file_data | files]
|
||||
timer_ref = Process.send_after(self(), {:timed_out, pid}, threshold * 1000)
|
||||
files = [{pid, ref, file, timer_ref} | files]
|
||||
spawn_workers(queue, spawned + 1, waiting, files, result, warnings, state)
|
||||
end
|
||||
|
||||
# No more queue, nothing waiting, this cycle is done
|
||||
defp spawn_workers([], 0, [], [], result, warnings, state) do
|
||||
cycle_return = each_cycle_return(state.each_cycle.())
|
||||
state = cycle_timing(result, state)
|
||||
|
||||
case cycle_return do
|
||||
{:runtime, dependent_modules, extra_warnings} ->
|
||||
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
|
||||
case each_cycle_return(state.each_cycle.()) do
|
||||
{:runtime, dependent_modules} ->
|
||||
verify_modules(result, warnings, dependent_modules, state)
|
||||
|
||||
{:compile, [], extra_warnings} ->
|
||||
verify_modules(result, extra_warnings ++ warnings, [], state)
|
||||
{:compile, []} ->
|
||||
verify_modules(result, warnings, [], state)
|
||||
|
||||
{:compile, more, extra_warnings} ->
|
||||
spawn_workers(more, 0, [], [], result, extra_warnings ++ warnings, state)
|
||||
{:compile, more} ->
|
||||
spawn_workers(more, 0, [], [], result, warnings, state)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -366,7 +341,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
[],
|
||||
1,
|
||||
[{_, pid, ref, _, _, _}] = waiting,
|
||||
[%{pid: pid}] = files,
|
||||
[{pid, _, _, _}] = files,
|
||||
result,
|
||||
warnings,
|
||||
state
|
||||
@@ -380,31 +355,24 @@ defmodule Kernel.ParallelCompiler do
|
||||
# There is potentially a deadlock. We will release modules with
|
||||
# the following order:
|
||||
#
|
||||
# 1. Code.ensure_compiled/1 checks without a known definition (deadlock = soft)
|
||||
# 2. Code.ensure_compiled/1 checks with a known definition (deadlock = soft)
|
||||
# 3. Struct/import/require/ensure_compiled! checks without a known definition (deadlock = hard)
|
||||
# 4. Modules without a known definition
|
||||
# 5. Code invocation (deadlock = raise)
|
||||
# 1. Code.ensure_compiled/1 checks (deadlock = soft)
|
||||
# 2. Struct checks (deadlock = hard)
|
||||
# 3. Modules without a known definition
|
||||
# 4. Code invocation (deadlock = raise)
|
||||
#
|
||||
# The reason for step 3 and 4 is to not treat typos as deadlocks and
|
||||
# help developers handle those sooner. However, this can have false
|
||||
# positives in case multiple modules are defined in the same file
|
||||
# and the module we are waiting for is defined later on.
|
||||
#
|
||||
# Finally, note there is no difference between hard and raise, the
|
||||
# In theory there is no difference between hard and raise, the
|
||||
# difference is where the raise is happening, inside the compiler
|
||||
# or in the caller.
|
||||
cond do
|
||||
deadlocked = deadlocked(waiting, :soft) || deadlocked(waiting, :hard) ->
|
||||
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
deadlocked =
|
||||
deadlocked(waiting, :soft, false) ||
|
||||
deadlocked(waiting, :soft, true) || deadlocked(waiting, :hard, false) ||
|
||||
without_definition(waiting, files)
|
||||
without_definition = without_definition(waiting, files) ->
|
||||
spawn_workers(without_definition, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
if deadlocked do
|
||||
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, state)
|
||||
else
|
||||
errors = handle_deadlock(waiting, files)
|
||||
{{:error, errors, warnings}, state}
|
||||
true ->
|
||||
errors = handle_deadlock(waiting, files)
|
||||
{:error, errors, warnings}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -430,11 +398,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp cycle_timing(_result, %{profile: :none} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp cycle_timing(result, %{profile: {:time, cycle_start, module_counter}} = state) do
|
||||
defp cycle_timing(result, %{profile: :time} = state) do
|
||||
%{cycle_start: cycle_start, module_counter: module_counter} = state
|
||||
num_modules = count_modules(result)
|
||||
diff_modules = num_modules - module_counter
|
||||
now = System.monotonic_time()
|
||||
@@ -445,40 +410,35 @@ defmodule Kernel.ParallelCompiler do
|
||||
"[profile] Finished compilation cycle of #{diff_modules} modules in #{time}ms"
|
||||
)
|
||||
|
||||
%{state | profile: {:time, now, num_modules}}
|
||||
%{state | cycle_start: now, module_counter: num_modules}
|
||||
end
|
||||
|
||||
defp cycle_timing(_result, %{profile: nil} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp count_modules(result) do
|
||||
Enum.count(result, &match?({{:module, _}, _}, &1))
|
||||
end
|
||||
|
||||
# TODO: Deprecate other returns on v1.14
|
||||
defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings}
|
||||
defp each_cycle_return({kind, modules}), do: {kind, modules, []}
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules, []}
|
||||
# TODO: Deprecate on v1.14
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
|
||||
defp each_cycle_return(other), do: other
|
||||
|
||||
# The goal of this function is to find leaves in the dependency graph,
|
||||
# i.e. to find code that depends on code that we know is not being defined.
|
||||
# Note that not all files have been compile yet, so they may not be in waiting.
|
||||
defp without_definition(waiting, files) do
|
||||
nillify_empty(
|
||||
for %{pid: pid} <- files,
|
||||
for {pid, _, _, _} <- files,
|
||||
{_, ^pid, ref, on, _, _} <- List.wrap(List.keyfind(waiting, pid, 1)),
|
||||
not defining?(on, waiting),
|
||||
not Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end),
|
||||
do: {ref, :not_found}
|
||||
)
|
||||
end
|
||||
|
||||
defp deadlocked(waiting, type, defining?) do
|
||||
nillify_empty(
|
||||
for {_, _, ref, on, _, ^type} <- waiting,
|
||||
defining?(on, waiting) == defining?,
|
||||
do: {ref, :deadlock}
|
||||
)
|
||||
end
|
||||
|
||||
defp defining?(on, waiting) do
|
||||
Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end)
|
||||
defp deadlocked(waiting, type) do
|
||||
nillify_empty(for {_, _, ref, _, _, ^type} <- waiting, do: {ref, :deadlock})
|
||||
end
|
||||
|
||||
defp nillify_empty([]), do: nil
|
||||
@@ -511,6 +471,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
for {:module, _, ref, ^module, _defining, _deadlock} <- waiting,
|
||||
do: {ref, :found}
|
||||
|
||||
cancel_waiting_timer(files, child)
|
||||
result = Map.put(result, {:module, module}, {binary, module_map})
|
||||
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
@@ -521,33 +482,24 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
{:waiting, kind, child, ref, on, defining, deadlock?} ->
|
||||
# If we already got what we were waiting for, do not put it on waiting.
|
||||
# If we're waiting on ourselves, send :found so that we can crash with
|
||||
# a better error.
|
||||
{files, waiting} =
|
||||
# Alternatively, we're waiting on ourselves,
|
||||
# send :found so that we can crash with a better error.
|
||||
waiting =
|
||||
if Map.has_key?(result, {kind, on}) or on in defining do
|
||||
send(child, {ref, :found})
|
||||
{files, waiting}
|
||||
waiting
|
||||
else
|
||||
files = update_timing(files, child, :compiling)
|
||||
{files, [{kind, child, ref, on, defining, deadlock?} | waiting]}
|
||||
[{kind, child, ref, on, defining, deadlock?} | waiting]
|
||||
end
|
||||
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
:threshold_check ->
|
||||
files =
|
||||
for data <- files do
|
||||
if data.warned or List.keymember?(waiting, data.pid, 1) do
|
||||
data
|
||||
else
|
||||
data = update_timing(data, :compiling)
|
||||
data = maybe_warn_long_compilation(data, state)
|
||||
data
|
||||
end
|
||||
end
|
||||
{:timed_out, child} ->
|
||||
case List.keyfind(files, child, 0) do
|
||||
{^child, _, file, _} -> state.each_long_compilation.(file)
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
{:warning, file, line, message} ->
|
||||
@@ -559,9 +511,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
{:file_ok, child_pid, ref, file, lexical} ->
|
||||
state.each_file.(file, lexical)
|
||||
send(child_pid, ref)
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
|
||||
discard_down(child_pid)
|
||||
new_files = discard_and_maybe_log_file(files, child_pid, state)
|
||||
new_files = List.keydelete(files, child_pid, 0)
|
||||
|
||||
# Sometimes we may have spurious entries in the waiting list
|
||||
# because someone invoked try/rescue UndefinedFunctionError
|
||||
@@ -569,84 +522,28 @@ defmodule Kernel.ParallelCompiler do
|
||||
spawn_workers(queue, spawned - 1, new_waiting, new_files, result, warnings, state)
|
||||
|
||||
{:file_cancel, child_pid} ->
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
discard_down(child_pid)
|
||||
new_files = Enum.reject(files, &(&1.pid == child_pid))
|
||||
new_files = List.keydelete(files, child_pid, 0)
|
||||
spawn_workers(queue, spawned - 1, waiting, new_files, result, warnings, state)
|
||||
|
||||
{:file_error, child_pid, file, {kind, reason, stack}} ->
|
||||
print_error(file, kind, reason, stack)
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
discard_down(child_pid)
|
||||
files |> Enum.reject(&(&1.pid == child_pid)) |> terminate()
|
||||
{{:error, [to_error(file, kind, reason, stack)], warnings}, state}
|
||||
files |> List.keydelete(child_pid, 0) |> terminate()
|
||||
{:error, [to_error(file, kind, reason, stack)], warnings}
|
||||
|
||||
{:DOWN, ref, :process, pid, reason} ->
|
||||
waiting = List.keydelete(waiting, pid, 1)
|
||||
|
||||
case handle_down(files, ref, reason) do
|
||||
:ok -> wait_for_messages(queue, spawned - 1, waiting, files, result, warnings, state)
|
||||
{:error, errors} -> {{:error, errors, warnings}, state}
|
||||
{:error, errors} -> {:error, errors, warnings}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp update_timing(files, pid, key) do
|
||||
Enum.map(files, fn data ->
|
||||
if data.pid == pid do
|
||||
time = System.monotonic_time()
|
||||
%{data | key => data[key] + time - data.timestamp, timestamp: time}
|
||||
else
|
||||
data
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_timing(data, key) do
|
||||
time = System.monotonic_time()
|
||||
%{data | key => data[key] + time - data.timestamp, timestamp: time}
|
||||
end
|
||||
|
||||
defp maybe_warn_long_compilation(data, state) do
|
||||
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
|
||||
|
||||
if not data.warned and compiling >= state.long_compilation_threshold do
|
||||
state.each_long_compilation.(data.file)
|
||||
%{data | warned: true}
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp discard_and_maybe_log_file(files, pid, state) do
|
||||
Enum.reject(files, fn data ->
|
||||
if data.pid == pid do
|
||||
data = update_timing(data, :compiling)
|
||||
data = maybe_warn_long_compilation(data, state)
|
||||
|
||||
if state.profile != :none do
|
||||
compiling = to_padded_ms(data.compiling)
|
||||
waiting = to_padded_ms(data.waiting)
|
||||
relative = Path.relative_to_cwd(data.file)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] #{compiling}ms compiling + #{waiting}ms waiting for #{relative}"
|
||||
)
|
||||
end
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp to_padded_ms(time) do
|
||||
time
|
||||
|> System.convert_time_unit(:native, :millisecond)
|
||||
|> Integer.to_string()
|
||||
|> String.pad_leading(6, " ")
|
||||
end
|
||||
|
||||
defp discard_down(pid) do
|
||||
receive do
|
||||
{:DOWN, _, :process, ^pid, _} -> :ok
|
||||
@@ -658,20 +555,24 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp handle_down(files, ref, reason) do
|
||||
case Enum.find(files, &(&1.ref == ref)) do
|
||||
%{pid: pid, file: file} ->
|
||||
case List.keyfind(files, ref, 1) do
|
||||
{child_pid, ^ref, file, _timer_ref} ->
|
||||
print_error(file, :exit, reason, [])
|
||||
files |> Enum.reject(&(&1.pid == pid)) |> terminate()
|
||||
|
||||
files
|
||||
|> List.keydelete(child_pid, 0)
|
||||
|> terminate()
|
||||
|
||||
{:error, [to_error(file, :exit, reason, [])]}
|
||||
|
||||
nil ->
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_deadlock(waiting, files) do
|
||||
deadlock =
|
||||
for %{pid: pid, file: file} <- files do
|
||||
for {pid, _, file, _} <- files do
|
||||
{:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
|
||||
Process.exit(pid, :kill)
|
||||
|
||||
@@ -706,8 +607,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp terminate(files) do
|
||||
for %{pid: pid} <- files, do: Process.exit(pid, :kill)
|
||||
for %{pid: pid} <- files, do: discard_down(pid)
|
||||
for {pid, _, _, _} <- files, do: Process.exit(pid, :kill)
|
||||
for {pid, _, _, _} <- files, do: discard_down(pid)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -718,6 +619,22 @@ defmodule Kernel.ParallelCompiler do
|
||||
])
|
||||
end
|
||||
|
||||
defp cancel_waiting_timer(files, child_pid) do
|
||||
case List.keyfind(files, child_pid, 0) do
|
||||
{^child_pid, _ref, _file, timer_ref} ->
|
||||
Process.cancel_timer(timer_ref)
|
||||
# Let's flush the message in case it arrived before we canceled the timeout.
|
||||
receive do
|
||||
{:timed_out, ^child_pid} -> :ok
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp to_error(file, kind, reason, stack) do
|
||||
line = get_line(file, reason, stack)
|
||||
file = Path.absname(file)
|
||||
|
||||
@@ -187,7 +187,13 @@ defmodule Kernel.SpecialForms do
|
||||
iex> <<0, "foo">>
|
||||
<<0, 102, 111, 111>>
|
||||
|
||||
Binaries need to be explicitly tagged as `binary`:
|
||||
Variables or any other type need to be explicitly tagged:
|
||||
|
||||
iex> rest = "oo"
|
||||
iex> <<102, rest>>
|
||||
** (ArgumentError) argument error
|
||||
|
||||
We can solve this by explicitly tagging it as `binary`:
|
||||
|
||||
iex> rest = "oo"
|
||||
iex> <<102, rest::binary>>
|
||||
@@ -201,12 +207,6 @@ defmodule Kernel.SpecialForms do
|
||||
iex> <<"foo"::utf32>>
|
||||
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
|
||||
|
||||
Otherwise we get an `ArgumentError` when constructing the binary:
|
||||
|
||||
rest = "oo"
|
||||
<<102, rest>>
|
||||
** (ArgumentError) argument error
|
||||
|
||||
## Options
|
||||
|
||||
Many options can be given by using `-` as separator. Order is
|
||||
@@ -233,9 +233,9 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Sizes for types are a bit more nuanced. The default size for integers is 8.
|
||||
|
||||
For floats, it is 64. For floats, `size * unit` must result in 16, 32, or 64,
|
||||
For floats, it is 64. For floats, `size * unit` must result in 32 or 64,
|
||||
corresponding to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)
|
||||
binary16, binary32, and binary64, respectively.
|
||||
binary32 and binary64, respectively.
|
||||
|
||||
For binaries, the default is the size of the binary. Only the last binary in a
|
||||
match can use the default size. All others must have their size specified
|
||||
@@ -365,13 +365,13 @@ defmodule Kernel.SpecialForms do
|
||||
ERL_COMPILER_OPTIONS=bin_opt_info mix compile
|
||||
|
||||
To learn more about specific optimizations and performance considerations,
|
||||
check out the
|
||||
["Constructing and matching binaries" chapter of the Erlang's Efficiency Guide](https://erlang.org/doc/efficiency_guide/binaryhandling.html).
|
||||
check out
|
||||
[Erlang's Efficiency Guide on handling binaries](http://www.erlang.org/doc/efficiency_guide/binaryhandling.html).
|
||||
"""
|
||||
defmacro unquote(:<<>>)(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Dot operator. Defines a remote call, a call to an anonymous function, or an alias.
|
||||
Defines a remote call, a call to an anonymous function, or an alias.
|
||||
|
||||
The dot (`.`) in Elixir can be used for remote calls:
|
||||
|
||||
@@ -438,7 +438,7 @@ defmodule Kernel.SpecialForms do
|
||||
...> end
|
||||
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
|
||||
|
||||
Note that we have an inner tuple, containing the atom `:.` representing
|
||||
Notice we have an inner tuple, containing the atom `:.` representing
|
||||
the dot as first element:
|
||||
|
||||
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
|
||||
@@ -513,7 +513,7 @@ defmodule Kernel.SpecialForms do
|
||||
Keyword.values #=> uses MyKeyword.values
|
||||
Elixir.Keyword.values #=> uses Keyword.values
|
||||
|
||||
Note that calling `alias` without the `:as` option automatically
|
||||
Notice that calling `alias` without the `:as` option automatically
|
||||
sets an alias based on the last part of the module. For example:
|
||||
|
||||
alias Foo.Bar.Baz
|
||||
@@ -599,7 +599,7 @@ defmodule Kernel.SpecialForms do
|
||||
## Selector
|
||||
|
||||
By default, Elixir imports functions and macros from the given
|
||||
module, except the ones starting with an underscore (which are
|
||||
module, except the ones starting with underscore (which are
|
||||
usually callbacks):
|
||||
|
||||
import List
|
||||
@@ -617,11 +617,9 @@ defmodule Kernel.SpecialForms do
|
||||
import List, only: [flatten: 1]
|
||||
import String, except: [split: 2]
|
||||
|
||||
Importing the same module again will erase the previous imports,
|
||||
except when the `except` option is used, which is always exclusive
|
||||
on a previously declared `import/2`. If there is no previous import,
|
||||
then it applies to all functions and macros in the module. For
|
||||
example:
|
||||
Notice that calling `except` is always exclusive on a previously
|
||||
declared `import/2`. If there is no previous import, then it applies
|
||||
to all functions and macros in the module. For example:
|
||||
|
||||
import List, only: [flatten: 1, keyfind: 4]
|
||||
import List, except: [flatten: 1]
|
||||
@@ -639,7 +637,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Lexical scope
|
||||
|
||||
It is important to note that `import/2` is lexical. This means you
|
||||
It is important to notice that `import/2` is lexical. This means you
|
||||
can import specific macros inside specific functions:
|
||||
|
||||
defmodule Math do
|
||||
@@ -723,11 +721,10 @@ defmodule Kernel.SpecialForms do
|
||||
To retrieve the stacktrace of the current process, use
|
||||
`Process.info(self(), :current_stacktrace)` instead.
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
defmacro __STACKTRACE__, do: error!([])
|
||||
|
||||
@doc """
|
||||
Pin operator. Accesses an already bound variable in match clauses.
|
||||
Accesses an already bound variable in match clauses. Also known as the pin operator.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -759,12 +756,12 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro ^var, do: error!([var])
|
||||
|
||||
@doc """
|
||||
Match operator. Matches the value on the right against the pattern on the left.
|
||||
Matches the value on the right against the pattern on the left.
|
||||
"""
|
||||
defmacro left = right, do: error!([left, right])
|
||||
|
||||
@doc """
|
||||
Type operator. Used by types and bitstrings to specify types.
|
||||
Used by types and bitstrings to specify types.
|
||||
|
||||
This operator is used in two distinct occasions in Elixir.
|
||||
It is used in typespecs to specify the type of a variable,
|
||||
@@ -808,7 +805,7 @@ defmodule Kernel.SpecialForms do
|
||||
* The first element of the tuple is always an atom or
|
||||
another tuple in the same representation.
|
||||
|
||||
* The second element of the tuple represents [metadata](`t:Macro.metadata/0`).
|
||||
* The second element of the tuple represents [metadata](t:Macro.metadata/0).
|
||||
|
||||
* The third element of the tuple are the arguments for the
|
||||
function call. The third argument may be an atom, which is
|
||||
@@ -1066,7 +1063,7 @@ defmodule Kernel.SpecialForms do
|
||||
Hygiene.no_interference()
|
||||
#=> %{}
|
||||
|
||||
Note that, even though the alias `M` is not available
|
||||
Notice that, even though the alias `M` is not available
|
||||
in the context the macro is expanded, the code above works
|
||||
because `M` still expands to `Map`.
|
||||
|
||||
@@ -1416,7 +1413,7 @@ defmodule Kernel.SpecialForms do
|
||||
The `IO` module provides streams, that are both `Enumerable` and
|
||||
`Collectable`, here is an upcase echo server using comprehensions:
|
||||
|
||||
for line <- IO.stream(), into: IO.stream() do
|
||||
for line <- IO.stream(:stdio, :line), into: IO.stream(:stdio, :line) do
|
||||
String.upcase(line)
|
||||
end
|
||||
|
||||
@@ -1550,16 +1547,9 @@ defmodule Kernel.SpecialForms do
|
||||
...> else
|
||||
...> :error ->
|
||||
...> {:error, :wrong_data}
|
||||
...>
|
||||
...> _other_error ->
|
||||
...> :unexpected_error
|
||||
...> end
|
||||
{:error, :wrong_data}
|
||||
|
||||
The `else` block works like a `case` clause: it can have multiple clauses,
|
||||
and the first match will be used. Variables bound inside `with` (such as
|
||||
`width` in this example) are not available in the `else` block.
|
||||
|
||||
If an `else` block is used and there are no matching clauses, a `WithClauseError`
|
||||
exception is raised.
|
||||
"""
|
||||
@@ -1568,8 +1558,6 @@ defmodule Kernel.SpecialForms do
|
||||
@doc """
|
||||
Defines an anonymous function.
|
||||
|
||||
See `Function` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> add = fn a, b -> a + b end
|
||||
@@ -1607,7 +1595,7 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro unquote(:__block__)(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Capture operator. Captures or creates an anonymous function.
|
||||
Captures or creates an anonymous function.
|
||||
|
||||
## Capture
|
||||
|
||||
@@ -1730,21 +1718,19 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Examples
|
||||
|
||||
case File.read(file) do
|
||||
{:ok, contents} when is_binary(contents) ->
|
||||
String.split(contents, "\n")
|
||||
|
||||
{:error, _reason} ->
|
||||
Logger.warning "could not find #{file}, assuming empty..."
|
||||
[]
|
||||
case thing do
|
||||
{:selector, i, value} when is_integer(i) ->
|
||||
value
|
||||
value ->
|
||||
value
|
||||
end
|
||||
|
||||
In the example above, we match the result of `File.read/1`
|
||||
against each clause "head" and execute the clause "body"
|
||||
corresponding to the first clause that matches.
|
||||
In the example above, we match `thing` against each clause "head"
|
||||
and execute the clause "body" corresponding to the first clause
|
||||
that matches.
|
||||
|
||||
If no clause matches, an error is raised. For this reason,
|
||||
it may be necessary to add a final catch-all clause (like `_`)
|
||||
If no clause matches, an error is raised.
|
||||
For this reason, it may be necessary to add a final catch-all clause (like `_`)
|
||||
which will always match.
|
||||
|
||||
x = 10
|
||||
@@ -1752,7 +1738,6 @@ defmodule Kernel.SpecialForms do
|
||||
case x do
|
||||
0 ->
|
||||
"This clause won't match"
|
||||
|
||||
_ ->
|
||||
"This clause would match any value (x = #{x})"
|
||||
end
|
||||
@@ -1760,7 +1745,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Variable handling
|
||||
|
||||
Note that variables bound in a clause do not leak to the outer context:
|
||||
Notice that variables bound in a clause do not leak to the outer context:
|
||||
|
||||
case data do
|
||||
{:ok, value} -> value
|
||||
@@ -1770,7 +1755,8 @@ defmodule Kernel.SpecialForms do
|
||||
value
|
||||
#=> unbound variable value
|
||||
|
||||
Variables in the outer context cannot be overridden either:
|
||||
When binding variables with the same names as variables in the outer context,
|
||||
the variables in the outer context are not affected.
|
||||
|
||||
value = 7
|
||||
|
||||
@@ -1797,19 +1783,6 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
#=> "Will match"
|
||||
|
||||
## Using guards to match against multiple values
|
||||
|
||||
While it is not possible to match against multiple patterns in a single
|
||||
clause, it's possible to match against multiple values by using guards:
|
||||
|
||||
case data do
|
||||
value when value in [:one, :two] ->
|
||||
"#{value} has been matched"
|
||||
|
||||
:three ->
|
||||
"three has been matched"
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro case(condition, clauses), do: error!([condition, clauses])
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ defmodule Kernel.Typespec do
|
||||
{:docs_v1, _, _, _, _, _, docs} ->
|
||||
for {{:type, name, arity}, _, _, doc, _} <- docs do
|
||||
case doc do
|
||||
%{"en" => doc_string} -> {{name, arity}, doc_string}
|
||||
:none -> {{name, arity}, nil}
|
||||
:hidden -> {{name, arity}, false}
|
||||
_ -> {{name, arity}, nil}
|
||||
%{"en" => doc_string} -> {{name, arity}, doc_string}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +83,7 @@ defmodule Kernel.Typespec do
|
||||
store_typespec(bag, kind, expr, pos)
|
||||
|
||||
case :ets.lookup(set, {:function, name, arity}) do
|
||||
[{{:function, ^name, ^arity}, _, line, _, doc, doc_meta}] ->
|
||||
[{{:function, ^name, ^arity}, line, _, doc, doc_meta}] ->
|
||||
store_doc(set, kind, name, arity, line, :doc, doc, doc_meta)
|
||||
|
||||
_ ->
|
||||
@@ -130,18 +130,11 @@ defmodule Kernel.Typespec do
|
||||
store_typespec(bag, kind, expr, pos)
|
||||
end
|
||||
|
||||
@reserved_signatures [required: 1, optional: 1]
|
||||
def deftypespec(kind, expr, line, file, module, pos)
|
||||
when kind in [:type, :typep, :opaque] do
|
||||
{set, bag} = :elixir_module.data_tables(module)
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = signature when signature in @reserved_signatures ->
|
||||
compile_error(
|
||||
:elixir_locals.get_cached_env(pos),
|
||||
"type #{name}/#{arity} is a reserved type and it cannot be defined"
|
||||
)
|
||||
|
||||
{name, arity} when kind == :typep ->
|
||||
{line, doc} = get_doc_info(set, :typedoc, line)
|
||||
|
||||
@@ -257,13 +250,7 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
if Map.has_key?(type_pairs, type_pair) do
|
||||
{error_full_path, error_line} = type_pairs[type_pair]
|
||||
error_relative_path = Path.relative_to_cwd(error_full_path)
|
||||
|
||||
compile_error(
|
||||
env,
|
||||
"type #{name}/#{arity} is already defined in #{error_relative_path}:#{error_line}"
|
||||
)
|
||||
compile_error(env, "type #{name}/#{arity} is already defined")
|
||||
end
|
||||
|
||||
Map.put(type_pairs, type_pair, {file, line})
|
||||
@@ -477,7 +464,7 @@ defmodule Kernel.Typespec do
|
||||
_,
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
|
||||
when is_atom(ctx1) and is_atom(ctx2) and is_integer(unit) and unit >= 0 do
|
||||
line = line(meta)
|
||||
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
|
||||
end
|
||||
@@ -502,7 +489,7 @@ defmodule Kernel.Typespec do
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
|
||||
size >= 0 and unit in 1..256 do
|
||||
is_integer(unit) and size >= 0 and unit >= 0 do
|
||||
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
|
||||
{{:type, line(meta), :binary, args}, state}
|
||||
end
|
||||
@@ -510,7 +497,7 @@ defmodule Kernel.Typespec do
|
||||
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
|
||||
message =
|
||||
"invalid binary specification, expected <<_::size>>, <<_::_*unit>>, " <>
|
||||
"or <<_::size, _::_*unit>> with size being non-negative integers, and unit being an integer between 1 and 256"
|
||||
"or <<_::size, _::_*unit>> with size and unit being non-negative integers"
|
||||
|
||||
compile_error(caller, message)
|
||||
end
|
||||
@@ -643,7 +630,10 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
defp typespec({:__aliases__, _, _} = alias, vars, caller, state) do
|
||||
typespec(expand_remote(alias, caller), vars, caller, state)
|
||||
# We set a function name to avoid tracking
|
||||
# aliases in typespecs as compile time dependencies.
|
||||
atom = Macro.expand(alias, %{caller | function: {:typespec, 0}})
|
||||
typespec(atom, vars, caller, state)
|
||||
end
|
||||
|
||||
# Handle funs
|
||||
@@ -731,7 +721,9 @@ defmodule Kernel.Typespec do
|
||||
|
||||
# Handle remote calls
|
||||
defp typespec({{:., meta, [remote, name]}, _, args} = orig, vars, caller, state) do
|
||||
remote = expand_remote(remote, caller)
|
||||
# We set a function name to avoid tracking
|
||||
# aliases in typespecs as compile time dependencies.
|
||||
remote = Macro.expand(remote, %{caller | function: {:typespec, 0}})
|
||||
|
||||
cond do
|
||||
not is_atom(remote) ->
|
||||
@@ -902,25 +894,6 @@ defmodule Kernel.Typespec do
|
||||
|
||||
## Helpers
|
||||
|
||||
# This is a backport of Macro.expand/2 because we want to expand
|
||||
# aliases but we don't them to become compile-time references.
|
||||
defp expand_remote({:__aliases__, _, _} = alias, env) do
|
||||
case :elixir_aliases.expand(alias, env) do
|
||||
receiver when is_atom(receiver) ->
|
||||
receiver
|
||||
|
||||
aliases ->
|
||||
aliases = :lists.map(&Macro.expand_once(&1, env), aliases)
|
||||
|
||||
case :lists.all(&is_atom/1, aliases) do
|
||||
true -> :elixir_aliases.concat(aliases)
|
||||
false -> alias
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_remote(other, env), do: Macro.expand(other, env)
|
||||
|
||||
defp compile_error(caller, desc) do
|
||||
raise CompileError, file: caller.file, line: caller.line, description: desc
|
||||
end
|
||||
@@ -1022,11 +995,7 @@ defmodule Kernel.Typespec do
|
||||
:elixir_errors.erl_warn(caller.line, caller.file, warning)
|
||||
|
||||
{_, :used_once} ->
|
||||
compile_error(
|
||||
caller,
|
||||
"type variable #{name} is used only once. Type variables in typespecs " <>
|
||||
"must be referenced at least twice, otherwise it is equivalent to term()"
|
||||
)
|
||||
compile_error(caller, "type variable #{name} is unused")
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -108,16 +108,7 @@ defmodule Kernel.Utils do
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
|
||||
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
|
||||
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
[] ->
|
||||
{struct, enforce_keys, Module.get_attribute(module, :derive)}
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"@enforce_keys required keys (#{inspect(error_keys)}) that are not defined in defstruct: " <>
|
||||
"#{inspect(fields)}"
|
||||
end
|
||||
{struct, enforce_keys, Module.get_attribute(module, :derive)}
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([]) do
|
||||
@@ -267,7 +258,7 @@ defmodule Kernel.Utils do
|
||||
{new_var, acc}
|
||||
|
||||
%{} ->
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc)))
|
||||
new_var = Macro.var(generated, Elixir)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
|
||||
+44
-118
@@ -1,11 +1,8 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
A keyword list is a list that consists exclusively of two-element tuples.
|
||||
|
||||
The first element of these tuples is known as the *key*, and it must be an atom.
|
||||
The second element, known as the *value*, can be any term.
|
||||
|
||||
Keywords are mostly used to work with optional values.
|
||||
Keyword lists are lists of two-element tuples, where the first
|
||||
element of the tuple is an atom and the second element can be any
|
||||
value, used mostly to work with optional values.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -39,32 +36,18 @@ defmodule Keyword do
|
||||
in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
entry matching the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put/3` and `Keyword.delete/2` ensure all duplicated
|
||||
entries for a given key are removed when invoked. Note, however, that
|
||||
keyword list operations need to traverse the whole list in order to find
|
||||
entries for a given key are removed when invoked. Note however that
|
||||
keyword list operations need to traverse the list in order to find
|
||||
keys, so these operations are slower than their map counterparts.
|
||||
|
||||
A handful of functions exist to handle duplicated keys, for example,
|
||||
`get_values/2` returns all values for a given key and `delete_first/2`
|
||||
deletes just one of the existing entries.
|
||||
|
||||
Even though lists preserve the user ordering, the functions in
|
||||
`Keyword` do not guarantee any ordering. For example, if you invoke
|
||||
`Keyword.put(opts, new_key, new_value)`, there is no guarantee to
|
||||
where `new_key` will be added (to the front, to the end, or
|
||||
anywhere else).
|
||||
|
||||
Given ordering is not guaranteed, it is not recommended to pattern
|
||||
match on keyword lists either. For example, a function such as:
|
||||
|
||||
def my_function([some_key: value, another_key: another_value])
|
||||
|
||||
will match
|
||||
|
||||
my_function([some_key: :foo, another_key: :bar])
|
||||
|
||||
but it won't match
|
||||
|
||||
my_function([another_key: :bar, some_key: :foo])
|
||||
The functions in `Keyword` do not guarantee any property when it comes
|
||||
to ordering. However, since a keyword list is simply a list, all the
|
||||
operations defined in `Enum` and `List` can be applied too, especially
|
||||
when ordering is required.
|
||||
|
||||
Most of the functions in this module work in linear time. This means
|
||||
that, the time it takes to perform an operation grows at the same
|
||||
@@ -72,8 +55,8 @@ defmodule Keyword do
|
||||
|
||||
## Call syntax
|
||||
|
||||
When keyword lists are passed as the last argument to a function,
|
||||
the square brackets around the keyword list can be omitted. For
|
||||
When keyword lists are passed as the last argument to a function, then
|
||||
the square brackets around the keyword list can be omitted as well. For
|
||||
example, the keyword list syntax:
|
||||
|
||||
String.split("1-0", "-", [trim: true, parts: 2])
|
||||
@@ -259,13 +242,13 @@ defmodule Keyword do
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
|
||||
This `fun` argument receives the value of `key` (or `nil` if `key`
|
||||
is not present) and must return a two-element tuple: the current value
|
||||
is not present) and must return a two-element tuple: the "get" value
|
||||
(the retrieved value, which can be operated on before being returned)
|
||||
and the new value to be stored under `key`. The `fun` may also
|
||||
return `:pop`, implying the current value shall be removed from the
|
||||
keyword list and returned.
|
||||
|
||||
The returned value is a tuple with the current value returned by
|
||||
The returned value is a tuple with the "get" value returned by
|
||||
`fun` and a new keyword list with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -287,9 +270,7 @@ defmodule Keyword do
|
||||
{nil, [a: 1]}
|
||||
|
||||
"""
|
||||
@spec get_and_update(t, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_keywords :: t}
|
||||
when current_value: value
|
||||
@spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
|
||||
def get_and_update(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key),
|
||||
do: get_and_update(keywords, [], key, fun)
|
||||
@@ -326,11 +307,11 @@ defmodule Keyword do
|
||||
Gets the value from `key` and updates it. Raises if there is no `key`.
|
||||
|
||||
This `fun` argument receives the value of `key` and must return a
|
||||
two-element tuple: the current value (the retrieved value, which can be
|
||||
two-element tuple: the "get" value (the retrieved value, which can be
|
||||
operated on before being returned) and the new value to be stored under
|
||||
`key`.
|
||||
|
||||
The returned value is a tuple with the current value returned by `fun` and a new
|
||||
The returned value is a tuple with the "get" value returned by `fun` and a new
|
||||
keyword list with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -351,9 +332,7 @@ defmodule Keyword do
|
||||
{1, []}
|
||||
|
||||
"""
|
||||
@spec get_and_update!(t, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_keywords :: t}
|
||||
when current_value: value
|
||||
@spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} when get: term
|
||||
def get_and_update!(keywords, key, fun) do
|
||||
get_and_update!(keywords, key, fun, [])
|
||||
end
|
||||
@@ -452,30 +431,13 @@ defmodule Keyword do
|
||||
|
||||
iex> Keyword.keys(a: 1, b: 2)
|
||||
[:a, :b]
|
||||
|
||||
iex> Keyword.keys(a: 1, b: 2, a: 3)
|
||||
[:a, :b, :a]
|
||||
|
||||
iex> Keyword.keys([{:a, 1}, {"b", 2}, {:c, 3}])
|
||||
** (ArgumentError) expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, got: {"b", 2}
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(keywords) when is_list(keywords) do
|
||||
try do
|
||||
:lists.map(
|
||||
fn
|
||||
{key, _} when is_atom(key) -> key
|
||||
element -> throw(element)
|
||||
end,
|
||||
keywords
|
||||
)
|
||||
catch
|
||||
element ->
|
||||
raise ArgumentError,
|
||||
"expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, " <>
|
||||
"got: #{inspect(element)}"
|
||||
end
|
||||
:lists.map(fn {k, _} -> k end, keywords)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -650,41 +612,18 @@ defmodule Keyword do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `keywords`.
|
||||
|
||||
In the case a value is stored multiple times in the keyword list,
|
||||
later occurrences are removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.replace([a: 1, b: 2, a: 4], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> Keyword.replace([a: 1], :b, 2)
|
||||
[a: 1]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec replace(t, key, value) :: t
|
||||
@doc false
|
||||
@deprecated "Use Keyword.fetch/2 + Keyword.put/3 instead"
|
||||
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
do_replace(keywords, key, value)
|
||||
end
|
||||
|
||||
defp do_replace([{key, _} | keywords], key, value) do
|
||||
[{key, value} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp do_replace([{_, _} = e | keywords], key, value) do
|
||||
[e | do_replace(keywords, key, value)]
|
||||
end
|
||||
|
||||
defp do_replace([], _key, _value) do
|
||||
[]
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> [{key, value} | delete(keywords, key)]
|
||||
false -> keywords
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `keywords`.
|
||||
Alters the value stored under `key` to `value`, but only
|
||||
if the entry `key` already exists in `keywords`.
|
||||
|
||||
If `key` is not present in `keywords`, a `KeyError` exception is raised.
|
||||
|
||||
@@ -713,7 +652,7 @@ defmodule Keyword do
|
||||
[e | replace!(keywords, key, value, original)]
|
||||
end
|
||||
|
||||
defp replace!([], key, _value, original) do
|
||||
defp replace!([], key, _value, original) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
@@ -732,16 +671,10 @@ defmodule Keyword do
|
||||
iex> Keyword.equal?([a: 1, b: 2, a: 3], [b: 2, a: 3, a: 1])
|
||||
true
|
||||
|
||||
Comparison between values is done with `===/3`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Keyword.equal?([a: 1.0], [a: 1])
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(left, right) when is_list(left) and is_list(right) do
|
||||
:lists.sort(left) === :lists.sort(right)
|
||||
:lists.sort(left) == :lists.sort(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -889,7 +822,7 @@ defmodule Keyword do
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (current_value :: value -> new_value :: value)) :: t
|
||||
@spec update!(t, key, (value -> value)) :: t
|
||||
def update!(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 1) do
|
||||
update!(keywords, key, fun, keywords)
|
||||
@@ -899,52 +832,45 @@ defmodule Keyword do
|
||||
[{key, fun.(value)} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp update!([{_, _} = pair | keywords], key, fun, original) do
|
||||
[pair | update!(keywords, key, fun, original)]
|
||||
defp update!([{_, _} = e | keywords], key, fun, original) do
|
||||
[e | update!(keywords, key, fun, original)]
|
||||
end
|
||||
|
||||
defp update!([], key, _fun, original) do
|
||||
defp update!([], key, _fun, original) when is_atom(key) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates the `key` in `keywords` with the given function.
|
||||
|
||||
If the `key` does not exist, it inserts the given `default` value.
|
||||
If the `key` does not exist, inserts the given `initial` value.
|
||||
|
||||
If there are duplicated keys, they are all removed and only the first one
|
||||
is updated.
|
||||
|
||||
The default value will not be passed through the update function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update([a: 1], :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update([a: 1, a: 2], :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
|
||||
[a: 2]
|
||||
|
||||
iex> Keyword.update([a: 1], :b, 11, fn existing_value -> existing_value * 2 end)
|
||||
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
|
||||
[a: 1, b: 11]
|
||||
|
||||
"""
|
||||
@spec update(t, key, default :: value, (existing_value :: value -> new_value :: value)) :: t
|
||||
def update(keywords, key, default, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 1) do
|
||||
update_guarded(keywords, key, default, fun)
|
||||
end
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(keywords, key, initial, fun)
|
||||
|
||||
defp update_guarded([{key, value} | keywords], key, _default, fun) do
|
||||
def update([{key, value} | keywords], key, _initial, fun) do
|
||||
[{key, fun.(value)} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp update_guarded([{_, _} = pair | keywords], key, default, fun) do
|
||||
[pair | update_guarded(keywords, key, default, fun)]
|
||||
def update([{_, _} = e | keywords], key, initial, fun) do
|
||||
[e | update(keywords, key, initial, fun)]
|
||||
end
|
||||
|
||||
defp update_guarded([], key, default, _fun) do
|
||||
[{key, default}]
|
||||
def update([], key, initial, _fun) when is_atom(key) do
|
||||
[{key, initial}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1048,7 +974,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value for `key` and removes all associated entries in the keyword list,
|
||||
Returns the first value for `key` and removes all associated antries in the keyword list,
|
||||
raising if `key` is not present.
|
||||
|
||||
This function behaves like `pop/3`, but raises in cases the `key` is not present in the
|
||||
|
||||
+19
-36
@@ -1,6 +1,6 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Linked lists hold zero, one, or more elements in the chosen order.
|
||||
Linked lists hold zero, one, or more elements in the choosen order.
|
||||
|
||||
Lists in Elixir are specified between square brackets:
|
||||
|
||||
@@ -15,13 +15,6 @@ defmodule List do
|
||||
iex> [1, true, 2, false, 3, true] -- [true, false]
|
||||
[1, 2, 3, true]
|
||||
|
||||
An element can be prepended to a list using `|`:
|
||||
|
||||
iex> new = 0
|
||||
iex> list = [1, 2, 3]
|
||||
iex> [new | list]
|
||||
[0, 1, 2, 3]
|
||||
|
||||
Lists in Elixir are effectively linked lists, which means
|
||||
they are internally represented in pairs containing the
|
||||
head and the tail of a list:
|
||||
@@ -262,18 +255,13 @@ defmodule List do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first element in `list` or `default` if `list` is empty.
|
||||
|
||||
`first/2` has been introduced in Elixir v1.12.0, while `first/1` has been available since v1.0.0.
|
||||
Returns the first element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.first([])
|
||||
nil
|
||||
|
||||
iex> List.first([], 1)
|
||||
1
|
||||
|
||||
iex> List.first([1])
|
||||
1
|
||||
|
||||
@@ -281,25 +269,19 @@ defmodule List do
|
||||
1
|
||||
|
||||
"""
|
||||
@spec first([], any) :: any
|
||||
@spec first([elem, ...], any) :: elem when elem: var
|
||||
def first(list, default \\ nil)
|
||||
def first([], default), do: default
|
||||
def first([head | _], _default), do: head
|
||||
@spec first([]) :: nil
|
||||
@spec first([elem, ...]) :: elem when elem: var
|
||||
def first([]), do: nil
|
||||
def first([head | _]), do: head
|
||||
|
||||
@doc """
|
||||
Returns the last element in `list` or `default` if `list` is empty.
|
||||
|
||||
`last/2` has been introduced in Elixir v1.12.0, while `last/1` has been available since v1.0.0.
|
||||
Returns the last element in `list` or `nil` if `list` is empty.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.last([])
|
||||
nil
|
||||
|
||||
iex> List.last([], 1)
|
||||
1
|
||||
|
||||
iex> List.last([1])
|
||||
1
|
||||
|
||||
@@ -307,13 +289,11 @@ defmodule List do
|
||||
3
|
||||
|
||||
"""
|
||||
@spec last([], any) :: any
|
||||
@spec last([elem, ...], any) :: elem when elem: var
|
||||
@compile {:inline, last: 2}
|
||||
def last(list, default \\ nil)
|
||||
def last([], default), do: default
|
||||
def last([head], _default), do: head
|
||||
def last([_ | tail], default), do: last(tail, default)
|
||||
@spec last([]) :: nil
|
||||
@spec last([elem, ...]) :: elem when elem: var
|
||||
def last([]), do: nil
|
||||
def last([head]), do: head
|
||||
def last([_ | tail]), do: last(tail)
|
||||
|
||||
@doc """
|
||||
Receives a list of tuples and returns the first tuple
|
||||
@@ -831,6 +811,9 @@ defmodule List do
|
||||
iex> List.to_existing_atom('🌢 Elixir')
|
||||
:"🌢 Elixir"
|
||||
|
||||
iex> List.to_existing_atom('this_atom_will_never_exist')
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec to_existing_atom(charlist) :: atom
|
||||
def to_existing_atom(charlist) do
|
||||
@@ -914,9 +897,9 @@ defmodule List do
|
||||
* integers representing Unicode code points
|
||||
* a list containing one of these three elements
|
||||
|
||||
Note that this function expects a list of integers representing
|
||||
Notice that this function expects a list of integers representing
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](`:binary`).
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -969,9 +952,9 @@ defmodule List do
|
||||
Converts a list of integers representing Unicode code points, lists or
|
||||
strings into a charlist.
|
||||
|
||||
Note that this function expects a list of integers representing
|
||||
Notice that this function expects a list of integers representing
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](`:binary`).
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ defprotocol List.Chars do
|
||||
The `List.Chars` protocol is responsible for
|
||||
converting a structure to a charlist (only if applicable).
|
||||
|
||||
The only function that must be implemented is
|
||||
The only function required to be implemented is
|
||||
`to_charlist/1` which does the conversion.
|
||||
|
||||
The `to_charlist/1` function automatically imported
|
||||
|
||||
+47
-119
@@ -14,7 +14,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
def fun_inspect(value) do
|
||||
IO.inspect(value)
|
||||
IO.inpect(value)
|
||||
value
|
||||
end
|
||||
end
|
||||
@@ -32,7 +32,7 @@ defmodule Macro do
|
||||
#=> 1
|
||||
|
||||
So far they behave the same, as we are passing an integer as argument.
|
||||
But what happens when we pass an expression:
|
||||
But what happens when we pass an expresion:
|
||||
|
||||
macro_inspect(1 + 2)
|
||||
#=> {:+, [line: 3], [1, 2]}
|
||||
@@ -173,7 +173,6 @@ defmodule Macro do
|
||||
* `:end_of_expression` - denotes when the end of expression effectively
|
||||
happens. Available for all expressions except the last one inside a
|
||||
`__block__` (when `:token_metadata` is true)
|
||||
* `:indentation` - indentation of a sigil heredoc
|
||||
|
||||
The following metadata keys are private:
|
||||
|
||||
@@ -270,7 +269,7 @@ defmodule Macro do
|
||||
def pipe(expr, {:fn, _, _}, _integer) do
|
||||
raise ArgumentError,
|
||||
"cannot pipe #{to_string(expr)} into an anonymous function without" <>
|
||||
" calling the function; use Kernel.then/2 instead or" <>
|
||||
" calling the function; use something like (fn ... end).() or" <>
|
||||
" define the anonymous function as a regular private function"
|
||||
end
|
||||
|
||||
@@ -337,13 +336,8 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.var/2`.
|
||||
|
||||
Note the arguments are not unique. If you later on want
|
||||
to access the same variables, you can invoke this function
|
||||
with the same inputs. Use `generate_unique_arguments/2` to
|
||||
generate a unique arguments that can't be overridden.
|
||||
Generates AST nodes for a given number of required argument variables using
|
||||
`Macro.var/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -354,47 +348,19 @@ defmodule Macro do
|
||||
@doc since: "1.5.0"
|
||||
@spec generate_arguments(0, context :: atom) :: []
|
||||
@spec generate_arguments(pos_integer, context) :: [{atom, [], context}, ...] when context: atom
|
||||
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
|
||||
def generate_arguments(amount, context)
|
||||
|
||||
@doc """
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.unique_var/2`.
|
||||
def generate_arguments(0, context) when is_atom(context), do: []
|
||||
|
||||
## Examples
|
||||
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
||||
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
||||
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
||||
iex> is_integer(c1) and is_integer(c2)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) :: [
|
||||
{atom, [counter: integer], context},
|
||||
...
|
||||
]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
|
||||
defp generate_arguments(0, context, _fun) when is_atom(context), do: []
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
def generate_arguments(amount, context)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: var(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates an AST node representing the variable given
|
||||
by the atoms `var` and `context`.
|
||||
|
||||
Note this variable is not unique. If you later on want
|
||||
to access this same variable, you can invoke `var/2`
|
||||
again with the same arguments. Use `unique_var/2` to
|
||||
generate a unique variable that can't be overridden.
|
||||
|
||||
## Examples
|
||||
|
||||
In order to build a variable, a context is expected.
|
||||
@@ -416,24 +382,6 @@ defmodule Macro do
|
||||
{var, [], context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates an AST node representing a unique variable
|
||||
given by the atoms `var` and `context`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
||||
iex> is_integer(c)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
||||
when var: atom, context: atom
|
||||
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [counter: :elixir_module.next_counter(context)], context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first traversal of quoted expressions
|
||||
using an accumulator.
|
||||
@@ -478,14 +426,10 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
defp do_traverse_args(args, acc, pre, post) when is_list(args) do
|
||||
:lists.mapfoldl(
|
||||
fn x, acc ->
|
||||
{x, acc} = pre.(x, acc)
|
||||
do_traverse(x, acc, pre, post)
|
||||
end,
|
||||
acc,
|
||||
args
|
||||
)
|
||||
Enum.map_reduce(args, acc, fn x, acc ->
|
||||
{x, acc} = pre.(x, acc)
|
||||
do_traverse(x, acc, pre, post)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -545,15 +489,10 @@ defmodule Macro do
|
||||
iex> Macro.decompose_call(quote(do: 42))
|
||||
:error
|
||||
|
||||
iex> Macro.decompose_call(quote(do: {:foo, [], []}))
|
||||
:error
|
||||
|
||||
"""
|
||||
@spec decompose_call(t()) :: {atom, [t()]} | {t(), atom, [t()]} | :error
|
||||
def decompose_call(ast)
|
||||
|
||||
def decompose_call({:{}, _, args}) when is_list(args), do: :error
|
||||
|
||||
def decompose_call({{:., _, [remote, function]}, _, args})
|
||||
when is_tuple(remote) or is_atom(remote),
|
||||
do: {remote, function, args}
|
||||
@@ -593,7 +532,7 @@ defmodule Macro do
|
||||
As an example, `ExUnit` stores the AST of every assertion, so when
|
||||
an assertion fails we can show code snippets to users. Without this
|
||||
option, each time the test module is compiled, we get a different
|
||||
MD5 of the module bytecode, because the AST contains metadata,
|
||||
MD5 of the module byte code, because the AST contains metadata,
|
||||
such as counters, specific to the compilation environment. By pruning
|
||||
the metadata, we ensure that the module is deterministic and reduce
|
||||
the amount of data `ExUnit` needs to keep around.
|
||||
@@ -636,19 +575,17 @@ defmodule Macro do
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled. This function is also capable of
|
||||
not have been compiled. This function is even capable of
|
||||
expanding structs defined under the module being compiled.
|
||||
|
||||
It will raise `CompileError` if the struct is not available.
|
||||
From Elixir v1.12, calling this function also adds an export
|
||||
dependency on the given struct.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec struct!(module, Macro.Env.t()) :: %{__struct__: module} when module: module()
|
||||
def struct!(module, env) when is_atom(module) do
|
||||
if module == env.module do
|
||||
Module.get_attribute(module, :__struct__)
|
||||
end || :elixir_map.load_struct([line: env.line], module, [], [], env)
|
||||
Module.get_attribute(module, :struct)
|
||||
end || :elixir_map.load_struct([line: env.line], module, [], env)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -725,8 +662,8 @@ defmodule Macro do
|
||||
and return a version with it unescaped.
|
||||
"""
|
||||
@spec unescape_string(String.t()) :: String.t()
|
||||
def unescape_string(string) do
|
||||
:elixir_interpolation.unescape_string(string)
|
||||
def unescape_string(chars) do
|
||||
:elixir_interpolation.unescape_chars(chars)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -741,9 +678,8 @@ defmodule Macro do
|
||||
representing the code point of the character it wants to unescape.
|
||||
Here is the default mapping function implemented by Elixir:
|
||||
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(:unicode), do: true
|
||||
def unescape_map(:hex), do: true
|
||||
def unescape_map(unicode), do: true
|
||||
def unescape_map(hex), do: true
|
||||
def unescape_map(?0), do: ?0
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(?b), do: ?\b
|
||||
@@ -760,9 +696,9 @@ defmodule Macro do
|
||||
If the `unescape_map/1` function returns `false`, the char is
|
||||
not escaped and the backslash is kept in the string.
|
||||
|
||||
Newlines, Unicode, and hexadecimals code points will be escaped if
|
||||
the map returns `true` respectively for `:newline`, `:unicode`, and
|
||||
`:hex`.
|
||||
Hexadecimals and Unicode code points will be escaped if the map
|
||||
function returns `true` for `?x`. Unicode code points if the map
|
||||
function returns `true` for `?u`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -772,23 +708,25 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@spec unescape_string(String.t(), (non_neg_integer -> non_neg_integer | false)) :: String.t()
|
||||
def unescape_string(string, map) do
|
||||
:elixir_interpolation.unescape_string(string, map)
|
||||
def unescape_string(chars, map) do
|
||||
:elixir_interpolation.unescape_chars(chars, map)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Traverse over the arguments using Enum.map/2 instead"
|
||||
def unescape_tokens(tokens) do
|
||||
for token <- tokens do
|
||||
if is_binary(token), do: unescape_string(token), else: token
|
||||
case :elixir_interpolation.unescape_tokens(tokens) do
|
||||
{:ok, unescaped_tokens} -> unescaped_tokens
|
||||
{:error, reason} -> raise ArgumentError, to_string(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Traverse over the arguments using Enum.map/2 instead"
|
||||
def unescape_tokens(tokens, map) do
|
||||
for token <- tokens do
|
||||
if is_binary(token), do: unescape_string(token, map), else: token
|
||||
case :elixir_interpolation.unescape_tokens(tokens, map) do
|
||||
{:ok, unescaped_tokens} -> unescaped_tokens
|
||||
{:error, reason} -> raise ArgumentError, to_string(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -957,19 +895,19 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
# All other calls
|
||||
def to_string({{:., _, [left, _]} = target, meta, []} = ast, fun) do
|
||||
to_string = call_to_string(target, fun)
|
||||
def to_string({target, meta, []} = ast, fun) do
|
||||
target = call_to_string(target, fun)
|
||||
|
||||
if is_tuple(left) && meta[:no_parens] do
|
||||
fun.(ast, to_string)
|
||||
if meta[:no_parens] do
|
||||
fun.(ast, target)
|
||||
else
|
||||
fun.(ast, to_string <> "()")
|
||||
fun.(ast, target <> "()")
|
||||
end
|
||||
end
|
||||
|
||||
def to_string({target, _, args} = ast, fun) when is_list(args) do
|
||||
with :error <- unary_call(ast, fun),
|
||||
:error <- op_call(ast, fun),
|
||||
:error <- binary_call(ast, fun),
|
||||
:error <- sigil_call(ast, fun) do
|
||||
{list, last} = split_last(args)
|
||||
|
||||
@@ -1024,14 +962,10 @@ defmodule Macro do
|
||||
Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)
|
||||
end
|
||||
|
||||
defp bitpart_to_string({:"::", meta, [left, right]} = ast, fun) do
|
||||
defp bitpart_to_string({:"::", _, [left, right]} = ast, fun) do
|
||||
result =
|
||||
if meta[:inferred_bitstring_spec] do
|
||||
to_string(left, fun)
|
||||
else
|
||||
op_to_string(left, fun, :"::", :left) <>
|
||||
"::" <> bitmods_to_string(right, fun, :"::", :right)
|
||||
end
|
||||
op_to_string(left, fun, :"::", :left) <>
|
||||
"::" <> bitmods_to_string(right, fun, :"::", :right)
|
||||
|
||||
fun.(ast, result)
|
||||
end
|
||||
@@ -1087,6 +1021,8 @@ defmodule Macro do
|
||||
"\#{" <> to_string(arg, fun) <> "}"
|
||||
|
||||
binary when is_binary(binary) ->
|
||||
binary = inspect_no_limit(binary)
|
||||
binary = binary_part(binary, 1, byte_size(binary) - 2)
|
||||
escape_sigil(binary, left)
|
||||
end)
|
||||
|
||||
@@ -1141,14 +1077,7 @@ defmodule Macro do
|
||||
:error
|
||||
end
|
||||
|
||||
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
|
||||
left = op_to_string(left, fun, :.., :left)
|
||||
middle = op_to_string(middle, fun, :.., :right)
|
||||
right = op_to_string(right, fun, :"//", :right)
|
||||
{:ok, fun.(ast, left <> ".." <> middle <> "//" <> right)}
|
||||
end
|
||||
|
||||
defp op_call({op, _, [left, right]} = ast, fun) when is_atom(op) do
|
||||
defp binary_call({op, _, [left, right]} = ast, fun) when is_atom(op) do
|
||||
case Identifier.binary_op(op) do
|
||||
{_, _} ->
|
||||
left = op_to_string(left, fun, op, :left)
|
||||
@@ -1161,7 +1090,7 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
defp op_call(_, _) do
|
||||
defp binary_call(_, _) do
|
||||
:error
|
||||
end
|
||||
|
||||
@@ -1679,8 +1608,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
defp do_underscore(<<h, t, rest::binary>>, _)
|
||||
when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and not (t >= ?0 and t <= ?9) and
|
||||
t != ?. and t != ?_ do
|
||||
when h >= ?A and h <= ?Z and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ do
|
||||
<<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
|
||||
end
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule Macro.Env do
|
||||
line: line,
|
||||
macro_aliases: macro_aliases,
|
||||
macros: macros,
|
||||
module: module,
|
||||
module: atom,
|
||||
prematch_vars: prematch_vars,
|
||||
unused_vars: unused_vars,
|
||||
requires: requires,
|
||||
@@ -105,9 +105,7 @@ defmodule Macro.Env do
|
||||
vars: vars
|
||||
}
|
||||
|
||||
# Define the __struct__ callbacks by hand for bootstrap reasons.
|
||||
# TODO: Remove :vars field on v2.0
|
||||
@doc false
|
||||
def __struct__ do
|
||||
%{
|
||||
__struct__: __MODULE__,
|
||||
@@ -132,7 +130,6 @@ defmodule Macro.Env do
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __struct__(kv) do
|
||||
Enum.reduce(kv, __struct__(), fn {k, v}, acc -> :maps.update(k, v, acc) end)
|
||||
end
|
||||
|
||||
+61
-90
@@ -19,13 +19,8 @@ defmodule Map do
|
||||
in a map literal, the last one prevails.
|
||||
|
||||
When the key in a key-value pair is an atom, the `key: value` shorthand syntax
|
||||
can be used (as in many other special forms):
|
||||
|
||||
iex> %{a: 1, b: 2}
|
||||
%{a: 1, b: 2}
|
||||
|
||||
If you want to mix the shorthand syntax with `=>`, the shorthand syntax must come
|
||||
at the end:
|
||||
can be used (as in many other special forms), provided key-value pairs are put at
|
||||
the end:
|
||||
|
||||
iex> %{"hello" => "world", a: 1, b: 2}
|
||||
%{:a => 1, :b => 2, "hello" => "world"}
|
||||
@@ -42,20 +37,16 @@ defmodule Map do
|
||||
iex> map["non_existing_key"]
|
||||
nil
|
||||
|
||||
To access atom keys, one may also use the `map.key` notation. Note that `map.key`
|
||||
will raise a `KeyError` if the `map` doesn't contain the key `:key`, compared to
|
||||
`map[:key]`, that would return `nil`.
|
||||
For accessing atom keys, one may also `map.key`. Note that while `map[key]` will
|
||||
return `nil` if `map` doesn't contain `key`, `map.key` will raise if `map` doesn't
|
||||
contain the key `:key`.
|
||||
|
||||
map = %{foo: "bar", baz: "bong"}
|
||||
map.foo
|
||||
#=> "bar"
|
||||
map.non_existing_key
|
||||
iex> map = %{foo: "bar", baz: "bong"}
|
||||
iex> map.foo
|
||||
"bar"
|
||||
iex> map.non_existing_key
|
||||
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
|
||||
|
||||
> Note: do not add parens when accessing fields, such as in `data.key()`.
|
||||
> If parenthesis are used, Elixir will expect `data` to be an atom representing
|
||||
> a module and attempt to call the *function* `key/0` in it.
|
||||
|
||||
The two syntaxes for accessing keys reveal the dual nature of maps. The `map[key]`
|
||||
syntax is used for dynamically created maps that may have any key, of any type.
|
||||
`map.key` is used with maps that hold a predetermined set of atoms keys, which are
|
||||
@@ -72,10 +63,8 @@ defmodule Map do
|
||||
iex> %{a: a} = %{:a => 1, "b" => 2, [:c, :e, :e] => 3}
|
||||
iex> a
|
||||
1
|
||||
|
||||
But this will raise a `MatchError` exception:
|
||||
|
||||
%{:c => 3} = %{:a => 1, 2 => :b}
|
||||
iex> %{:c => 3} = %{:a => 1, 2 => :b}
|
||||
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
|
||||
|
||||
Variables can be used as map keys both when writing map literals as well as
|
||||
when matching:
|
||||
@@ -93,10 +82,8 @@ defmodule Map do
|
||||
iex> map = %{one: 1, two: 2}
|
||||
iex> %{map | one: "one"}
|
||||
%{one: "one", two: 2}
|
||||
|
||||
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
|
||||
|
||||
%{map | three: 3}
|
||||
iex> %{map | three: 3}
|
||||
** (KeyError) key :three not found
|
||||
|
||||
The functions in this module that need to find a specific key work in logarithmic time.
|
||||
This means that the time it takes to find keys grows as the map grows, but it's not
|
||||
@@ -216,10 +203,20 @@ defmodule Map do
|
||||
@spec new(Enumerable.t(), (term -> {key, value})) :: map
|
||||
def new(enumerable, transform) when is_function(transform, 1) do
|
||||
enumerable
|
||||
|> Enum.map(transform)
|
||||
|> Enum.to_list()
|
||||
|> new_transform(transform, [])
|
||||
end
|
||||
|
||||
defp new_transform([], _fun, acc) do
|
||||
acc
|
||||
|> :lists.reverse()
|
||||
|> :maps.from_list()
|
||||
end
|
||||
|
||||
defp new_transform([element | rest], fun, acc) do
|
||||
new_transform(rest, fun, [fun.(element) | acc])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns whether the given `key` exists in the given `map`.
|
||||
|
||||
@@ -239,8 +236,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Fetches the value for a specific `key` in the given `map`.
|
||||
|
||||
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
|
||||
If `map` doesn't contain `key`, `:error` is returned.
|
||||
If `map` contains the given `key` with value `value`, then `{:ok, value}` is
|
||||
returned. If `map` doesn't contain `key`, `:error` is returned.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -259,7 +256,7 @@ defmodule Map do
|
||||
Fetches the value for a specific `key` in the given `map`, erroring out if
|
||||
`map` doesn't contain `key`.
|
||||
|
||||
If `map` contains `key`, the corresponding value is returned. If
|
||||
If `map` contains the given `key`, the corresponding value is returned. If
|
||||
`map` doesn't contain `key`, a `KeyError` exception is raised.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -301,20 +298,8 @@ defmodule Map do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace(%{a: 1}, :b, 2)
|
||||
%{a: 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec replace(map, key, value) :: map
|
||||
@doc false
|
||||
@deprecated "Use Map.fetch/2 + Map.put/3 instead"
|
||||
def replace(map, key, value) do
|
||||
case map do
|
||||
%{^key => _value} ->
|
||||
@@ -329,7 +314,8 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
Alters the value stored under `key` to `value`, but only
|
||||
if the entry `key` already exists in `map`.
|
||||
|
||||
If `key` is not present in `map`, a `KeyError` exception is raised.
|
||||
|
||||
@@ -434,7 +420,7 @@ defmodule Map do
|
||||
@doc """
|
||||
Gets the value for a specific `key` in `map`.
|
||||
|
||||
If `key` is present in `map` then its value `value` is
|
||||
If `key` is present in `map` with value `value`, then `value` is
|
||||
returned. Otherwise, `default` is returned.
|
||||
|
||||
If `default` is not provided, `nil` is used.
|
||||
@@ -468,7 +454,7 @@ defmodule Map do
|
||||
@doc """
|
||||
Gets the value for a specific `key` in `map`.
|
||||
|
||||
If `key` is present in `map` then its value `value` is
|
||||
If `key` is present in `map` with value `value`, then `value` is
|
||||
returned. Otherwise, `fun` is evaluated and its result is returned.
|
||||
|
||||
This is useful if the default value is very expensive to calculate or
|
||||
@@ -596,39 +582,38 @@ defmodule Map do
|
||||
@doc """
|
||||
Updates the `key` in `map` with the given function.
|
||||
|
||||
If `key` is present in `map` then the existing value is passed to `fun` and its result is
|
||||
used as the updated value of `key`. If `key` is
|
||||
not present in `map`, `default` is inserted as the value of `key`. The default
|
||||
If `key` is present in `map` with value `value`, `fun` is invoked with
|
||||
argument `value` and its result is used as the new value of `key`. If `key` is
|
||||
not present in `map`, `initial` is inserted as the value of `key`. The initial
|
||||
value will not be passed through the update function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.update(%{a: 1}, :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
iex> Map.update(%{a: 1}, :a, 13, &(&1 * 2))
|
||||
%{a: 2}
|
||||
iex> Map.update(%{a: 1}, :b, 11, fn existing_value -> existing_value * 2 end)
|
||||
iex> Map.update(%{a: 1}, :b, 11, &(&1 * 2))
|
||||
%{a: 1, b: 11}
|
||||
|
||||
"""
|
||||
@spec update(map, key, default :: value, (existing_value :: value -> new_value :: value)) ::
|
||||
map
|
||||
def update(map, key, default, fun) when is_function(fun, 1) do
|
||||
@spec update(map, key, value, (value -> value)) :: map
|
||||
def update(map, key, initial, fun) when is_function(fun, 1) do
|
||||
case map do
|
||||
%{^key => value} ->
|
||||
put(map, key, fun.(value))
|
||||
|
||||
%{} ->
|
||||
put(map, key, default)
|
||||
put(map, key, initial)
|
||||
|
||||
other ->
|
||||
:erlang.error({:badmap, other}, [map, key, default, fun])
|
||||
:erlang.error({:badmap, other}, [map, key, initial, fun])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the value associated with `key` in `map` and returns the value and the updated map.
|
||||
Returns and removes the value associated with `key` in `map`.
|
||||
|
||||
If `key` is present in `map`, it returns `{value, updated_map}` where `value` is the value of
|
||||
the key and `updated_map` is the result of removing `key` from `map`. If `key`
|
||||
If `key` is present in `map` with value `value`, `{value, new_map}` is
|
||||
returned where `new_map` is the result of removing `key` from `map`. If `key`
|
||||
is not present in `map`, `{default, map}` is returned.
|
||||
|
||||
## Examples
|
||||
@@ -641,7 +626,7 @@ defmodule Map do
|
||||
{3, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
|
||||
@spec pop(map, key, value) :: {value, map}
|
||||
def pop(map, key, default \\ nil) do
|
||||
case :maps.take(key, map) do
|
||||
{_, _} = tuple -> tuple
|
||||
@@ -650,8 +635,8 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the value associated with `key` in `map` and returns the value
|
||||
and the updated map, or it raises if `key` is not present.
|
||||
Returns and removes the value associated with `key` in `map` or raises
|
||||
if `key` is not present.
|
||||
|
||||
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
|
||||
|
||||
@@ -666,7 +651,7 @@ defmodule Map do
|
||||
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(map, key) :: {value, updated_map :: map}
|
||||
@spec pop!(map, key) :: {value, map}
|
||||
def pop!(map, key) do
|
||||
case :maps.take(key, map) do
|
||||
{_, _} = tuple -> tuple
|
||||
@@ -677,8 +662,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Lazily returns and removes the value associated with `key` in `map`.
|
||||
|
||||
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
|
||||
the key and `new_map` is the result of removing `key` from `map`. If `key`
|
||||
If `key` is present in `map` with value `value`, `{value, new_map}` is
|
||||
returned where `new_map` is the result of removing `key` from `map`. If `key`
|
||||
is not present in `map`, `{fun_result, map}` is returned, where `fun_result`
|
||||
is the result of applying `fun`.
|
||||
|
||||
@@ -794,8 +779,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Updates `key` with the given function.
|
||||
|
||||
If `key` is present in `map` then the existing value is passed to `fun` and its result is
|
||||
used as the updated value of `key`. If `key` is
|
||||
If `key` is present in `map` with value `value`, `fun` is invoked with
|
||||
argument `value` and its result is used as the new value of `key`. If `key` is
|
||||
not present in `map`, a `KeyError` exception is raised.
|
||||
|
||||
## Examples
|
||||
@@ -807,7 +792,7 @@ defmodule Map do
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
"""
|
||||
@spec update!(map, key, (existing_value :: value -> new_value :: value)) :: map
|
||||
@spec update!(map, key, (value -> value)) :: map
|
||||
def update!(map, key, fun) when is_function(fun, 1) do
|
||||
value = fetch!(map, key)
|
||||
put(map, key, fun.(value))
|
||||
@@ -817,13 +802,13 @@ defmodule Map do
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
|
||||
`fun` is called with the current value under `key` in `map` (or `nil` if `key`
|
||||
is not present in `map`) and must return a two-element tuple: the current value
|
||||
is not present in `map`) and must return a two-element tuple: the "get" value
|
||||
(the retrieved value, which can be operated on before being returned) and the
|
||||
new value to be stored under `key` in the resulting new map. `fun` may also
|
||||
return `:pop`, which means the current value shall be removed from `map` and
|
||||
returned (making this function behave like `Map.pop(map, key)`).
|
||||
|
||||
The returned value is a two-element tuple with the current value returned by
|
||||
The returned value is a tuple with the "get" value returned by
|
||||
`fun` and a new map with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -836,7 +821,7 @@ defmodule Map do
|
||||
iex> Map.get_and_update(%{a: 1}, :b, fn current_value ->
|
||||
...> {current_value, "new value!"}
|
||||
...> end)
|
||||
{nil, %{a: 1, b: "new value!"}}
|
||||
{nil, %{b: "new value!", a: 1}}
|
||||
|
||||
iex> Map.get_and_update(%{a: 1}, :a, fn _ -> :pop end)
|
||||
{1, %{}}
|
||||
@@ -845,9 +830,7 @@ defmodule Map do
|
||||
{nil, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec get_and_update(map, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, new_map :: map}
|
||||
when current_value: value
|
||||
@spec get_and_update(map, key, (value -> {get, value} | :pop)) :: {get, map} when get: term
|
||||
def get_and_update(map, key, fun) when is_function(fun, 1) do
|
||||
current = get(map, key)
|
||||
|
||||
@@ -864,7 +847,7 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value from `key` and updates it, all in one pass. Raises if there is no `key`.
|
||||
Gets the value from `key` and updates it. Raises if there is no `key`.
|
||||
|
||||
Behaves exactly like `get_and_update/3`, but raises a `KeyError` exception if
|
||||
`key` is not present in `map`.
|
||||
@@ -887,9 +870,8 @@ defmodule Map do
|
||||
{1, %{}}
|
||||
|
||||
"""
|
||||
@spec get_and_update!(map, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, map}
|
||||
when current_value: value
|
||||
@spec get_and_update!(map, key, (value -> {get, value} | :pop)) :: {get, map}
|
||||
when get: term
|
||||
def get_and_update!(map, key, fun) when is_function(fun, 1) do
|
||||
value = fetch!(map, key)
|
||||
|
||||
@@ -940,11 +922,6 @@ defmodule Map do
|
||||
Two maps are considered to be equal if they contain
|
||||
the same keys and those keys contain the same values.
|
||||
|
||||
Note this function exists for completeness so the `Map`
|
||||
and `Keyword` modules provide similar APIs. In practice,
|
||||
developers often compare maps using `==/2` or `===/2`
|
||||
directly.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 2, a: 1})
|
||||
@@ -952,12 +929,6 @@ defmodule Map do
|
||||
iex> Map.equal?(%{a: 1, b: 2}, %{b: 1, a: 2})
|
||||
false
|
||||
|
||||
Comparison between keys and values is done with `===/3`,
|
||||
which means integers are not equivalent to floats:
|
||||
|
||||
iex> Map.equal?(%{a: 1.0}, %{a: 1})
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(map, map) :: boolean
|
||||
def equal?(map1, map2)
|
||||
|
||||
@@ -57,7 +57,7 @@ defmodule MapSet do
|
||||
@opaque t(value) :: %__MODULE__{map: %{optional(value) => []}}
|
||||
@type t :: t(term)
|
||||
|
||||
# TODO: Remove version key on Elixir v2.0
|
||||
# TODO: Remove version key on v2.0
|
||||
defstruct map: %{}, version: 2
|
||||
|
||||
@doc """
|
||||
@@ -223,9 +223,7 @@ defmodule MapSet do
|
||||
@doc """
|
||||
Checks if two sets are equal.
|
||||
|
||||
The comparison between elements is done using `===/2`,
|
||||
which a set with `1` is not equivalent to a set with
|
||||
`1.0`.
|
||||
The comparison between elements must be done using `===/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -233,16 +231,14 @@ defmodule MapSet do
|
||||
true
|
||||
iex> MapSet.equal?(MapSet.new([1, 2]), MapSet.new([3, 4]))
|
||||
false
|
||||
iex> MapSet.equal?(MapSet.new([1]), MapSet.new([1.0]))
|
||||
false
|
||||
|
||||
"""
|
||||
@spec equal?(t, t) :: boolean
|
||||
def equal?(%MapSet{map: map1, version: version}, %MapSet{map: map2, version: version}) do
|
||||
map1 === map2
|
||||
Map.equal?(map1, map2)
|
||||
end
|
||||
|
||||
# Elixir v1.5 changed the map representation, so on
|
||||
# Elixir v1.5 change the map representation, so on
|
||||
# version mismatch we need to compare the keys directly.
|
||||
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
|
||||
map_size(map1) == map_size(map2) and all_in?(map1, map2)
|
||||
|
||||
+45
-252
@@ -57,7 +57,7 @@ defmodule Module do
|
||||
one of the callbacks, a warning will be raised.
|
||||
|
||||
For detailed documentation, see the
|
||||
[behaviour typespec documentation](typespecs.md#behaviours).
|
||||
[behaviour typespec documentation](typespecs.html#behaviours).
|
||||
|
||||
### `@impl`
|
||||
|
||||
@@ -235,7 +235,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
For the list of supported warnings, see
|
||||
[`:dialyzer` module](`:dialyzer`).
|
||||
[`:dialyzer` module](http://www.erlang.org/doc/man/dialyzer.html).
|
||||
|
||||
Multiple uses of `@dialyzer` will accumulate instead of overriding
|
||||
previous ones.
|
||||
@@ -248,13 +248,8 @@ defmodule Module do
|
||||
attribute allows the module to annotate which external resources
|
||||
have been used.
|
||||
|
||||
Tools may use this information to ensure the module is recompiled
|
||||
in case any of the external resources change, see for example:
|
||||
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
If the external resource does not exist, the module still has
|
||||
a dependency on it, causing the module to be recompiled as soon
|
||||
as the file is added.
|
||||
Tools like Mix may use this information to ensure the module is
|
||||
recompiled in case any of the external resources change.
|
||||
|
||||
### `@file`
|
||||
|
||||
@@ -305,9 +300,9 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module or
|
||||
`{function_name, 0}` tuple where `function_name` is the name of a function in
|
||||
the current module. The function must have an arity of 0 (no arguments). If
|
||||
the function does not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
the current module. The function must be public and have an arity of 0 (no
|
||||
arguments). If the function does not return `:ok`, the loading of the module
|
||||
will be aborted. For example:
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_check
|
||||
@@ -335,16 +330,6 @@ defmodule Module do
|
||||
@vsn "1.0"
|
||||
end
|
||||
|
||||
### Struct attributes
|
||||
|
||||
* `@derive` - derives an implementation for the given protocol for the
|
||||
struct defined in the current module
|
||||
|
||||
* `@enforce_keys` - ensures the given keys are always set when building
|
||||
the struct defined in the current module
|
||||
|
||||
See `Kernel.defstruct/1` for more information on building and using structs.
|
||||
|
||||
### Typespec attributes
|
||||
|
||||
The following attributes are part of typespecs and are also built-in in
|
||||
@@ -360,8 +345,6 @@ defmodule Module do
|
||||
behaviour callbacks are optional
|
||||
* `@impl` - declares an implementation of a callback function or macro
|
||||
|
||||
For detailed documentation, see the [typespec documentation](typespecs.md).
|
||||
|
||||
### Custom attributes
|
||||
|
||||
In addition to the built-in attributes outlined above, custom attributes may
|
||||
@@ -516,6 +499,8 @@ defmodule Module do
|
||||
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
|
||||
the given module or the given `Mod.fun/arity` are not defined
|
||||
|
||||
You can see a handful more options used by the Erlang compiler in
|
||||
the documentation for the [`:compile` module](http://www.erlang.org/doc/man/compile.html).
|
||||
'''
|
||||
|
||||
@typep definition :: {atom, arity}
|
||||
@@ -553,103 +538,6 @@ defmodule Module do
|
||||
@callback __info__(:md5) :: binary()
|
||||
@callback __info__(:module) :: module()
|
||||
|
||||
@doc """
|
||||
Returns information about module attributes used by Elixir.
|
||||
|
||||
See the "Module attributes" section in the module documentation for more
|
||||
information on each attribute.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = Module.reserved_attributes()
|
||||
iex> Map.has_key?(map, :moduledoc)
|
||||
true
|
||||
iex> Map.has_key?(map, :doc)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def reserved_attributes() do
|
||||
%{
|
||||
after_compile: %{
|
||||
doc: "A hook that will be invoked right after the current module is compiled."
|
||||
},
|
||||
before_compile: %{
|
||||
doc: "A hook that will be invoked before the module is compiled."
|
||||
},
|
||||
behaviour: %{
|
||||
doc: "Specifies that the current module implements a given behaviour."
|
||||
},
|
||||
on_definition: %{
|
||||
doc:
|
||||
"A hook that will be invoked when each function or macro in the current module is defined."
|
||||
},
|
||||
impl: %{
|
||||
doc: "Declares an implementation of a callback function or macro."
|
||||
},
|
||||
compile: %{
|
||||
doc: "Defines options for module compilation."
|
||||
},
|
||||
deprecated: %{
|
||||
doc: "Provides the deprecation reason for a function."
|
||||
},
|
||||
moduledoc: %{
|
||||
doc: "Provides documentation for the current module."
|
||||
},
|
||||
doc: %{
|
||||
doc: "Provides documentation for a function/macro/callback."
|
||||
},
|
||||
typedoc: %{
|
||||
doc: "Provides documentation for a type."
|
||||
},
|
||||
dialyzer: %{
|
||||
doc: "Defines Dialyzer warnings to request or suppress."
|
||||
},
|
||||
external_resource: %{
|
||||
doc: "Specifies an external resource for the current module."
|
||||
},
|
||||
file: %{
|
||||
doc:
|
||||
"Changes the filename used in stacktraces for the function or macro that follows the attribute."
|
||||
},
|
||||
on_load: %{
|
||||
doc: "A hook that will be invoked whenever the module is loaded."
|
||||
},
|
||||
vsn: %{
|
||||
doc: "Specify the module version."
|
||||
},
|
||||
type: %{
|
||||
doc: "Defines a type to be used in `@spec`."
|
||||
},
|
||||
typep: %{
|
||||
doc: "Defines a private type to be used in `@spec`."
|
||||
},
|
||||
opaque: %{
|
||||
doc: "Defines an opaque type to be used in `@spec`."
|
||||
},
|
||||
spec: %{
|
||||
doc: "Provides a specification for a function."
|
||||
},
|
||||
callback: %{
|
||||
doc: "Provides a specification for a behaviour callback."
|
||||
},
|
||||
macrocallback: %{
|
||||
doc: "Provides a specification for a macro behaviour callback."
|
||||
},
|
||||
optional_callbacks: %{
|
||||
doc: "Specifies which behaviour callbacks and macro behaviour callbacks are optional."
|
||||
},
|
||||
derive: %{
|
||||
doc:
|
||||
"Derives an implementation for the given protocol for the struct defined in the current module."
|
||||
},
|
||||
enforce_keys: %{
|
||||
doc:
|
||||
"Ensures the given keys are always set when building the struct defined in the current module."
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a module is open.
|
||||
|
||||
@@ -737,7 +625,7 @@ defmodule Module do
|
||||
|
||||
It returns a tuple of shape `{:module, module, binary, term}`
|
||||
where `module` is the module name, `binary` is the module
|
||||
bytecode and `term` is the result of the last expression in
|
||||
byte code and `term` is the result of the last expression in
|
||||
`quoted`.
|
||||
|
||||
Similar to `Kernel.defmodule/2`, the binary will only be
|
||||
@@ -831,6 +719,9 @@ defmodule Module do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat([Module, Unknown])
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat([List, Chars])
|
||||
List.Chars
|
||||
|
||||
@@ -849,6 +740,9 @@ defmodule Module do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Module.safe_concat(Module, Unknown)
|
||||
** (ArgumentError) argument error
|
||||
|
||||
iex> Module.safe_concat(List, Chars)
|
||||
List.Chars
|
||||
|
||||
@@ -1155,67 +1049,11 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec definitions_in(module, def_kind) :: [definition]
|
||||
def definitions_in(module, kind)
|
||||
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
def definitions_in(module, def_kind)
|
||||
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
assert_not_compiled!(__ENV__.function, module, @extra_error_msg_definitions_in)
|
||||
{set, _} = data_tables_for(module)
|
||||
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the definition for the given name-arity pair.
|
||||
|
||||
It returns a tuple with the `version`, the `kind`,
|
||||
the definition `metadata`, and a list with each clause.
|
||||
Each clause is a four-element tuple with metadata,
|
||||
the arguments, the guards, and the clause AST.
|
||||
|
||||
The clauses are returned in the Elixir AST but a subset
|
||||
that has already been expanded and normalized. This makes
|
||||
it useful for analyzing code but it cannot be reinjected
|
||||
into the module as it will have lost some of its original
|
||||
context. Given this AST representation is mostly internal,
|
||||
it is versioned and it may change at any time. Therefore,
|
||||
**use this API with caution**.
|
||||
"""
|
||||
@spec get_definition(module, definition) ::
|
||||
{:v1, def_kind, meta :: keyword,
|
||||
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
|
||||
@doc since: "1.12.0"
|
||||
def get_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
assert_not_compiled!(__ENV__.function, module, "")
|
||||
{set, bag} = data_tables_for(module)
|
||||
|
||||
case :ets.lookup(set, {:def, {name, arity}}) do
|
||||
[{_key, kind, meta, _, _, _}] ->
|
||||
{:v1, kind, meta, bag_lookup_element(bag, {:clauses, {name, arity}}, 2)}
|
||||
|
||||
[] ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a definition from a module.
|
||||
|
||||
It returns true if the definition exists and it was removed,
|
||||
otherwise it returns false.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec delete_definition(module, definition) :: boolean()
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
|
||||
case :elixir_def.take_definition(module, {name, arity}) do
|
||||
false ->
|
||||
false
|
||||
|
||||
_ ->
|
||||
:elixir_locals.yank({name, arity}, module)
|
||||
true
|
||||
end
|
||||
:lists.concat(:ets.match(set, {{:def, :"$1"}, def_kind, :_, :_, :_, :_}))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1559,7 +1397,7 @@ defmodule Module do
|
||||
if doc, do: {:error, :private_doc}, else: :ok
|
||||
else
|
||||
{set, _bag} = data_tables_for(module)
|
||||
compile_doc(set, nil, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
|
||||
compile_doc(set, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1572,17 +1410,16 @@ defmodule Module do
|
||||
{set, bag} = data_tables_for(module)
|
||||
{arity, defaults} = args_count(args, 0, 0)
|
||||
|
||||
context = Keyword.get(:ets.lookup_element(set, {:def, {name, arity}}, 3), :context)
|
||||
impl = compile_impl(set, bag, context, name, env, kind, arity, defaults)
|
||||
impl = compile_impl(set, bag, name, env, kind, arity, defaults)
|
||||
doc_meta = compile_doc_meta(set, bag, name, arity, defaults)
|
||||
|
||||
{line, doc} = get_doc_info(set, env)
|
||||
compile_doc(set, context, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
|
||||
compile_doc(set, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp compile_doc(_table, _ctx, line, kind, name, arity, _args, _body, doc, _meta, env, _impl)
|
||||
defp compile_doc(_table, line, kind, name, arity, _args, _body, doc, _doc_meta, env, _impl)
|
||||
when kind in [:defp, :defmacrop] do
|
||||
if doc do
|
||||
message =
|
||||
@@ -1593,38 +1430,21 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
defp compile_doc(table, ctx, line, kind, name, arity, args, body, doc, doc_meta, env, impl) do
|
||||
defp compile_doc(table, line, kind, name, arity, args, _body, doc, doc_meta, env, impl) do
|
||||
key = {doc_key(kind), name, arity}
|
||||
signature = build_signature(args, env)
|
||||
|
||||
case :ets.lookup(table, key) do
|
||||
[] ->
|
||||
doc = if is_nil(doc) && impl, do: false, else: doc
|
||||
:ets.insert(table, {key, ctx, line, signature, doc, doc_meta})
|
||||
|
||||
[{_, current_ctx, current_line, current_sign, current_doc, current_doc_meta}] ->
|
||||
if is_binary(current_doc) and is_binary(doc) and body != nil and is_nil(current_ctx) do
|
||||
message = ~s'''
|
||||
redefining @doc attribute previously set at line #{current_line}.
|
||||
|
||||
Please remove the duplicate docs. If instead you want to override a \
|
||||
previously defined @doc, attach the @doc attribute to a function head \
|
||||
(the function signature not followed by any do-block). For example:
|
||||
|
||||
@doc """
|
||||
new docs
|
||||
"""
|
||||
def #{name}(...)
|
||||
'''
|
||||
|
||||
IO.warn(message, Macro.Env.stacktrace(%{env | line: line}))
|
||||
end
|
||||
:ets.insert(table, {key, line, signature, doc, doc_meta})
|
||||
|
||||
[{_, current_line, current_sign, current_doc, current_doc_meta}] ->
|
||||
signature = merge_signatures(current_sign, signature, 1)
|
||||
doc = if is_nil(doc), do: current_doc, else: doc
|
||||
doc = if is_nil(doc) && impl, do: false, else: doc
|
||||
doc_meta = Map.merge(current_doc_meta, doc_meta)
|
||||
:ets.insert(table, {key, ctx, current_line, signature, doc, doc_meta})
|
||||
:ets.insert(table, {key, current_line, signature, doc, doc_meta})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1672,12 +1492,14 @@ defmodule Module do
|
||||
defp deprecated_reason(name, arity, reason),
|
||||
do: {:deprecated, {{name, arity}, reason}}
|
||||
|
||||
defp compile_impl(set, bag, context, name, env, kind, arity, defaults) do
|
||||
defp compile_impl(set, bag, name, env, kind, arity, defaults) do
|
||||
%{line: line, file: file} = env
|
||||
|
||||
case :ets.take(set, :impl) do
|
||||
[{:impl, value, _}] ->
|
||||
impl = {{name, arity}, context, defaults, kind, line, file, value}
|
||||
pair = {name, arity}
|
||||
meta = :ets.lookup_element(set, {:def, pair}, 3)
|
||||
impl = {pair, Keyword.get(meta, :context), defaults, kind, line, file, value}
|
||||
:ets.insert(bag, {:impls, impl})
|
||||
value
|
||||
|
||||
@@ -2131,13 +1953,20 @@ defmodule Module do
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:impl, value) do
|
||||
if is_boolean(value) or (is_atom(value) and value != nil) do
|
||||
value
|
||||
else
|
||||
raise ArgumentError,
|
||||
"@impl is a built-in module attribute that marks the next definition " <>
|
||||
"as a callback implementation. It should be a module or a boolean, " <>
|
||||
"got: #{inspect(value)}"
|
||||
case value do
|
||||
_ when is_boolean(value) ->
|
||||
value
|
||||
|
||||
module when is_atom(module) and module != nil ->
|
||||
# Attempt to compile behaviour but ignore failure (will warn later)
|
||||
_ = Code.ensure_compiled(module)
|
||||
value
|
||||
|
||||
_ ->
|
||||
raise ArgumentError,
|
||||
"@impl is a built-in module attribute that marks the next definition " <>
|
||||
"as a callback implementation. It should be a module or a boolean, " <>
|
||||
"got: #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2185,46 +2014,10 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:dialyzer, value) do
|
||||
# From https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_lint.erl
|
||||
:lists.foreach(
|
||||
fn attr ->
|
||||
if not valid_dialyzer_attribute?(attr) do
|
||||
raise ArgumentError, "invalid value for @dialyzer attribute: #{inspect(attr)}"
|
||||
end
|
||||
end,
|
||||
List.wrap(value)
|
||||
)
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
defp preprocess_attribute(_key, value) do
|
||||
value
|
||||
end
|
||||
|
||||
defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do
|
||||
(key == :nowarn_function or valid_dialyzer_attribute?(key)) and
|
||||
:lists.all(
|
||||
fn
|
||||
{fun, arity} when is_atom(fun) and is_integer(arity) -> true
|
||||
_ -> false
|
||||
end,
|
||||
List.wrap(fun_arities)
|
||||
)
|
||||
end
|
||||
|
||||
defp valid_dialyzer_attribute?(attr) do
|
||||
:lists.member(
|
||||
attr,
|
||||
[:no_return, :no_unused, :no_improper_lists, :no_fun_app] ++
|
||||
[:no_match, :no_opaque, :no_fail_call, :no_contracts] ++
|
||||
[:no_behaviours, :no_undefined_callbacks, :unmatched_returns] ++
|
||||
[:error_handling, :race_conditions, :no_missing_calls] ++
|
||||
[:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs]
|
||||
)
|
||||
end
|
||||
|
||||
defp preprocess_doc_meta([], _module, _line, map), do: map
|
||||
|
||||
defp preprocess_doc_meta([{key, _} | tail], module, line, map)
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
defmodule Module.Checker do
|
||||
alias Module.ParallelChecker
|
||||
|
||||
@moduledoc false
|
||||
|
||||
def verify(module, cache) do
|
||||
case prepare_module(module) do
|
||||
{:ok, map} ->
|
||||
undefined_and_deprecation_warnings = undefined_and_deprecation_warnings(map, cache)
|
||||
infer_warnings = infer_definitions(map)
|
||||
warnings = infer_warnings ++ undefined_and_deprecation_warnings
|
||||
emit_warnings(warnings)
|
||||
|
||||
:error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_module({module, module_map}) when is_map(module_map) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: module_map.file,
|
||||
definitions: module_map.definitions,
|
||||
deprecated: module_map.deprecated,
|
||||
no_warn_undefined: no_warn_undefined(module_map.compile_opts)
|
||||
}}
|
||||
end
|
||||
|
||||
defp prepare_module({module, binary}) when is_binary(binary) do
|
||||
with {:ok, debug_info} <- debug_info(module, binary),
|
||||
{:ok, checker_info} <- checker_chunk(binary) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: debug_info.file,
|
||||
definitions: debug_info.definitions,
|
||||
deprecated: checker_info.deprecated,
|
||||
no_warn_undefined: checker_info.no_warn_undefined
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
value <- List.wrap(values),
|
||||
do: value
|
||||
)
|
||||
end
|
||||
|
||||
defp debug_info(module, binary) do
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- chunk,
|
||||
{:ok, info} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
{:ok, %{definitions: info.definitions, file: info.relative_file}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_chunk(binary) do
|
||||
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
deprecated = Enum.map(contents.exports, fn {fun, map} -> {fun, map.deprecated_reason} end)
|
||||
{:ok, %{deprecated: deprecated, no_warn_undefined: contents.no_warn_undefined}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp infer_definitions(map) do
|
||||
results = Module.Types.infer_definitions(map.file, map.module, map.definitions)
|
||||
Enum.flat_map(results, fn {_function, reasons} -> reasons end)
|
||||
end
|
||||
|
||||
defp undefined_and_deprecation_warnings(map, cache) do
|
||||
state = %{
|
||||
cache: cache,
|
||||
file: map.file,
|
||||
module: map.module,
|
||||
no_warn_undefined: merge_no_warn_undefined(map),
|
||||
function: nil,
|
||||
warnings: []
|
||||
}
|
||||
|
||||
state = check_definitions(map.definitions, state)
|
||||
|
||||
state.warnings
|
||||
|> merge_warnings()
|
||||
|> sort_warnings()
|
||||
end
|
||||
|
||||
defp merge_no_warn_undefined(map) do
|
||||
case Code.get_compiler_option(:no_warn_undefined) do
|
||||
:all ->
|
||||
:all
|
||||
|
||||
list when is_list(list) ->
|
||||
map.no_warn_undefined ++ list
|
||||
end
|
||||
end
|
||||
|
||||
defp check_definitions(definitions, state) do
|
||||
Enum.reduce(definitions, state, &check_definition/2)
|
||||
end
|
||||
|
||||
defp check_definition({function, _kind, meta, clauses}, state) do
|
||||
with_file_meta(%{state | function: function}, meta, fn state ->
|
||||
Enum.reduce(clauses, state, &check_clause/2)
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_file_meta(%{file: original_file} = state, meta, fun) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} ->
|
||||
state = fun.(%{state | file: meta_file})
|
||||
%{state | file: original_file}
|
||||
|
||||
:error ->
|
||||
fun.(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_clause({_meta, args, _guards, body}, state) do
|
||||
state = check_expr(args, state)
|
||||
check_expr(body, state)
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
defp check_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
check_remote(module, fun, arity, meta, state)
|
||||
end
|
||||
|
||||
# Mod.fun(...)
|
||||
defp check_expr({{:., meta, [module, fun]}, _, args}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
state = check_remote(module, fun, length(args), meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# %Module{...}
|
||||
defp check_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
|
||||
when is_atom(module) and is_list(args) do
|
||||
state = check_remote(module, :__struct__, 0, meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# Function call
|
||||
defp check_expr({left, _meta, right}, state) when is_list(right) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# {x, y}
|
||||
defp check_expr({left, right}, state) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# [...]
|
||||
defp check_expr(list, state) when is_list(list) do
|
||||
Enum.reduce(list, state, &check_expr/2)
|
||||
end
|
||||
|
||||
defp check_expr(_other, state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp check_remote(module, fun, arity, meta, state) do
|
||||
# TODO: In the future we may want to warn for modules defined
|
||||
# in the local context
|
||||
if Keyword.get(meta, :context_module, false) and state.module != module do
|
||||
state
|
||||
else
|
||||
ParallelChecker.preload_module(state.cache, module)
|
||||
check_export(module, fun, arity, meta, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_export(module, fun, arity, meta, state) do
|
||||
case ParallelChecker.fetch_export(state.cache, module, fun, arity) do
|
||||
{:ok, :def, reason} ->
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:ok, :defmacro, reason} ->
|
||||
state = warn(meta, state, {:unrequired_module, module, fun, arity})
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
warn(meta, state, {:undefined_module, module, fun, arity})
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
exports = ParallelChecker.all_exports(state.cache, module)
|
||||
warn(meta, state, {:undefined_function, module, fun, arity, exports})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(module, fun, arity, reason, meta, state) do
|
||||
if reason do
|
||||
warn(meta, state, {:deprecated, module, fun, arity, reason})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Do not warn inside guards
|
||||
# TODO: Properly handle protocols
|
||||
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _state), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, state) do
|
||||
not Enum.any?(state.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
defp warn(meta, state, warning) do
|
||||
{fun, arity} = state.function
|
||||
location = {state.file, meta[:line] || 0, {state.module, fun, arity}}
|
||||
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
|
||||
end
|
||||
|
||||
defp merge_warnings(warnings) do
|
||||
Enum.reduce(warnings, %{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
end
|
||||
|
||||
defp sort_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
print_warning([message, ?\n, format_locations(locations)])
|
||||
|
||||
Enum.map(locations, fn {file, line, _mfa} ->
|
||||
{file, line, message}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:undefined_function, module, fun, arity, exports}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:deprecated, module, fun, arity, reason}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:unrequired_module, module, fun, arity}) do
|
||||
[
|
||||
"you must require ",
|
||||
inspect(module),
|
||||
" before invoking the macro ",
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_locations([location]) do
|
||||
format_location(location)
|
||||
end
|
||||
|
||||
defp format_locations(locations) do
|
||||
[
|
||||
"Found at #{length(locations)} locations:\n",
|
||||
Enum.map(locations, &format_location/1)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_location({file, line, {module, fun, arity}}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line > 0, do: [Integer.to_string(line), ": "], else: []
|
||||
mfa = Exception.format_mfa(module, fun, arity)
|
||||
[" ", file, ?:, line, mfa, ?\n]
|
||||
end
|
||||
|
||||
defp print_warning(message) do
|
||||
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
|
||||
end
|
||||
end
|
||||
@@ -137,7 +137,7 @@ defmodule Module.LocalsTracker do
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defmacrop, reachable, reattached) do
|
||||
# All private macros are unreachable unless they have been
|
||||
# All private micros are unreachable unless they have been
|
||||
# reattached and they are reachable.
|
||||
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
|
||||
end
|
||||
|
||||
@@ -4,31 +4,23 @@ defmodule Module.ParallelChecker do
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type kind() :: :def | :defmacro
|
||||
@type mode() :: :elixir | :erlang
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
binaries and a list of warnings from the verification.
|
||||
"""
|
||||
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer() | nil) :: [warning()]
|
||||
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer()) :: [warning()]
|
||||
def verify(compiled_modules, runtime_binaries, schedulers \\ nil) do
|
||||
compiled_maps = Enum.map(compiled_modules, fn {map, _binary} -> {map.module, map} end)
|
||||
check_modules = compiled_maps ++ runtime_binaries
|
||||
|
||||
case compiled_maps ++ runtime_binaries do
|
||||
[] ->
|
||||
[]
|
||||
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
{:ok, server} = :gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
|
||||
preload_cache(get_ets(server), check_modules)
|
||||
start(server)
|
||||
|
||||
check_modules ->
|
||||
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
|
||||
{:ok, server} =
|
||||
:gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
|
||||
|
||||
preload_cache(get_ets(server), check_modules)
|
||||
start(server)
|
||||
collect_results(length(check_modules), [])
|
||||
end
|
||||
collect_results(length(check_modules), [])
|
||||
end
|
||||
|
||||
defp collect_results(0, warnings) do
|
||||
@@ -42,14 +34,6 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, pid} = :gen_server.start_link(__MODULE__, [[], self(), 1], [])
|
||||
{pid, get_ets(pid)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Preloads a module into the cache. Call this function before any other
|
||||
cache lookups for the module.
|
||||
@@ -68,17 +52,17 @@ defmodule Module.ParallelChecker do
|
||||
or if the function does not exist return `{:error, :function}`.
|
||||
"""
|
||||
@spec fetch_export(cache(), module(), atom(), arity()) ::
|
||||
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
|
||||
{:ok, kind(), binary() | nil} | {:error, :function | :module}
|
||||
def fetch_export({_server, ets}, module, fun, arity) do
|
||||
case :ets.lookup(ets, {:cached, module}) do
|
||||
[{_key, false}] ->
|
||||
{:error, :module}
|
||||
|
||||
[{_key, mode}] ->
|
||||
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
|
||||
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
|
||||
[{_key, true}] ->
|
||||
case :ets.lookup(ets, {:export, {module, fun, arity}}) do
|
||||
[{_key, kind, reason}] -> {:ok, kind, reason}
|
||||
[] -> {:error, :function}
|
||||
end
|
||||
|
||||
[{_key, false}] ->
|
||||
{:error, :module}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -90,122 +74,13 @@ defmodule Module.ParallelChecker do
|
||||
def all_exports({_server, ets}, module) do
|
||||
# This is only called after we get a deprecation notice
|
||||
# so we can assume it's a cached module
|
||||
ets
|
||||
|> :ets.match({{:export, module, :"$1"}, :_, :_})
|
||||
|> Enum.flat_map(& &1)
|
||||
[{_key, exports}] = :ets.lookup(ets, {:all_exports, module})
|
||||
|
||||
exports
|
||||
|> Enum.map(fn {function, _kind} -> function end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module, cache) do
|
||||
case extract_definitions(module) do
|
||||
{:ok, module, file, definitions, no_warn_undefined} ->
|
||||
Module.Types.warnings(module, file, definitions, no_warn_undefined, cache)
|
||||
|> group_warnings()
|
||||
|> emit_warnings()
|
||||
|
||||
:error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_definitions({module, module_map}) when is_map(module_map) do
|
||||
no_warn_undefined =
|
||||
module_map.compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
|
||||
{:ok, module, module_map.file, module_map.definitions, no_warn_undefined}
|
||||
end
|
||||
|
||||
defp extract_definitions({module, binary}) when is_binary(binary) do
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
extract_definitions({module, module_map})
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
value <- List.wrap(values),
|
||||
do: value
|
||||
)
|
||||
end
|
||||
|
||||
defp merge_compiler_no_warn_undefined(no_warn_undefined) do
|
||||
case Code.get_compiler_option(:no_warn_undefined) do
|
||||
:all ->
|
||||
:all
|
||||
|
||||
list when is_list(list) ->
|
||||
no_warn_undefined ++ list
|
||||
end
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
def group_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
def emit_warnings(warnings) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
print_warning([message, ?\n, format_locations(locations)])
|
||||
|
||||
Enum.map(locations, fn {file, line, _mfa} ->
|
||||
{file, line, message}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_locations([location]) do
|
||||
format_location(location)
|
||||
end
|
||||
|
||||
defp format_locations(locations) do
|
||||
[
|
||||
"Found at #{length(locations)} locations:\n",
|
||||
Enum.map(locations, &format_location/1)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_location({file, line, {module, fun, arity}}) do
|
||||
mfa = Exception.format_mfa(module, fun, arity)
|
||||
[format_file_line(file, line), ": ", mfa, ?\n]
|
||||
end
|
||||
|
||||
defp format_location({file, line, nil}) do
|
||||
[format_file_line(file, line), ?\n]
|
||||
end
|
||||
|
||||
defp format_location({file, line, module}) do
|
||||
[format_file_line(file, line), ": ", inspect(module), ?\n]
|
||||
end
|
||||
|
||||
defp format_file_line(file, line) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line > 0, do: [?: | Integer.to_string(line)], else: []
|
||||
[" ", file, line]
|
||||
end
|
||||
|
||||
defp print_warning(message) do
|
||||
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
|
||||
def init([modules, send_results, schedulers]) do
|
||||
ets = :ets.new(:checker_cache, [:set, :public, {:read_concurrency, true}])
|
||||
|
||||
@@ -244,6 +119,10 @@ defmodule Module.ParallelChecker do
|
||||
{:reply, ets, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, %{modules: []} = state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, state) do
|
||||
{:noreply, spawn_checkers(state)}
|
||||
end
|
||||
@@ -297,7 +176,7 @@ defmodule Module.ParallelChecker do
|
||||
send_results_pid = state.send_results
|
||||
|
||||
spawn_link(fn ->
|
||||
warnings = check_module(verify, {parent, ets})
|
||||
warnings = Module.Checker.verify(verify, {parent, ets})
|
||||
send(send_results_pid, {__MODULE__, module, warnings})
|
||||
send(parent, {__MODULE__, :done})
|
||||
end)
|
||||
@@ -336,31 +215,28 @@ defmodule Module.ParallelChecker do
|
||||
definitions_to_exports(map.definitions)
|
||||
|
||||
deprecated = Map.new(map.deprecated)
|
||||
cache_info(ets, map.module, exports, deprecated, :elixir)
|
||||
cache_info(ets, map.module, exports, deprecated)
|
||||
end
|
||||
|
||||
defp cache_from_info(ets, module) do
|
||||
if Code.ensure_loaded?(module) do
|
||||
{mode, exports} = info_exports(module)
|
||||
exports = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(ets, module, exports, deprecated, mode)
|
||||
cache_info(ets, module, exports, deprecated)
|
||||
else
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
end
|
||||
end
|
||||
|
||||
defp info_exports(module) do
|
||||
map =
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
|
||||
{:elixir, map}
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
rescue
|
||||
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
|
||||
_ -> Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))
|
||||
end
|
||||
|
||||
defp info_deprecated(module) do
|
||||
@@ -369,32 +245,39 @@ defmodule Module.ParallelChecker do
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
defp cache_info(ets, module, exports, deprecated, mode) do
|
||||
Enum.each(exports, fn {{fun, arity}, kind} ->
|
||||
reason = Map.get(deprecated, {fun, arity})
|
||||
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
defp cache_info(ets, module, exports, deprecated) do
|
||||
exports =
|
||||
Enum.map(exports, fn {{fun, arity}, kind} ->
|
||||
reason = Map.get(deprecated, {fun, arity})
|
||||
:ets.insert(ets, {{:export, {module, fun, arity}}, kind, reason})
|
||||
|
||||
:ets.insert(ets, {{:cached, module}, mode})
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:all_exports, module}, exports})
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp cache_chunk(ets, module, exports) do
|
||||
Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
|
||||
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
|
||||
exports =
|
||||
Enum.map(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
|
||||
:ets.insert(ets, {{:export, {module, fun, arity}}, kind, reason})
|
||||
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
{{fun, arity}, kind}
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
|
||||
:ets.insert(ets, {{:cached, module}, :elixir})
|
||||
:ets.insert(ets, {{:export, {module, :__info__, 1}}, :def, nil})
|
||||
exports = [{{:__info__, 1}, :def} | exports]
|
||||
|
||||
:ets.insert(ets, {{:all_exports, module}, exports})
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
|
||||
defp behaviour_exports(%{is_behaviour: false}), do: []
|
||||
|
||||
defp behaviour_exports(module) when is_atom(module) do
|
||||
if function_exported?(module, :behaviour_info, 1) do
|
||||
if {:behaviour_info, 1} in module.module_info(:functions) do
|
||||
[{{:behaviour_info, 1}, :def}]
|
||||
else
|
||||
[]
|
||||
|
||||
+266
-333
@@ -1,75 +1,69 @@
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
|
||||
defmodule Error do
|
||||
defexception [:message]
|
||||
import Module.Types.Helpers
|
||||
alias Module.Types.{Expr, Pattern}
|
||||
|
||||
@doc """
|
||||
Infer function definitions' types.
|
||||
"""
|
||||
def infer_definitions(file, module, defs) do
|
||||
clauses = infer_signatures(file, module, defs)
|
||||
infer_bodies(clauses)
|
||||
end
|
||||
|
||||
import Module.Types.Helpers
|
||||
alias Module.Types.{Expr, Pattern, Unify}
|
||||
defp infer_signatures(file, module, defs) do
|
||||
Enum.map(defs, fn {{fun, _arity} = function, kind, meta, clauses} ->
|
||||
stack = head_stack()
|
||||
context = head_context(file, module, function)
|
||||
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
stack = stack()
|
||||
clauses =
|
||||
Enum.map(clauses, fn {_meta, params, guards, body} ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], params})]}
|
||||
stack = push_expr_stack(def_expr, stack)
|
||||
|
||||
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
|
||||
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
|
||||
case of_head(params, guards, stack, context) do
|
||||
{:ok, _signature, context} -> {:ok, {context, body}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
|
||||
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
|
||||
|
||||
try do
|
||||
warnings_from_clause(args, guards, body, def_expr, stack, context)
|
||||
rescue
|
||||
e ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
|
||||
|
||||
error =
|
||||
Error.exception("""
|
||||
found error while checking types for #{Exception.format_mfa(module, fun, arity)}
|
||||
|
||||
#{Macro.to_string(def_expr)}
|
||||
|
||||
Please report this bug: https://github.com/elixir-lang/elixir/issues
|
||||
|
||||
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
|
||||
""")
|
||||
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end)
|
||||
{function, clauses}
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_file_meta(meta, file) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> meta_file
|
||||
:error -> file
|
||||
end
|
||||
end
|
||||
defp infer_bodies(clauses) do
|
||||
Enum.map(clauses, fn {function, clauses} ->
|
||||
errors =
|
||||
Enum.flat_map(clauses, fn
|
||||
{:ok, {head_context, body}} ->
|
||||
stack = body_stack()
|
||||
context = body_context(head_context)
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
case Expr.of_expr(body, stack, context) do
|
||||
{:ok, _type, _context} -> []
|
||||
{:error, reason} -> [reason]
|
||||
end
|
||||
|
||||
defp guards_to_expr([guard | guards], left) do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
{:error, reason} ->
|
||||
[reason]
|
||||
end)
|
||||
|
||||
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
|
||||
head_stack = Unify.push_expr_stack(def_expr, stack)
|
||||
|
||||
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
|
||||
{:ok, _type, context} <- Expr.of_expr(body, :dynamic, stack, context) do
|
||||
context.warnings
|
||||
else
|
||||
{:error, {type, error, context}} ->
|
||||
[error_to_warning(type, error, context) | context.warnings]
|
||||
end
|
||||
{function, errors}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def context(file, module, function, no_warn_undefined, cache) do
|
||||
def of_head(params, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns boolean() | :fail
|
||||
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, lift_types(types, context), context}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def head_context(file, module, function) do
|
||||
%{
|
||||
# File of module
|
||||
file: file,
|
||||
@@ -77,10 +71,6 @@ defmodule Module.Types do
|
||||
module: module,
|
||||
# Current function
|
||||
function: function,
|
||||
# List of calls to not warn on as undefined
|
||||
no_warn_undefined: no_warn_undefined,
|
||||
# A list of cached modules received from the parallel compiler
|
||||
cache: cache,
|
||||
# Expression variable to type variable
|
||||
vars: %{},
|
||||
# Type variable to expression variable
|
||||
@@ -92,206 +82,226 @@ defmodule Module.Types do
|
||||
traces: %{},
|
||||
# Counter to give type variables unique names
|
||||
counter: 0,
|
||||
# Track if a variable was inferred from a type guard function such is_tuple/1
|
||||
# Track if a variable was infered from a type guard function such is_tuple/1
|
||||
# or a guard function that fails such as elem/2, possible values are:
|
||||
# `:guarded` when `is_tuple(x)`
|
||||
# `:guarded` when `is_tuple and elem(x, 0)`
|
||||
# `:fail` when `elem(x, 0)`
|
||||
guard_sources: %{},
|
||||
# A list with all warnings from the running the code
|
||||
warnings: []
|
||||
# `:guarded_fail` when `is_tuple and elem(x, 0)`
|
||||
guard_sources: %{}
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stack() do
|
||||
def head_stack() do
|
||||
%{
|
||||
# Stack of variables we have refined during unification,
|
||||
# used for creating relevant traces
|
||||
unify_stack: [],
|
||||
# Last expression we have recursed through during inference,
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
last_expr: nil,
|
||||
expr_stack: [],
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
# There are two factors that control how we track guards.
|
||||
#
|
||||
# * consider_type_guards?: if type guards should be considered.
|
||||
# This applies only at the root and root-based "and" and "or" nodes.
|
||||
#
|
||||
# * keep_guarded? - if a guarded clause should remain as guarded
|
||||
# even on failure. Used on the right side of and.
|
||||
#
|
||||
type_guards: {_consider_type_guards? = true, _keep_guarded? = false},
|
||||
# Track if we are in a context where type guard functions should
|
||||
# affect inference
|
||||
type_guards_enabled?: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: nil
|
||||
context: :pattern
|
||||
}
|
||||
end
|
||||
|
||||
## ERROR TO WARNING
|
||||
|
||||
# Collect relevant information from context and traces to report error
|
||||
def error_to_warning(:unable_unify, {left, right, stack}, context) do
|
||||
{fun, arity} = context.function
|
||||
line = get_meta(stack.last_expr)[:line]
|
||||
location = {context.file, line, {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(stack, context)
|
||||
{left, right, traces} = lift_all_types(left, right, traces, context)
|
||||
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
|
||||
{Module.Types, error, location}
|
||||
@doc false
|
||||
def body_context(head_context) do
|
||||
%{
|
||||
# File of module
|
||||
file: head_context.file,
|
||||
# Module of definitions
|
||||
module: head_context.module,
|
||||
# Current function
|
||||
function: head_context.function,
|
||||
# Expression variable to type variable
|
||||
vars: head_context.vars,
|
||||
# Type variable to expression variable
|
||||
types_to_vars: head_context.types_to_vars,
|
||||
# Type variable to type
|
||||
types: head_context.types,
|
||||
# Trace of all variables that have been refined to a type,
|
||||
# including the type they were refined to, why, and where
|
||||
traces: head_context.traces,
|
||||
# Counter to give type variables unique names
|
||||
counter: head_context.counter
|
||||
}
|
||||
end
|
||||
|
||||
# Collect relevant traces from context.traces using stack.unify_stack
|
||||
defp type_traces(stack, context) do
|
||||
# TODO: Do we need the unify_stack or is enough to only get the last variable
|
||||
# in the stack since we get related variables anyway?
|
||||
stack =
|
||||
stack.unify_stack
|
||||
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|
||||
|> Enum.uniq()
|
||||
|
||||
Enum.flat_map(stack, fn var_index ->
|
||||
with %{^var_index => traces} <- context.traces,
|
||||
%{^var_index => expr_var} <- context.types_to_vars do
|
||||
Enum.map(traces, &tag_trace(expr_var, &1, context))
|
||||
else
|
||||
_other -> []
|
||||
end
|
||||
end)
|
||||
@doc false
|
||||
def body_stack() do
|
||||
%{
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
expr_stack: [],
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: :expr
|
||||
}
|
||||
end
|
||||
|
||||
defp related_variables(var, types) do
|
||||
Enum.flat_map(types, fn
|
||||
{related_var, {:var, ^var}} ->
|
||||
[related_var | related_variables(related_var, types)]
|
||||
@doc """
|
||||
Lifts type variables to their infered types from the context.
|
||||
"""
|
||||
def lift_types(types, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
lifted_types: %{},
|
||||
lifted_counter: 0
|
||||
}
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
{types, _context} = Enum.map_reduce(types, context, &do_lift_type/2)
|
||||
types
|
||||
end
|
||||
|
||||
# Tag if trace is for a concrete type or type variable
|
||||
defp tag_trace(var, {type, expr, location}, context) do
|
||||
with {:var, var_index} <- type,
|
||||
%{^var_index => expr_var} <- context.types_to_vars do
|
||||
{:var, var, expr_var, expr, location}
|
||||
else
|
||||
_ -> {:type, var, type, expr, location}
|
||||
@doc false
|
||||
def lift_type(type, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
lifted_types: %{},
|
||||
lifted_counter: 0
|
||||
}
|
||||
|
||||
{type, _context} = do_lift_type(type, context)
|
||||
type
|
||||
end
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Remove this and let multiple when be treated as multiple clauses,
|
||||
# meaning they will be intersection types
|
||||
defp guards_to_or([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp guards_to_or(guards) do
|
||||
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
|
||||
end
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
|
||||
defp guards_to_expr([guard | guards], left) do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
|
||||
## VARIABLE LIFTING
|
||||
|
||||
# Lift type variable to its infered (hopefully concrete) types from the context
|
||||
defp do_lift_type({:var, var}, context) do
|
||||
case Map.fetch(context.lifted_types, var) do
|
||||
{:ok, lifted_var} ->
|
||||
{{:var, lifted_var}, context}
|
||||
|
||||
:error ->
|
||||
case Map.fetch(context.types, var) do
|
||||
{:ok, :unbound} ->
|
||||
new_lifted_var(var, context)
|
||||
|
||||
{:ok, type} ->
|
||||
# Remove visited types to avoid infinite loops
|
||||
# then restore after we are done recursing on vars
|
||||
types = context.types
|
||||
context = %{context | types: Map.delete(context.types, var)}
|
||||
{type, context} = do_lift_type(type, context)
|
||||
{type, %{context | types: types}}
|
||||
|
||||
:error ->
|
||||
new_lifted_var(var, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp lift_all_types(left, right, traces, context) do
|
||||
all_types = [left, right] ++ for({:type, _, type, _, _} <- traces, do: type)
|
||||
[left, right | all_types] = Unify.lift_types(all_types, context)
|
||||
defp do_lift_type({:tuple, types}, context) do
|
||||
{types, context} = Enum.map_reduce(types, context, &do_lift_type/2)
|
||||
{{:tuple, types}, context}
|
||||
end
|
||||
|
||||
{traces, []} =
|
||||
Enum.map_reduce(traces, all_types, fn
|
||||
{:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc}
|
||||
other, acc -> {other, acc}
|
||||
defp do_lift_type({:map, pairs}, context) do
|
||||
{pairs, context} =
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{key, context} = do_lift_type(key, context)
|
||||
{value, context} = do_lift_type(value, context)
|
||||
{{key, value}, context}
|
||||
end)
|
||||
|
||||
{left, right, traces}
|
||||
{{:map, pairs}, context}
|
||||
end
|
||||
|
||||
## FORMAT WARNINGS
|
||||
defp do_lift_type({:list, type}, context) do
|
||||
{type, context} = do_lift_type(type, context)
|
||||
{{:list, type}, context}
|
||||
end
|
||||
|
||||
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
|
||||
cond do
|
||||
map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) ->
|
||||
{:ok, atom, known_atoms} = missing_field(left, right)
|
||||
defp do_lift_type(other, context) do
|
||||
{other, context}
|
||||
end
|
||||
|
||||
# Drop the last trace which is the expression map.foo
|
||||
traces = Enum.drop(traces, 1)
|
||||
{traces, hints} = format_traces(traces, true)
|
||||
defp new_lifted_var(original_var, context) do
|
||||
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
|
||||
counter = context.lifted_counter + 1
|
||||
|
||||
type = {:var, context.lifted_counter}
|
||||
context = %{context | lifted_types: types, lifted_counter: counter}
|
||||
{type, context}
|
||||
end
|
||||
|
||||
## ERROR FORMATTING
|
||||
|
||||
def format_warning({:unable_unify, left, right, expr, traces}) do
|
||||
[
|
||||
"incompatible types:\n\n ",
|
||||
format_type(left),
|
||||
" !~ ",
|
||||
format_type(right),
|
||||
"\n\n",
|
||||
format_expr(expr),
|
||||
format_traces(traces),
|
||||
"Conflict found at"
|
||||
]
|
||||
end
|
||||
|
||||
defp format_expr(nil) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_expr(expr) do
|
||||
[
|
||||
"in expression:\n\n ",
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
end
|
||||
|
||||
defp format_traces([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_traces(traces) do
|
||||
Enum.map(traces, fn
|
||||
{var, {:type, type, expr, location}} ->
|
||||
[
|
||||
"undefined field \"#{atom}\" ",
|
||||
format_expr(expr, location),
|
||||
"expected one of the following fields: ",
|
||||
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
|
||||
"\n\n",
|
||||
traces,
|
||||
format_message_hints(hints),
|
||||
"Conflict found at"
|
||||
]
|
||||
|
||||
true ->
|
||||
simplify_left? = simplify_type?(left, right)
|
||||
simplify_right? = simplify_type?(right, left)
|
||||
|
||||
{traces, hints} = format_traces(traces, simplify_left? or simplify_right?)
|
||||
|
||||
[
|
||||
"incompatible types:\n\n ",
|
||||
Unify.format_type(left, simplify_left?),
|
||||
" !~ ",
|
||||
Unify.format_type(right, simplify_right?),
|
||||
"\n\n",
|
||||
format_expr(expr, location),
|
||||
traces,
|
||||
format_message_hints(hints),
|
||||
"Conflict found at"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp missing_field(
|
||||
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
|
||||
{:map, fields}
|
||||
) do
|
||||
matched_missing_field(fields, type, atom)
|
||||
end
|
||||
|
||||
defp missing_field(
|
||||
{:map, fields},
|
||||
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
|
||||
) do
|
||||
matched_missing_field(fields, type, atom)
|
||||
end
|
||||
|
||||
defp missing_field(_, _), do: :error
|
||||
|
||||
defp matched_missing_field(fields, type, atom) do
|
||||
if List.keymember?(fields, type, 1) do
|
||||
:error
|
||||
else
|
||||
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
|
||||
{:ok, atom, known_atoms}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_traces([], _simplify?) do
|
||||
{[], []}
|
||||
end
|
||||
|
||||
defp format_traces(traces, simplify?) do
|
||||
traces
|
||||
|> Enum.uniq()
|
||||
|> Enum.reverse()
|
||||
|> Enum.map_reduce([], fn
|
||||
{:type, var, type, expr, location}, hints ->
|
||||
{hint, hints} = format_type_hint(type, expr, hints)
|
||||
|
||||
trace = [
|
||||
"where \"",
|
||||
Macro.to_string(var),
|
||||
"\" was given the type ",
|
||||
Unify.format_type(type, simplify?),
|
||||
hint,
|
||||
Module.Types.format_type(type),
|
||||
" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
indent(expr_to_string(expr)),
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
|
||||
{trace, hints}
|
||||
|
||||
{:var, var1, var2, expr, location}, hints ->
|
||||
trace = [
|
||||
{var1, {:var, var2, expr, location}} ->
|
||||
[
|
||||
"where \"",
|
||||
Macro.to_string(var1),
|
||||
"\" was given the same type as \"",
|
||||
@@ -299,42 +309,57 @@ defmodule Module.Types do
|
||||
"\" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
indent(expr_to_string(expr)),
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
|
||||
{trace, hints}
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_location({file, line, _mfa}) do
|
||||
format_location({file, line})
|
||||
end
|
||||
|
||||
defp format_location({file, line}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line, do: [Integer.to_string(line)], else: []
|
||||
[file, ?:, line, ?\n]
|
||||
end
|
||||
|
||||
defp simplify_type?(type, other) do
|
||||
map_type?(type) and not map_type?(other)
|
||||
@doc false
|
||||
def format_type({:union, types}) do
|
||||
"#{Enum.map_join(types, " | ", &format_type/1)}"
|
||||
end
|
||||
|
||||
## EXPRESSION FORMATTING
|
||||
|
||||
defp format_expr(nil, _location) do
|
||||
[]
|
||||
def format_type({:tuple, types}) do
|
||||
"{#{Enum.map_join(types, ", ", &format_type/1)}}"
|
||||
end
|
||||
|
||||
defp format_expr(expr, location) do
|
||||
[
|
||||
"in expression:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
indent(expr_to_string(expr)),
|
||||
"\n\n"
|
||||
]
|
||||
def format_type({:list, type}) do
|
||||
"[#{format_type(type)}]"
|
||||
end
|
||||
|
||||
def format_type({:map, pairs}) do
|
||||
case List.keytake(pairs, :__struct__, 0) do
|
||||
{{:__struct__, struct}, pairs} ->
|
||||
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
|
||||
|
||||
nil ->
|
||||
"%{#{format_map_pairs(pairs)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:atom, literal}) do
|
||||
inspect(literal)
|
||||
end
|
||||
|
||||
def format_type(atom) when is_atom(atom) do
|
||||
"#{atom}()"
|
||||
end
|
||||
|
||||
def format_type({:var, index}) do
|
||||
"var#{index}"
|
||||
end
|
||||
|
||||
defp format_map_pairs(pairs) do
|
||||
Enum.map_join(pairs, ", ", fn {left, right} ->
|
||||
"#{format_type(left)} => #{format_type(right)}"
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -346,109 +371,17 @@ defmodule Module.Types do
|
||||
|
||||
defp reverse_rewrite(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
|
||||
{:., _, [:erlang, :orelse]} -> :or
|
||||
{:., _, [:erlang, :andalso]} -> :and
|
||||
{{:., _, [mod, fun]}, _, args} -> erl_to_ex(mod, fun, args)
|
||||
other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
defp erl_to_ex(mod, fun, args) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
{Kernel, fun, args} -> {fun, [], args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, [], args}
|
||||
end
|
||||
end
|
||||
|
||||
## Hints
|
||||
|
||||
defp format_message_hints(hints) do
|
||||
hints |> Enum.uniq() |> Enum.reverse() |> Enum.map(&format_message_hint/1)
|
||||
end
|
||||
|
||||
defp format_message_hint(:inferred_dot) do
|
||||
"""
|
||||
HINT: "var.field" (without parentheses) implies "var" is a map() while \
|
||||
"var.fun()" (with parentheses) implies "var" is an atom()
|
||||
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_message_hint(:inferred_bitstring_spec) do
|
||||
"""
|
||||
HINT: all expressions given to binaries are assumed to be of type \
|
||||
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
|
||||
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
|
||||
to change the default behaviour.
|
||||
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_type_hint(type, expr, hints) do
|
||||
case format_type_hint(type, expr) do
|
||||
{message, hint} -> {message, [hint | hints]}
|
||||
:error -> {[], hints}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_type_hint(type, expr) do
|
||||
cond do
|
||||
dynamic_map_dot?(type, expr) ->
|
||||
{" (due to calling var.field)", :inferred_dot}
|
||||
|
||||
dynamic_remote_call?(type, expr) ->
|
||||
{" (due to calling var.fun())", :inferred_dot}
|
||||
|
||||
inferred_bitstring_spec?(type, expr) ->
|
||||
{[], :inferred_bitstring_spec}
|
||||
|
||||
true ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp dynamic_map_dot?(type, expr) do
|
||||
with true <- map_type?(type),
|
||||
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
|
||||
true <- Keyword.get(meta2, :no_parens, false) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp dynamic_remote_call?(type, expr) do
|
||||
with true <- atom_type?(type),
|
||||
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
|
||||
false <- Keyword.get(meta2, :no_parens, false) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp inferred_bitstring_spec?(type, expr) do
|
||||
with true <- integer_type?(type),
|
||||
{:<<>>, _, args} <- expr,
|
||||
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
## Formatting helpers
|
||||
|
||||
defp indent(string) do
|
||||
String.replace(string, "\n", " \n")
|
||||
end
|
||||
|
||||
defp map_type?({:map, _}), do: true
|
||||
defp map_type?(_other), do: false
|
||||
|
||||
defp atom_type?(:atom), do: true
|
||||
defp atom_type?({:atom, _}), do: false
|
||||
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
|
||||
defp atom_type?(_other), do: false
|
||||
|
||||
defp integer_type?(:integer), do: true
|
||||
defp integer_type?(_other), do: false
|
||||
end
|
||||
|
||||
@@ -1,524 +1,7 @@
|
||||
defmodule Module.Types.Expr do
|
||||
@moduledoc false
|
||||
|
||||
alias Module.Types.{Of, Pattern}
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
def of_expr(expr, expected, %{context: stack_context} = stack, context)
|
||||
when stack_context != :expr do
|
||||
of_expr(expr, expected, %{stack | context: :expr}, context)
|
||||
end
|
||||
|
||||
# :atom
|
||||
def of_expr(atom, _expected, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
def of_expr(literal, _expected, _stack, context) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
def of_expr(literal, _expected, _stack, context) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
def of_expr(literal, _expected, _stack, context) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# #PID<...>
|
||||
def of_expr(literal, _expected, _stack, context) when is_pid(literal) do
|
||||
def of_expr(_expr, _stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_expr({:<<>>, _meta, args}, _expected, stack, context) do
|
||||
case Of.binary(args, stack, context, &of_expr/4) do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
def of_expr({:|, _meta, [left_expr, []]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
of_expr(left_expr, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
def of_expr({:|, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_expr(left_expr, :dynamic, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case of_expr(right_expr, :dynamic, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
def of_expr([], _expected, _stack, context) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_expr(exprs, _expected, stack, context) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# __CALLER__
|
||||
def of_expr({:__CALLER__, _meta, var_context}, _expected, _stack, context)
|
||||
when is_atom(var_context) do
|
||||
struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}}
|
||||
|
||||
pairs =
|
||||
Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} ->
|
||||
{:required, {:atom, key}, :dynamic}
|
||||
end)
|
||||
|
||||
{:ok, {:map, [struct_pair | pairs]}, context}
|
||||
end
|
||||
|
||||
# __STACKTRACE__
|
||||
def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _stack, context)
|
||||
when is_atom(var_context) do
|
||||
file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]}
|
||||
line = {:tuple, 2, [{:atom, :line}, :integer]}
|
||||
file_line = {:list, {:union, [file, line]}}
|
||||
type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}}
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr(var, _expected, _stack, context) when is_var(var) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, expected, stack, context) do
|
||||
of_expr({:{}, [], [left, right]}, expected, stack, context)
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_expr({:{}, _meta, exprs} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do
|
||||
# TODO: We might want to bring the expected type forward in case the type of this
|
||||
# pattern is not useful. For example: 1 = _ = expr
|
||||
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, left_type, context} <-
|
||||
Pattern.of_pattern(left_expr, stack, context),
|
||||
{:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context),
|
||||
do: unify(right_type, left_type, stack, context)
|
||||
end
|
||||
|
||||
# %{map | ...}
|
||||
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
map_type = {:map, [{:optional, :dynamic, :dynamic}]}
|
||||
|
||||
with {:ok, map_type, context} <- of_expr(map, map_type, stack, context),
|
||||
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/4),
|
||||
dynamic_value_pairs =
|
||||
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
|
||||
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
|
||||
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
|
||||
# Retrieve map type and overwrite with the new value types from the map update
|
||||
{:map, pairs} = resolve_var(type, context)
|
||||
|
||||
updated_pairs =
|
||||
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
|
||||
List.keyreplace(pairs, key, 1, {:required, key, value})
|
||||
end)
|
||||
|
||||
{:ok, {:map, updated_pairs}, context}
|
||||
end
|
||||
end
|
||||
|
||||
# %Struct{map | ...}
|
||||
def of_expr(
|
||||
{:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr,
|
||||
_expected,
|
||||
stack,
|
||||
context
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
map_type = {:map, [{:optional, :dynamic, :dynamic}]}
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta, context),
|
||||
{:ok, update, context} <- of_expr(update, map_type, stack, context) do
|
||||
unify(update, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
# %{...}
|
||||
def of_expr({:%{}, _meta, args} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
Of.closed_map(args, stack, context, &of_expr/4)
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/4) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
# ()
|
||||
def of_expr({:__block__, _meta, []}, _expected, _stack, context) do
|
||||
{:ok, {:atom, nil}, context}
|
||||
end
|
||||
|
||||
# (expr; expr)
|
||||
def of_expr({:__block__, _meta, exprs}, expected, stack, context) do
|
||||
expected_types = List.duplicate(:dynamic, length(exprs) - 1) ++ [expected]
|
||||
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(exprs, expected_types), context, fn {expr, expected}, context ->
|
||||
of_expr(expr, expected, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# case expr do pat -> expr end
|
||||
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(case_expr, :dynamic, stack, context),
|
||||
{:ok, context} <- of_clauses(clauses, stack, context),
|
||||
do: {:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_clauses(clauses, stack, context) do
|
||||
{:ok, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@try_blocks [:do, :after]
|
||||
@try_clause_blocks [:catch, :else, :after]
|
||||
|
||||
# try do expr end
|
||||
def of_expr({:try, _meta, [blocks]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:rescue, clauses}, context ->
|
||||
reduce_ok(clauses, context, fn
|
||||
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
|
||||
with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{:->, _, [[var], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
|
||||
with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
end)
|
||||
|
||||
{block, body}, context = acc when block in @try_blocks ->
|
||||
with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{block, clauses}, context when block in @try_clause_blocks ->
|
||||
of_clauses(clauses, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
# receive do pat -> expr end
|
||||
def of_expr({:receive, _meta, [blocks]} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:do, {:__block__, _, []}}, context ->
|
||||
{:ok, context}
|
||||
|
||||
{:do, clauses}, context ->
|
||||
of_clauses(clauses, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
|
||||
with {:ok, _type, context} <- of_expr(head, :dynamic, stack, context),
|
||||
{:ok, _type, context} <- of_expr(body, :dynamic, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
# for pat <- expr do expr end
|
||||
def of_expr({:for, _meta, args} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
|
||||
|
||||
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
|
||||
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
|
||||
if Keyword.has_key?(opts, :reduce) do
|
||||
with {:ok, context} <- of_clauses(block, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
else
|
||||
with {:ok, _type, context} <- of_expr(block, :dynamic, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# with pat <- expr do expr end
|
||||
def of_expr({:with, _meta, clauses} = expr, _expected, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do
|
||||
{:ok, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# fun.(args)
|
||||
def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, _expected, stack, context) do
|
||||
# TODO: Use expected type to infer intersection return type
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_expr(fun, :dynamic, stack, context) do
|
||||
{:ok, _fun_type, context} ->
|
||||
case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# expr.key_or_fun
|
||||
def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, _expected, stack, context)
|
||||
when not is_atom(expr1) do
|
||||
stack = push_expr_stack(expr2, stack)
|
||||
|
||||
if Keyword.get(meta2, :no_parens, false) do
|
||||
with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
|
||||
{value_var, context} = add_var(context),
|
||||
pair_type = {:required, {:atom, key_or_fun}, value_var},
|
||||
optional_type = {:optional, :dynamic, :dynamic},
|
||||
map_field_type = {:map, [pair_type, optional_type]},
|
||||
{:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context),
|
||||
do: {:ok, value_var, context}
|
||||
else
|
||||
# TODO: Use expected type to infer intersection return type
|
||||
with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
|
||||
{:ok, _map_type, context} <- unify(expr_type, :atom, stack, context),
|
||||
do: {:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
|
||||
# expr.fun(arg)
|
||||
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, _expected, stack, context) do
|
||||
# TODO: Use expected type to infer intersection return type
|
||||
|
||||
context = Of.remote(expr1, fun, length(args), meta1, context)
|
||||
stack = push_expr_stack(expr2, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
|
||||
{:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context) do
|
||||
case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# &Foo.bar/1
|
||||
def of_expr(
|
||||
{:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]},
|
||||
_expected,
|
||||
_stack,
|
||||
context
|
||||
)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
context = Of.remote(module, fun, arity, meta, context)
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# &foo/1
|
||||
# & &1
|
||||
def of_expr({:&, _meta, _arg}, _expected, _stack, context) do
|
||||
# TODO: Function type
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# fun(arg)
|
||||
def of_expr({fun, _meta, args} = expr, _expected, stack, context)
|
||||
when is_atom(fun) and is_list(args) do
|
||||
# TODO: Use expected type to infer intersection return type
|
||||
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp for_clause({:<-, _, [left, expr]}, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
|
||||
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause(list, stack, context) when is_list(list) do
|
||||
reduce_ok(list, context, &for_option(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp for_clause(expr, stack, context) do
|
||||
of_expr_context(expr, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:into, expr}, stack, context) do
|
||||
of_expr_context(expr, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:reduce, expr}, stack, context) do
|
||||
of_expr_context(expr, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:uniq, _}, _stack, context) do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
defp with_clause({:<-, _, [left, expr]}, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp with_clause(list, stack, context) when is_list(list) do
|
||||
reduce_ok(list, context, &with_option(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp with_clause(expr, stack, context) do
|
||||
of_expr_context(expr, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
defp with_option({:do, body}, stack, context) do
|
||||
of_expr_context(body, :dynamic, stack, context)
|
||||
end
|
||||
|
||||
defp with_option({:else, clauses}, stack, context) do
|
||||
of_clauses(clauses, stack, context)
|
||||
end
|
||||
|
||||
defp of_clauses(clauses, stack, context) do
|
||||
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context = acc ->
|
||||
{patterns, guards} = extract_head(head)
|
||||
|
||||
with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp keep_warnings(context, %{warnings: warnings}) do
|
||||
%{context | warnings: warnings}
|
||||
end
|
||||
|
||||
defp extract_head([{:when, _meta, args}]) do
|
||||
case Enum.split(args, -1) do
|
||||
{patterns, [guards]} -> {patterns, flatten_when(guards)}
|
||||
{patterns, []} -> {patterns, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_head(other) do
|
||||
{other, []}
|
||||
end
|
||||
|
||||
defp flatten_when({:when, _meta, [left, right]}) do
|
||||
[left | flatten_when(right)]
|
||||
end
|
||||
|
||||
defp flatten_when(other) do
|
||||
[other]
|
||||
end
|
||||
|
||||
defp of_expr_context(expr, expected, stack, context) do
|
||||
case of_expr(expr, expected, stack, context) do
|
||||
{:ok, _type, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
|
||||
{:dynamic, context}
|
||||
end
|
||||
|
||||
defp new_pattern_var(var, context) do
|
||||
new_var(var, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
defmodule Module.Types.Helpers do
|
||||
# AST and enumeration helpers.
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
@@ -20,10 +19,14 @@ defmodule Module.Types.Helpers do
|
||||
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
|
||||
|
||||
@doc """
|
||||
Returns the AST metadata.
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | expr_stack: [expr | stack.expr_stack]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
|
||||
@@ -38,6 +41,10 @@ defmodule Module.Types.Helpers do
|
||||
{:ok, acc} ->
|
||||
do_reduce_ok(tail, acc, fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_reduce_ok(tail, result, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
@@ -74,6 +81,10 @@ defmodule Module.Types.Helpers do
|
||||
{:ok, elem} ->
|
||||
do_map_ok(tail, [elem | acc], fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_map_ok(tail, [result | acc], fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
@@ -101,23 +112,6 @@ defmodule Module.Types.Helpers do
|
||||
|
||||
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
|
||||
|
||||
def flat_map_reduce_ok(list, acc, fun) do
|
||||
do_flat_map_reduce_ok(list, {[], acc}, fun)
|
||||
end
|
||||
|
||||
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, elems, acc} ->
|
||||
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
|
||||
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
|
||||
|
||||
@doc """
|
||||
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
|
||||
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
|
||||
@@ -129,14 +123,4 @@ defmodule Module.Types.Helpers do
|
||||
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this and let multiple when be treated as multiple clauses,
|
||||
# meaning they will be intersection types
|
||||
def guards_to_or([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
def guards_to_or(guards) do
|
||||
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
defmodule Module.Types.Infer do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
|
||||
@doc """
|
||||
Unifies two types and returns the unified type and an updated typing context
|
||||
or an error in case of a typing conflict.
|
||||
"""
|
||||
def unify(source, target, stack, context) do
|
||||
case do_unify(source, target, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, reason} ->
|
||||
if stack.context == :pattern do
|
||||
case do_unify(target, source, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(same, same, _stack, context) do
|
||||
{:ok, same, context}
|
||||
end
|
||||
|
||||
defp do_unify({:var, var}, type, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = true)
|
||||
end
|
||||
|
||||
defp do_unify(type, {:var, var}, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = false)
|
||||
end
|
||||
|
||||
defp do_unify({:tuple, sources}, {:tuple, targets}, stack, context)
|
||||
when length(sources) == length(targets) do
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
|
||||
unify(source, target, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:list, source}, {:list, target}, stack, context) do
|
||||
case unify(source, target, stack, context) do
|
||||
{:ok, type, context} -> {:ok, {:list, type}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
|
||||
# Since maps in patterns only support literal keys (excluding maps)
|
||||
# we can do exact type match without subtype checking
|
||||
|
||||
unique_right_pairs =
|
||||
Enum.reject(target_pairs, fn {key, _value} ->
|
||||
:lists.keyfind(key, 1, source_pairs)
|
||||
end)
|
||||
|
||||
unique_pairs = source_pairs ++ unique_right_pairs
|
||||
|
||||
# Build union of all unique key-value pairs between the maps
|
||||
result =
|
||||
map_reduce_ok(unique_pairs, context, fn {source_key, source_value}, context ->
|
||||
case :lists.keyfind(source_key, 1, target_pairs) do
|
||||
{^source_key, target_value} ->
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} -> {:ok, {source_key, value}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
false ->
|
||||
{:ok, {source_key, source_value}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(source, :dynamic, _stack, context) do
|
||||
{:ok, source, context}
|
||||
end
|
||||
|
||||
defp do_unify(source, target, stack, context) do
|
||||
if subtype?(source, target, context) do
|
||||
{:ok, source, context}
|
||||
else
|
||||
error({:unable_unify, source, target}, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_var(var, type, stack, context, var_source?) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
context = refine_var(var, type, stack, context)
|
||||
|
||||
if recursive_type?(type, [], context) do
|
||||
if var_source? do
|
||||
error({:unable_unify, {:var, var}, type}, stack, context)
|
||||
else
|
||||
error({:unable_unify, type, {:var, var}}, stack, context)
|
||||
end
|
||||
else
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
var_type ->
|
||||
context = trace_var(var, type, stack, context)
|
||||
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, var_type, context} ->
|
||||
context = refine_var(var, var_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variables.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
"""
|
||||
def new_var(var, context) do
|
||||
case Map.fetch(context.vars, var_name(var)) do
|
||||
{:ok, type} ->
|
||||
{type, context}
|
||||
|
||||
:error ->
|
||||
type = {:var, context.counter}
|
||||
vars = Map.put(context.vars, var_name(var), type)
|
||||
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: vars,
|
||||
types_to_vars: types_to_vars,
|
||||
types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the type for a variable and add trace.
|
||||
"""
|
||||
def refine_var(var, type, stack, context) do
|
||||
types = Map.put(context.types, var, type)
|
||||
context = %{context | types: types}
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove type variable and all its traces.
|
||||
"""
|
||||
def remove_var(var, context) do
|
||||
types = Map.delete(context.types, var)
|
||||
traces = Map.delete(context.traces, var)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(var, type, %{trace: true, expr_stack: expr_stack} = _stack, context) do
|
||||
line = get_meta(hd(expr_stack))[:line]
|
||||
trace = {type, expr_stack, {context.file, line}}
|
||||
traces = Map.update!(context.traces, var, &[trace | &1])
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
# Check if a variable is recursive and incompatible with itself
|
||||
# Bad: `{var} = var`
|
||||
# Good: `x = y; y = z; z = x`
|
||||
defp recursive_type?({:var, var} = parent, parents, context) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
false
|
||||
|
||||
type ->
|
||||
if type in parents do
|
||||
not Enum.all?(parents, &match?({:var, _}, &1))
|
||||
else
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recursive_type?({:list, type} = parent, parents, context) do
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
|
||||
defp recursive_type?({:tuple, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:map, pairs} = parent, parents, context) do
|
||||
Enum.any?(pairs, fn {key, value} ->
|
||||
recursive_type?(key, [parent | parents], context) or
|
||||
recursive_type?(value, [parent | parents], context)
|
||||
end)
|
||||
end
|
||||
|
||||
defp recursive_type?(_other, _parents, _context) do
|
||||
false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
Only checks for simple and concrete types.
|
||||
"""
|
||||
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
def subtype?(:boolean, :atom, _context), do: true
|
||||
def subtype?(:float, :number, _context), do: true
|
||||
def subtype?(:integer, :number, _context), do: true
|
||||
def subtype?({:tuple, _}, :tuple, _context), do: true
|
||||
|
||||
# TODO: Lift unions to unify/3?
|
||||
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right_union, context))
|
||||
end
|
||||
|
||||
def subtype?(left, {:union, right_types}, context) do
|
||||
Enum.any?(right_types, &subtype?(left, &1, context))
|
||||
end
|
||||
|
||||
def subtype?(left, right, _context), do: left == right
|
||||
|
||||
@doc """
|
||||
Returns a "simplified" union using `subtype?/3` to remove redundant types.
|
||||
|
||||
Due to limitations in `subtype?/3` some overlapping types may still be
|
||||
included. For example unions with overlapping non-concrete types such as
|
||||
`{boolean()} | {atom()}` will not be merged or types with variables that
|
||||
are distinct but equivalent such as `a | b when a ~ b`.
|
||||
"""
|
||||
# TODO: Translate union of all top types to dynamic()
|
||||
def to_union(types, context) when types != [] do
|
||||
if :dynamic in types do
|
||||
:dynamic
|
||||
else
|
||||
case unique_super_types(flatten_union(types), context) do
|
||||
[type] -> type
|
||||
types -> {:union, types}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp flatten_union(types) do
|
||||
Enum.flat_map(types, fn
|
||||
{:union, types} -> flatten_union(types)
|
||||
type -> [type]
|
||||
end)
|
||||
end
|
||||
|
||||
# Filter subtypes
|
||||
# `boolean() | atom()` => `atom()`
|
||||
# `:foo | atom()` => `atom()`
|
||||
# Does not unify `true | false` => `boolean()`
|
||||
defp unique_super_types([type | types], context) do
|
||||
types = Enum.reject(types, &subtype?(&1, type, context))
|
||||
|
||||
if Enum.any?(types, &subtype?(type, &1, context)) do
|
||||
unique_super_types(types, context)
|
||||
else
|
||||
[type | unique_super_types(types, context)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unique_super_types([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Collect relevant information from context and traces to report error
|
||||
defp error({:unable_unify, left, right}, stack, context) do
|
||||
{fun, arity} = context.function
|
||||
line = get_meta(hd(stack.expr_stack))[:line]
|
||||
location = {context.file, line, {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(context)
|
||||
common_expr = common_super_expr(traces) || hd(stack.expr_stack)
|
||||
traces = simplify_traces(traces, context)
|
||||
|
||||
{:error, {Module.Types, {:unable_unify, left, right, common_expr, traces}, [location]}}
|
||||
end
|
||||
|
||||
defp type_traces(context) do
|
||||
Enum.flat_map(context.traces, fn {var_index, traces} ->
|
||||
expr_var = Map.fetch!(context.types_to_vars, var_index)
|
||||
Enum.map(traces, &{expr_var, &1})
|
||||
end)
|
||||
end
|
||||
|
||||
# Only use last expr from trace and tag if trace is for
|
||||
# a concrete type or type variable
|
||||
defp simplify_traces(traces, context) do
|
||||
Enum.flat_map(traces, fn {var, {type, [expr | _], location}} ->
|
||||
case type do
|
||||
{:var, var_index} ->
|
||||
var2 = Map.fetch!(context.types_to_vars, var_index)
|
||||
[{var, {:var, var2, expr, location}}]
|
||||
|
||||
_ ->
|
||||
[{var, {:type, type, expr, location}}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Find first common super expression among all traces
|
||||
defp common_super_expr([]) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp common_super_expr([{_var, {_type, expr_stack, _location}} | traces]) do
|
||||
Enum.find_value(expr_stack, fn expr ->
|
||||
common? =
|
||||
Enum.all?(traces, fn {_var, {_type, expr_stack, _location}} -> expr in expr_stack end)
|
||||
|
||||
if common? do
|
||||
expr
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_meta({_fun, meta, _args}) when is_list(meta), do: meta
|
||||
defp get_meta(_other), do: []
|
||||
end
|
||||
@@ -1,365 +0,0 @@
|
||||
defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
# Generic AST and Enum helpers go to Module.Types.Helpers.
|
||||
@moduledoc false
|
||||
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
alias Module.ParallelChecker
|
||||
|
||||
import Module.Types.Helpers
|
||||
import Module.Types.Unify
|
||||
|
||||
# There are important assumptions on how we work with maps.
|
||||
#
|
||||
# First, the keys in the map must be ordered by subtyping.
|
||||
#
|
||||
# Second, optional keys must be a superset of the required
|
||||
# keys, i.e. %{required(atom) => integer, optional(:foo) => :bar}
|
||||
# is forbidden.
|
||||
#
|
||||
# Third, in order to preserve co/contra-variance, a supertype
|
||||
# must satisfy its subtypes. I.e. %{foo: :bar, atom() => :baz}
|
||||
# is forbidden, it must be %{foo: :bar, atom() => :baz | :bar}.
|
||||
#
|
||||
# Once we support user declared maps, we need to validate these
|
||||
# assumptions.
|
||||
|
||||
@doc """
|
||||
Handles open maps (with dynamic => dynamic).
|
||||
"""
|
||||
def open_map(args, stack, context, of_fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
|
||||
# If we match on a map such as %{"foo" => "bar"}, we cannot
|
||||
# assert that %{binary() => binary()}, since we are matching
|
||||
# only a single binary of infinite possible values. Therefore,
|
||||
# the correct would be to match it to %{binary() => binary() | var}.
|
||||
#
|
||||
# We can skip this in two cases:
|
||||
#
|
||||
# 1. If the key is a singleton, then we know that it has no
|
||||
# other value than the current one
|
||||
#
|
||||
# 2. If the value is a variable, then there is no benefit in
|
||||
# creating another variable, so we can skip it
|
||||
#
|
||||
# For now, we skip generating the var itself and introduce
|
||||
# :dynamic instead.
|
||||
pairs =
|
||||
for {key, value} <- pairs, not has_unbound_var?(key, context) do
|
||||
if singleton?(key, context) or match?({:var, _}, value) do
|
||||
{key, value}
|
||||
else
|
||||
{key, to_union([value, :dynamic], context)}
|
||||
end
|
||||
end
|
||||
|
||||
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
|
||||
{:ok, {:map, triplets}, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles closed maps (without dynamic => dynamic).
|
||||
"""
|
||||
def closed_map(args, stack, context, of_fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
|
||||
{:ok, {:map, closed_to_unions(pairs, context)}, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp map_pairs(pairs, stack, context, of_fun) do
|
||||
map_reduce_ok(pairs, context, fn {key, value}, context ->
|
||||
with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context),
|
||||
{:ok, value_type, context} <- of_fun.(value, :dynamic, stack, context),
|
||||
do: {:ok, {key_type, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
|
||||
|
||||
defp closed_to_unions(pairs, context) do
|
||||
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
|
||||
{[], pairs} -> pairs_to_unions(pairs, [], context)
|
||||
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
|
||||
end
|
||||
end
|
||||
|
||||
defp pairs_to_unions([{key, value} | ahead], behind, context) do
|
||||
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
|
||||
|
||||
# In case nothing matches, use the original ahead
|
||||
ahead = matched_ahead || ahead
|
||||
|
||||
all_values =
|
||||
[value | values] ++
|
||||
find_subtype_values(ahead, key, context) ++
|
||||
find_subtype_values(behind, key, context)
|
||||
|
||||
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
|
||||
end
|
||||
|
||||
defp pairs_to_unions([], acc, context) do
|
||||
acc
|
||||
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|
||||
|> Enum.map(fn {key, value} -> {:required, key, value} end)
|
||||
end
|
||||
|
||||
defp find_subtype_values(pairs, key, context) do
|
||||
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
|
||||
end
|
||||
|
||||
defp find_matching_values([{key, value} | ahead], key, acc, values) do
|
||||
find_matching_values(ahead, key, acc, [value | values])
|
||||
end
|
||||
|
||||
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
|
||||
find_matching_values(ahead, key, [pair | acc], values)
|
||||
end
|
||||
|
||||
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
|
||||
defp find_matching_values([], _key, _acc, []), do: {nil, []}
|
||||
|
||||
@doc """
|
||||
Handles structs.
|
||||
"""
|
||||
def struct(struct, meta, context) do
|
||||
context = remote(struct, :__struct__, 0, meta, context)
|
||||
|
||||
entries =
|
||||
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
|
||||
{:required, {:atom, key}, :dynamic}
|
||||
end
|
||||
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
|
||||
end
|
||||
|
||||
## Binary
|
||||
|
||||
@doc """
|
||||
Handles binaries.
|
||||
|
||||
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
|
||||
based on the position of the expression within the binary.
|
||||
"""
|
||||
def binary([], _stack, context, _of_fun) do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
def binary([head], stack, context, of_fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
|
||||
binary_segment(head, head_stack, context, of_fun)
|
||||
end
|
||||
|
||||
def binary([head | tail], stack, context, of_fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
|
||||
|
||||
case binary_segment(head, head_stack, context, of_fun) do
|
||||
{:ok, context} -> binary_many(tail, stack, context, of_fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_many([last], stack, context, of_fun) do
|
||||
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
|
||||
binary_segment(last, last_stack, context, of_fun)
|
||||
end
|
||||
|
||||
defp binary_many([head | tail], stack, context, of_fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
|
||||
|
||||
case binary_segment(head, head_stack, context, of_fun) do
|
||||
{:ok, context} -> binary_many(tail, stack, context, of_fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do
|
||||
expected_type =
|
||||
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
|
||||
|
||||
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
|
||||
float? = collect_binary_specifier(specifiers, &float_type?/1)
|
||||
|
||||
# Special case utf and float specifiers because they can be two types as literals
|
||||
# but only a specific type as a variable in a pattern
|
||||
cond do
|
||||
stack.context == :pattern and utf? and is_binary(expr) ->
|
||||
{:ok, context}
|
||||
|
||||
stack.context == :pattern and float? and is_integer(expr) ->
|
||||
{:ok, context}
|
||||
|
||||
true ->
|
||||
with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
|
||||
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
|
||||
end
|
||||
|
||||
defp collect_binary_specifier(other, fun) do
|
||||
fun.(other)
|
||||
end
|
||||
|
||||
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
|
||||
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:float, _, _}), do: :float
|
||||
defp binary_type(_context, {:integer, _, _}), do: :integer
|
||||
defp binary_type(_context, {:bits, _, _}), do: :binary
|
||||
defp binary_type(_context, {:bitstring, _, _}), do: :binary
|
||||
defp binary_type(_context, {:bytes, _, _}), do: :binary
|
||||
defp binary_type(_context, {:binary, _, _}), do: :binary
|
||||
defp binary_type(_context, _specifier), do: nil
|
||||
|
||||
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
|
||||
defp utf_type?(_), do: false
|
||||
|
||||
defp float_type?({:float, _, _}), do: true
|
||||
defp float_type?(_), do: false
|
||||
|
||||
## Remote
|
||||
|
||||
@doc """
|
||||
Handles remote calls.
|
||||
"""
|
||||
def remote(module, fun, arity, meta, context) when is_atom(module) do
|
||||
# TODO: In the future we may want to warn for modules defined
|
||||
# in the local context
|
||||
if Keyword.get(meta, :context_module, false) and context.module != module do
|
||||
context
|
||||
else
|
||||
ParallelChecker.preload_module(context.cache, module)
|
||||
check_export(module, fun, arity, meta, context)
|
||||
end
|
||||
end
|
||||
|
||||
def remote(_module, _fun, _arity, _meta, context), do: context
|
||||
|
||||
defp check_export(module, fun, arity, meta, context) do
|
||||
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
|
||||
{:ok, mode, :def, reason} ->
|
||||
check_deprecated(mode, module, fun, arity, reason, meta, context)
|
||||
|
||||
{:ok, mode, :defmacro, reason} ->
|
||||
context = warn(meta, context, {:unrequired_module, module, fun, arity})
|
||||
check_deprecated(mode, module, fun, arity, reason, meta, context)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, context) do
|
||||
warn(meta, context, {:undefined_module, module, fun, arity})
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, context) do
|
||||
exports = ParallelChecker.all_exports(context.cache, module)
|
||||
warn(meta, context, {:undefined_function, module, fun, arity, exports})
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do
|
||||
if reason do
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do
|
||||
case :otp_internal.obsolete(module, fun, arity) do
|
||||
{:deprecated, string} when is_list(string) ->
|
||||
reason = string |> List.to_string() |> String.capitalize()
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
|
||||
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
|
||||
reason = string |> List.to_string() |> String.capitalize()
|
||||
reason = "It will be removed in #{removal}. #{reason}"
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
# The protocol code dispatches to unknown modules, so we ignore them here.
|
||||
#
|
||||
# try do
|
||||
# SomeProtocol.Atom.__impl__
|
||||
# rescue
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# But for protocols we don't want to traverse the protocol code anyway.
|
||||
# TODO: remove this clause once we no longer traverse the protocol code.
|
||||
defp warn_undefined?(_module, :__impl__, 1, _context), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _context), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _context), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, context) do
|
||||
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
defp warn(meta, context, warning) do
|
||||
{fun, arity} = context.function
|
||||
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
|
||||
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
|
||||
end
|
||||
|
||||
## Warning formating
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:undefined_function, module, fun, arity, exports}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:deprecated, module, fun, arity, reason}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:unrequired_module, module, fun, arity}) do
|
||||
[
|
||||
"you must require ",
|
||||
inspect(module),
|
||||
" before invoking the macro ",
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -1,27 +1,109 @@
|
||||
defmodule Module.Types.Pattern do
|
||||
@moduledoc false
|
||||
|
||||
alias Module.Types.Of
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
@doc """
|
||||
Handles patterns and guards at once.
|
||||
"""
|
||||
def of_head(patterns, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns boolean() | :fail
|
||||
{:ok, _, context} <- of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, types, context}
|
||||
end
|
||||
import Module.Types.{Helpers, Infer}
|
||||
|
||||
@doc """
|
||||
Return the type and typing context of a pattern expression or an error
|
||||
in case of a typing conflict.
|
||||
"""
|
||||
def of_pattern(pattern, %{context: stack_context} = stack, context)
|
||||
when stack_context != :pattern do
|
||||
of_pattern(pattern, %{stack | context: :pattern}, context)
|
||||
# :atom
|
||||
def of_pattern(atom, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
def of_pattern(literal, _stack, context) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
def of_pattern(literal, _stack, context) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
def of_pattern(literal, _stack, context) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_pattern({:<<>>, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case reduce_ok(args, context, &of_binary(&1, stack, &2)) do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
def of_pattern({:|, _meta, [left_expr, []]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
of_pattern(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
def of_pattern({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
def of_pattern([], _stack, context) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_pattern(exprs, stack, context) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
def of_pattern(
|
||||
{{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr,
|
||||
stack,
|
||||
context
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, {:list, left}, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# _
|
||||
@@ -29,17 +111,27 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# ^var
|
||||
def of_pattern({:^, _meta, [var]}, _stack, context) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# var
|
||||
def of_pattern(var, _stack, context) when is_var(var) do
|
||||
{type, context} = new_var(var, context)
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_pattern({left, right}, stack, context) do
|
||||
of_pattern({:{}, [], [left, right]}, stack, context)
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_pattern({:{}, _meta, exprs} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_pattern({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
@@ -49,6 +141,16 @@ defmodule Module.Types.Pattern do
|
||||
do: unify(left_type, right_type, stack, context)
|
||||
end
|
||||
|
||||
# %{...}
|
||||
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %_{...}
|
||||
def of_pattern(
|
||||
{:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]} = expr,
|
||||
@@ -57,84 +159,123 @@ defmodule Module.Types.Pattern do
|
||||
)
|
||||
when is_atom(var_context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end
|
||||
|
||||
with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context}
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, :atom} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %var{...} and %^var{...}
|
||||
# %var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when not is_atom(var) do
|
||||
when is_var(var) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end
|
||||
|
||||
with {:ok, var_type, context} = of_pattern(var, stack, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context),
|
||||
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
|
||||
with {:ok, pairs, context} <- of_pairs(args, stack, context),
|
||||
{var_type, context} = new_var(var, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context) do
|
||||
pairs = [{{:atom, :__struct__}, var_type} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
end
|
||||
end
|
||||
|
||||
def of_pattern(expr, stack, context) do
|
||||
of_shared(expr, stack, context, &of_pattern/3)
|
||||
# %Struct{...}
|
||||
def of_pattern({:%, _meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, {:atom, module}} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_pairs(pairs, stack, context) do
|
||||
map_reduce_ok(pairs, context, fn {key, value}, context ->
|
||||
with {:ok, key_type, context} <- of_pattern(key, stack, context),
|
||||
{:ok, value_type, context} <- of_pattern(value, stack, context),
|
||||
do: {:ok, {key_type, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp pairs_to_unions(pairs, context) do
|
||||
# Maps only allow simple literal keys in patterns so
|
||||
# we do not have to do subtype checking
|
||||
|
||||
Enum.reduce(pairs, [], fn {key, value}, pairs ->
|
||||
case :lists.keyfind(key, 1, pairs) do
|
||||
{^key, {:union, union}} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value | union], context)})
|
||||
|
||||
{^key, original_value} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value, original_value], context)})
|
||||
|
||||
false ->
|
||||
[{key, value} | pairs]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Some guards can be changed to intersection types or higher order types
|
||||
@boolean {:union, [{:atom, true}, {:atom, false}]}
|
||||
@number {:union, [:integer, :float]}
|
||||
|
||||
@guard_functions %{
|
||||
{:is_atom, 1} => {[:atom], @boolean},
|
||||
{:is_binary, 1} => {[:binary], @boolean},
|
||||
{:is_bitstring, 1} => {[:binary], @boolean},
|
||||
{:is_boolean, 1} => {[@boolean], @boolean},
|
||||
{:is_float, 1} => {[:float], @boolean},
|
||||
{:is_function, 1} => {[:fun], @boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], @boolean},
|
||||
{:is_integer, 1} => {[:integer], @boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
|
||||
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
|
||||
{:is_number, 1} => {[@number], @boolean},
|
||||
{:is_pid, 1} => {[:pid], @boolean},
|
||||
{:is_port, 1} => {[:port], @boolean},
|
||||
{:is_reference, 1} => {[:reference], @boolean},
|
||||
{:is_tuple, 1} => {[:tuple], @boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:*, 2} => {[@number, @number], @number},
|
||||
{:+, 1} => {[@number], @number},
|
||||
{:+, 2} => {[@number, @number], @number},
|
||||
{:-, 1} => {[@number], @number},
|
||||
{:-, 2} => {[@number, @number], @number},
|
||||
{:/, 2} => {[@number, @number], @number},
|
||||
{:abs, 1} => {[@number], @number},
|
||||
{:ceil, 1} => {[@number], :integer},
|
||||
{:floor, 1} => {[@number], :integer},
|
||||
{:round, 1} => {[@number], :integer},
|
||||
{:trunc, 1} => {[@number], :integer},
|
||||
{:is_atom, 1} => {[:atom], :boolean},
|
||||
{:is_binary, 1} => {[:binary], :boolean},
|
||||
{:is_bitstring, 1} => {[:binary], :boolean},
|
||||
{:is_boolean, 1} => {[:boolean], :boolean},
|
||||
{:is_float, 1} => {[:float], :boolean},
|
||||
{:is_function, 1} => {[:fun], :boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], :boolean},
|
||||
{:is_integer, 1} => {[:integer], :boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
|
||||
{:is_map, 1} => {[{:map, []}], :boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:is_number, 1} => {[:number], :boolean},
|
||||
{:is_pid, 1} => {[:pid], :boolean},
|
||||
{:is_port, 1} => {[:port], :boolean},
|
||||
{:is_reference, 1} => {[:reference], :boolean},
|
||||
{:is_tuple, 1} => {[:tuple], :boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:*, 2} => {[:number, :number], :number},
|
||||
{:+, 1} => {[:number], :number},
|
||||
{:+, 2} => {[:number, :number], :number},
|
||||
{:-, 1} => {[:number], :number},
|
||||
{:-, 2} => {[:number, :number], :number},
|
||||
{:/, 2} => {[:number, :number], :number},
|
||||
{:abs, 1} => {[:number], :number},
|
||||
{:ceil, 1} => {[:number], :integer},
|
||||
{:floor, 1} => {[:number], :integer},
|
||||
{:round, 1} => {[:number], :integer},
|
||||
{:trunc, 1} => {[:number], :integer},
|
||||
{:element, 2} => {[:integer, :tuple], :dynamic},
|
||||
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:length, 1} => {[{:list, :dynamic}], :integer},
|
||||
{:map_get, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
|
||||
{:map_size, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :integer},
|
||||
{:map_get, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:map_size, 1} => {[{:map, []}], :integer},
|
||||
{:tl, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:tuple_size, 1} => {[:tuple], :integer},
|
||||
{:node, 1} => {[{:union, [:pid, :reference, :port]}], :atom},
|
||||
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
|
||||
{:bit_size, 1} => {[:binary], :integer},
|
||||
{:byte_size, 1} => {[:binary], :integer},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
|
||||
{:div, 2} => {[:integer, :integer], :integer},
|
||||
{:rem, 2} => {[:integer, :integer], :integer},
|
||||
{:node, 0} => {[], :atom},
|
||||
@@ -145,15 +286,13 @@ defmodule Module.Types.Pattern do
|
||||
{:bxor, 2} => {[:integer, :integer], :integer},
|
||||
{:bsl, 2} => {[:integer, :integer], :integer},
|
||||
{:bsr, 2} => {[:integer, :integer], :integer},
|
||||
{:or, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:and, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:xor, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:not, 1} => {[@boolean], @boolean}
|
||||
{:xor, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:not, 1} => {[:boolean], :boolean}
|
||||
|
||||
# Following guards are matched explicitly to handle
|
||||
# type guard functions such as is_atom/1
|
||||
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
|
||||
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
|
||||
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
|
||||
}
|
||||
|
||||
@type_guards [
|
||||
@@ -178,73 +317,49 @@ defmodule Module.Types.Pattern do
|
||||
Refines the type variables in the typing context using type check guards
|
||||
such as `is_integer/1`.
|
||||
"""
|
||||
def of_guard(expr, %{context: stack_context} = stack, context) when stack_context != :pattern do
|
||||
of_guard(expr, %{stack | context: :pattern}, context)
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, context} <- of_guard(left, stack, context),
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
{:ok, right_type, context} <- of_guard(right, keep_guarded(stack), context),
|
||||
do: {:ok, to_union([@boolean, right_type], context), context}
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
left_indexes = collect_var_indexes_from_expr(left, context)
|
||||
right_indexes = collect_var_indexes_from_expr(right, context)
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, context),
|
||||
context =
|
||||
merge_context_or(
|
||||
left_indexes,
|
||||
right_indexes,
|
||||
context,
|
||||
stack,
|
||||
left_context,
|
||||
right_context
|
||||
),
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
do: {:ok, @boolean, context}
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
end
|
||||
|
||||
# The unary operators + and - are special cased to avoid common warnings until
|
||||
# we add support for intersection types for the guard functions
|
||||
# -integer / +integer
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _stack, context)
|
||||
when guard in [:+, :-] and is_integer(integer) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# -float / +float
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _stack, context)
|
||||
when guard in [:+, :-] and is_float(float) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# fun(args)
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{param_types, return_type} = guard_signature(guard, length(args))
|
||||
type_guard? = type_guard?(guard)
|
||||
{consider_type_guards?, keep_guarded?} = stack.type_guards
|
||||
|
||||
# Only check type guards in the context of and/or/not,
|
||||
# a type guard in the context of is_tuple(x) > :foo
|
||||
# should not affect the inference of x
|
||||
if not type_guard? or consider_type_guards? do
|
||||
arg_stack = %{stack | type_guards: {false, keep_guarded?}}
|
||||
if not type_guard? or stack.type_guards_enabled? do
|
||||
arg_stack = %{stack | type_guards_enabled?: type_guard?}
|
||||
|
||||
with {:ok, arg_types, context} <-
|
||||
map_reduce_ok(args, context, &of_guard(&1, arg_stack, &2)),
|
||||
{:ok, context} <- unify_call(arg_types, param_types, stack, context) do
|
||||
{:ok, context} <- unify_call(param_types, arg_types, stack, context) do
|
||||
{arg_types, guard_sources} =
|
||||
case arg_types do
|
||||
[{:var, index} | rest_arg_types] when type_guard? ->
|
||||
guard_sources = Map.put_new(context.guard_sources, index, :guarded)
|
||||
guard_sources =
|
||||
Map.update(context.guard_sources, index, [:guarded], &[:guarded | &1])
|
||||
|
||||
{rest_arg_types, guard_sources}
|
||||
|
||||
_ ->
|
||||
@@ -254,7 +369,7 @@ defmodule Module.Types.Pattern do
|
||||
guard_sources =
|
||||
Enum.reduce(arg_types, guard_sources, fn
|
||||
{:var, index}, guard_sources ->
|
||||
Map.update(guard_sources, index, :fail, &guarded_if_keep_guarded(&1, keep_guarded?))
|
||||
Map.update(guard_sources, index, [:fail], &[:fail | &1])
|
||||
|
||||
_, guard_sources ->
|
||||
guard_sources
|
||||
@@ -267,114 +382,198 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
# map.field
|
||||
def of_guard({{:., meta1, [map, field]}, meta2, []}, stack, context) do
|
||||
of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, stack, context)
|
||||
end
|
||||
|
||||
# var
|
||||
def of_guard(var, _stack, context) when is_var(var) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
type = Map.fetch!(context.vars, var_name(var))
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
def of_guard(expr, stack, context) do
|
||||
of_shared(expr, stack, context, &of_guard/3)
|
||||
# Fall back to of_pattern/3 for literals
|
||||
of_pattern(expr, stack, context)
|
||||
end
|
||||
|
||||
defp collect_var_indexes_from_expr(expr, context) do
|
||||
{_, vars} =
|
||||
Macro.prewalk(expr, %{}, fn
|
||||
var, acc when is_var(var) ->
|
||||
var_name = var_name(var)
|
||||
%{^var_name => type} = context.vars
|
||||
{var, collect_var_indexes(type, context, acc)}
|
||||
|
||||
other, acc ->
|
||||
{other, acc}
|
||||
end)
|
||||
|
||||
Map.keys(vars)
|
||||
defp fresh_context(context) do
|
||||
types = Map.new(context.types, fn {var, _} -> {var, :unbound} end)
|
||||
traces = Map.new(context.traces, fn {var, _} -> {var, []} end)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp unify_call(args, params, stack, context) do
|
||||
reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context ->
|
||||
case unify(arg, param, stack, context) do
|
||||
defp unify_call(params, args, stack, context) do
|
||||
reduce_ok(Enum.zip(params, args), context, fn {param, arg}, context ->
|
||||
case unify(param, arg, stack, context) do
|
||||
{:ok, _, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_context_or(left_indexes, right_indexes, context, stack, left, right) do
|
||||
left_different = filter_different_indexes(left_indexes, left, right)
|
||||
right_different = filter_different_indexes(right_indexes, left, right)
|
||||
|
||||
case {left_different, right_different} do
|
||||
{[index], [index]} -> merge_context_or_equal(index, stack, left, right)
|
||||
{_, _} -> merge_context_or_diff(left_different, context, left)
|
||||
defp merge_context_and(context, stack, left, right) do
|
||||
with {:ok, context} <- unify_new_types(context, stack, left),
|
||||
{:ok, context} <- unify_new_types(context, stack, right) do
|
||||
guard_sources = and_guard_sources(left.guard_sources, right.guard_sources)
|
||||
guard_sources = merge_guard_sources([context.guard_sources, guard_sources])
|
||||
{:ok, %{context | guard_sources: guard_sources}}
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_different_indexes(indexes, left, right) do
|
||||
Enum.filter(indexes, fn index ->
|
||||
%{^index => left_type} = left.types
|
||||
%{^index => right_type} = right.types
|
||||
left_type != right_type
|
||||
end)
|
||||
end
|
||||
defp unify_new_types(context, stack, new_context) do
|
||||
context = merge_traces(context, new_context)
|
||||
|
||||
defp merge_context_or_equal(index, stack, left, right) do
|
||||
%{^index => left_type} = left.types
|
||||
%{^index => right_type} = right.types
|
||||
reduce_ok(Map.to_list(new_context.types), context, fn
|
||||
{_index, :unbound}, context ->
|
||||
{:ok, context}
|
||||
|
||||
cond do
|
||||
left_type == :unbound ->
|
||||
refine_var!(index, right_type, stack, left)
|
||||
{index, new_type}, context ->
|
||||
case unify({:var, index}, new_type, %{stack | trace: false}, context) do
|
||||
{:ok, _, context} ->
|
||||
{:ok, context}
|
||||
|
||||
right_type == :unbound ->
|
||||
left
|
||||
|
||||
true ->
|
||||
# Only include right side if left side is from type guard such as is_list(x),
|
||||
# do not refine in case of length(x)
|
||||
if left.guard_sources[index] == :fail do
|
||||
guard_sources = Map.put(left.guard_sources, index, :fail)
|
||||
left = %{left | guard_sources: guard_sources}
|
||||
refine_var!(index, left_type, stack, left)
|
||||
else
|
||||
guard_sources = merge_guard_sources([left.guard_sources, right.guard_sources])
|
||||
left = %{left | guard_sources: guard_sources}
|
||||
refine_var!(index, to_union([left_type, right_type], left), stack, left)
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If the variable failed, we can keep them from the left side as is.
|
||||
# If they didn't fail, then we need to restore them to their original value.
|
||||
defp merge_context_or_diff(indexes, old_context, new_context) do
|
||||
Enum.reduce(indexes, new_context, fn index, context ->
|
||||
if new_context.guard_sources[index] == :fail do
|
||||
context
|
||||
else
|
||||
restore_var!(index, new_context, old_context)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_guard_sources(sources) do
|
||||
Enum.reduce(sources, fn left, right ->
|
||||
Map.merge(left, right, fn
|
||||
_index, :guarded, :guarded -> :guarded
|
||||
_index, _, _ -> :fail
|
||||
end)
|
||||
Map.merge(left, right, fn _index, left, right -> join_guard_source(left, right) end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp guarded_if_keep_guarded(:guarded, true), do: :guarded
|
||||
defp guarded_if_keep_guarded(_, _), do: :fail
|
||||
defp join_guard_source(left, right) do
|
||||
sources = left ++ right
|
||||
|
||||
defp keep_guarded(%{type_guards: {consider?, _}} = stack),
|
||||
do: %{stack | type_guards: {consider?, true}}
|
||||
cond do
|
||||
:fail in sources -> [:fail]
|
||||
:guarded_fail in sources -> [:guarded_fail]
|
||||
:guarded in sources -> [:guarded]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp and_guard_sources(left, right) do
|
||||
Map.merge(left, right, fn _index, left, right ->
|
||||
# When the failing guard function wont fail due to type check function before it,
|
||||
# for example: is_list(x) and length(x)
|
||||
if :guarded in left and :fail in right do
|
||||
[:guarded_fail]
|
||||
else
|
||||
join_guard_source(left, right)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp merge_traces(context, new_context) do
|
||||
traces =
|
||||
:maps.fold(
|
||||
fn index, new_traces, traces ->
|
||||
:maps.update_with(index, &(new_traces ++ &1), new_traces, traces)
|
||||
end,
|
||||
context.traces,
|
||||
new_context.traces
|
||||
)
|
||||
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp merge_context_or(context, stack, left, right) do
|
||||
context =
|
||||
case {Map.to_list(left.types), Map.to_list(right.types)} do
|
||||
{[{index, :unbound}], [{index, type}]} ->
|
||||
refine_var(index, type, stack, context)
|
||||
|
||||
{[{index, type}], [{index, :unbound}]} ->
|
||||
refine_var(index, type, stack, context)
|
||||
|
||||
{[{index, left_type}], [{index, right_type}]} ->
|
||||
# Only include right side if left side is from type guard such as is_list(x),
|
||||
# do not refine in case of length(x)
|
||||
left_guard_sources = Map.get(left.guard_sources, index, [])
|
||||
|
||||
if :fail in left_guard_sources do
|
||||
guard_sources = Map.put(context.guard_sources, index, [:fail])
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, left_type, stack, context)
|
||||
else
|
||||
guard_sources =
|
||||
merge_guard_sources([
|
||||
context.guard_sources,
|
||||
left.guard_sources,
|
||||
right.guard_sources
|
||||
])
|
||||
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, to_union([left_type, right_type], context), stack, context)
|
||||
end
|
||||
|
||||
{left_types, _right_types} ->
|
||||
Enum.reduce(left_types, context, fn {index, left_type}, context ->
|
||||
left_guard_sources = Map.get(left.guard_sources, index, [])
|
||||
|
||||
if :fail in left_guard_sources do
|
||||
guard_sources =
|
||||
merge_guard_sources([
|
||||
context.guard_sources,
|
||||
left.guard_sources,
|
||||
right.guard_sources
|
||||
])
|
||||
|
||||
context = %{context | guard_sources: guard_sources}
|
||||
refine_var(index, left_type, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
# binary-pattern :: specifier
|
||||
defp of_binary({:"::", _meta, [expr, specifiers]} = full_expr, stack, context) do
|
||||
{expected_type, utf?} = collect_binary_type(specifiers) || {:integer, false}
|
||||
stack = push_expr_stack(full_expr, stack)
|
||||
|
||||
# Special case utf specifiers with binary literals since they allow
|
||||
# both integer and binary literals but variables are always integer
|
||||
if is_binary(expr) and utf? do
|
||||
{:ok, context}
|
||||
else
|
||||
with {:ok, type, context} <- of_pattern(expr, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# binary-pattern
|
||||
defp of_binary(expr, stack, context) do
|
||||
case of_pattern(expr, stack, context) do
|
||||
{:ok, type, context} when type in [:integer, :float, :binary] ->
|
||||
{:ok, context}
|
||||
|
||||
{:ok, type, _context} ->
|
||||
{:error, {:invalid_binary_type, type}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_type({:-, _meta, [left, right]}),
|
||||
do: collect_binary_type(left) || collect_binary_type(right)
|
||||
|
||||
defp collect_binary_type({:integer, _, _}), do: {:integer, false}
|
||||
defp collect_binary_type({:float, _, _}), do: {:float, false}
|
||||
defp collect_binary_type({:bits, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bitstring, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bytes, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:binary, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:utf8, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf16, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf32, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type(_), do: nil
|
||||
|
||||
defp guard_signature(name, arity) do
|
||||
Map.fetch!(@guard_functions, {name, arity})
|
||||
@@ -383,140 +582,4 @@ defmodule Module.Types.Pattern do
|
||||
defp type_guard?(name) do
|
||||
name in @type_guards
|
||||
end
|
||||
|
||||
## Shared
|
||||
|
||||
# :atom
|
||||
defp of_shared(atom, _stack, context, _fun) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
defp of_shared(literal, _stack, context, _fun) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
defp of_shared(literal, _stack, context, _fun) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
defp of_shared(literal, _stack, context, _fun) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
defp of_shared({:<<>>, _meta, args}, stack, context, fun) do
|
||||
expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end
|
||||
|
||||
case Of.binary(args, stack, context, expected_fun) do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
defp of_shared({:|, _meta, [left_expr, []]} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fun.(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
defp of_shared({:|, _meta, [left_expr, right_expr]} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case fun.(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case fun.(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
defp of_shared([], _stack, context, _fun) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
defp of_shared(exprs, stack, context, fun) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
defp of_shared(
|
||||
{{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr,
|
||||
stack,
|
||||
context,
|
||||
fun
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case fun.(left_expr, stack, context) do
|
||||
{:ok, {:list, left}, context} ->
|
||||
case fun.(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
defp of_shared({left, right}, stack, context, fun) do
|
||||
of_shared({:{}, [], [left, right]}, stack, context, fun)
|
||||
end
|
||||
|
||||
# {...}
|
||||
defp of_shared({:{}, _meta, exprs} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %{...}
|
||||
defp of_shared({:%{}, _meta, args} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end
|
||||
Of.open_map(args, stack, context, expected_fun)
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context, fun)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, expected_fun) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,856 +0,0 @@
|
||||
defmodule Module.Types.Unify do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
|
||||
# Those are the simple types known to the system:
|
||||
#
|
||||
# :dynamic
|
||||
# {:var, var}
|
||||
# {:atom, atom} < :atom
|
||||
# :integer
|
||||
# :float
|
||||
# :pid
|
||||
# :port
|
||||
# :reference
|
||||
#
|
||||
# Those are the composite types:
|
||||
#
|
||||
# {:list, type}
|
||||
# {:tuple, size, [type]} < :tuple
|
||||
# {:union, [type]}
|
||||
# {:map, [{:required | :optional, key_type, value_type}]}
|
||||
#
|
||||
# Once new types are added, they should be considered in:
|
||||
#
|
||||
# * unify (all)
|
||||
# * format_type (all)
|
||||
# * subtype? (subtypes only)
|
||||
# * has_unbound_var? (composite only)
|
||||
# * recursive_type? (composite only)
|
||||
# * collect_vars (composite only)
|
||||
# * lift_types (composite only)
|
||||
#
|
||||
|
||||
@doc """
|
||||
Unifies two types and returns the unified type and an updated typing context
|
||||
or an error in case of a typing conflict.
|
||||
"""
|
||||
def unify(source, target, stack, context) do
|
||||
case do_unify(source, target, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, reason} ->
|
||||
if stack.context == :pattern do
|
||||
case do_unify(target, source, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(same, same, _stack, context) do
|
||||
{:ok, same, context}
|
||||
end
|
||||
|
||||
defp do_unify(type, {:var, var}, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = false)
|
||||
end
|
||||
|
||||
defp do_unify({:var, var}, type, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = true)
|
||||
end
|
||||
|
||||
defp do_unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
|
||||
unify(source, target, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:list, source}, {:list, target}, stack, context) do
|
||||
case unify(source, target, stack, context) do
|
||||
{:ok, type, context} -> {:ok, {:list, type}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
|
||||
unify_maps(source_pairs, target_pairs, stack, context)
|
||||
end
|
||||
|
||||
defp do_unify(source, :dynamic, _stack, context) do
|
||||
{:ok, source, context}
|
||||
end
|
||||
|
||||
defp do_unify(:dynamic, target, _stack, context) do
|
||||
{:ok, target, context}
|
||||
end
|
||||
|
||||
defp do_unify({:union, types}, target, stack, context) do
|
||||
unify_result =
|
||||
map_reduce_ok(types, context, fn type, context ->
|
||||
unify(type, target, stack, context)
|
||||
end)
|
||||
|
||||
case unify_result do
|
||||
{:ok, types, context} -> {:ok, to_union(types, context), context}
|
||||
{:error, context} -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(source, target, stack, context) do
|
||||
cond do
|
||||
# TODO: This condition exists to handle unions with unbound vars.
|
||||
match?({:union, _}, target) and has_unbound_var?(target, context) ->
|
||||
{:ok, source, context}
|
||||
|
||||
subtype?(source, target, context) ->
|
||||
{:ok, source, context}
|
||||
|
||||
true ->
|
||||
error(:unable_unify, {source, target, stack}, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_var(var, :dynamic, _stack, context, _var_source?) do
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
defp unify_var(var, type, stack, context, var_source?) do
|
||||
case context.types do
|
||||
%{^var => :unbound} ->
|
||||
context = refine_var!(var, type, stack, context)
|
||||
stack = push_unify_stack(var, stack)
|
||||
|
||||
if recursive_type?(type, [], context) do
|
||||
if var_source? do
|
||||
error(:unable_unify, {{:var, var}, type, stack}, context)
|
||||
else
|
||||
error(:unable_unify, {type, {:var, var}, stack}, context)
|
||||
end
|
||||
else
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
%{^var => {:var, new_var} = var_type} ->
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, {type, reason, %{traces: error_traces} = error_context}} ->
|
||||
old_var_traces = Map.get(context.traces, new_var, [])
|
||||
new_var_traces = Map.get(error_traces, new_var, [])
|
||||
add_var_traces = Enum.drop(new_var_traces, -length(old_var_traces))
|
||||
|
||||
error_traces =
|
||||
error_traces
|
||||
|> Map.update(var, add_var_traces, &(add_var_traces ++ &1))
|
||||
|> Map.put(new_var, old_var_traces)
|
||||
|
||||
{:error, {type, reason, %{error_context | traces: error_traces}}}
|
||||
end
|
||||
|
||||
%{^var => var_type} ->
|
||||
# Only add trace if the variable wasn't already "expanded"
|
||||
context =
|
||||
if variable_expanded?(var, stack, context) do
|
||||
context
|
||||
else
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
stack = push_unify_stack(var, stack)
|
||||
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, {:var, ^var}, context} ->
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:ok, res_type, context} ->
|
||||
context = refine_var!(var, res_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# * All required keys on each side need to match to the other side.
|
||||
# * All optional keys on each side that do not match must be discarded.
|
||||
|
||||
defp unify_maps(source_pairs, target_pairs, stack, context) do
|
||||
{source_required, source_optional} = split_pairs(source_pairs)
|
||||
{target_required, target_optional} = split_pairs(target_pairs)
|
||||
|
||||
with {:ok, source_required_pairs, context} <-
|
||||
unify_source_required(source_required, target_pairs, stack, context),
|
||||
{:ok, target_required_pairs, context} <-
|
||||
unify_target_required(target_required, source_pairs, stack, context),
|
||||
{:ok, source_optional_pairs, context} <-
|
||||
unify_source_optional(source_optional, target_optional, stack, context),
|
||||
{:ok, target_optional_pairs, context} <-
|
||||
unify_target_optional(target_optional, source_optional, stack, context) do
|
||||
# Remove duplicate pairs from matching in both left and right directions
|
||||
pairs =
|
||||
Enum.uniq(
|
||||
source_required_pairs ++
|
||||
target_required_pairs ++
|
||||
source_optional_pairs ++
|
||||
target_optional_pairs
|
||||
)
|
||||
|
||||
{:ok, {:map, pairs}, context}
|
||||
else
|
||||
{:error, :unify} ->
|
||||
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
|
||||
|
||||
{:error, context} ->
|
||||
{:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_source_required(source_required, target_pairs, stack, context) do
|
||||
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
|
||||
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, {:required, key, value}, context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:required, source_key, source_value}]}
|
||||
target_map = {:map, [{target_kind, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
end) || {:error, :unify}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_target_required(target_required, source_pairs, stack, context) do
|
||||
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
|
||||
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, {:required, key, value}, context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{source_kind, source_key, source_value}]}
|
||||
target_map = {:map, [{:required, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
end) || {:error, :unify}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_source_optional(source_optional, target_optional, stack, context) do
|
||||
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
|
||||
Enum.find_value(target_optional, fn {target_key, target_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, [{:optional, key, value}], context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:optional, source_key, source_value}]}
|
||||
target_map = {:map, [{:optional, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end) || {:ok, [], context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_target_optional(target_optional, source_optional, stack, context) do
|
||||
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
|
||||
Enum.find_value(source_optional, fn {source_key, source_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, [{:optional, key, value}], context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:optional, source_key, source_value}]}
|
||||
target_map = {:map, [{:optional, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end) || {:ok, [], context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp split_pairs(pairs) do
|
||||
{required, optional} =
|
||||
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
|
||||
|
||||
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
|
||||
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
|
||||
{required, optional}
|
||||
end
|
||||
|
||||
defp error(type, reason, context), do: {:error, {type, reason, context}}
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | last_expr: expr}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a variable.
|
||||
"""
|
||||
def get_var!(var, context) do
|
||||
Map.fetch!(context.vars, var_name(var))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variable.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
"""
|
||||
def new_var(var, context) do
|
||||
var_name = var_name(var)
|
||||
|
||||
case context.vars do
|
||||
%{^var_name => type} ->
|
||||
{type, context}
|
||||
|
||||
%{} ->
|
||||
type = {:var, context.counter}
|
||||
vars = Map.put(context.vars, var_name, type)
|
||||
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: vars,
|
||||
types_to_vars: types_to_vars,
|
||||
types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an internal variable to the typing context and returns its type variable.
|
||||
An internal variable is used to help unify complex expressions,
|
||||
it does not belong to a specific AST expression.
|
||||
"""
|
||||
def add_var(context) do
|
||||
type = {:var, context.counter}
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolves a variable raising if it is unbound.
|
||||
"""
|
||||
def resolve_var({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> raise "cannot resolve unbound var"
|
||||
%{^var => type} -> resolve_var(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_var(other, _context), do: other
|
||||
|
||||
# Check unify stack to see if variable was already expanded
|
||||
defp variable_expanded?(var, stack, context) do
|
||||
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
|
||||
end
|
||||
|
||||
defp variable_same?(left, right, context) do
|
||||
case context.types do
|
||||
%{^left => {:var, new_left}} ->
|
||||
variable_same?(new_left, right, context)
|
||||
|
||||
%{^right => {:var, new_right}} ->
|
||||
variable_same?(left, new_right, context)
|
||||
|
||||
%{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp push_unify_stack(var, stack) do
|
||||
%{stack | unify_stack: [var | stack.unify_stack]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Restores the variable information from the old context into new context.
|
||||
"""
|
||||
def restore_var!(var, new_context, old_context) do
|
||||
%{^var => type} = old_context.types
|
||||
%{^var => trace} = old_context.traces
|
||||
types = Map.put(new_context.types, var, type)
|
||||
traces = Map.put(new_context.traces, var, trace)
|
||||
%{new_context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the type for a variable and add trace.
|
||||
"""
|
||||
def refine_var!(var, type, stack, context) do
|
||||
types = Map.put(context.types, var, type)
|
||||
context = %{context | types: types}
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove type variable and all its traces.
|
||||
"""
|
||||
def remove_var(var, context) do
|
||||
types = Map.delete(context.types, var)
|
||||
traces = Map.delete(context.traces, var)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
|
||||
line = get_meta(last_expr)[:line]
|
||||
trace = {type, last_expr, {context.file, line}}
|
||||
traces = Map.update!(context.traces, var, &[trace | &1])
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
# Check if a variable is recursive and incompatible with itself
|
||||
# Bad: `{var} = var`
|
||||
# Good: `x = y; y = z; z = x`
|
||||
defp recursive_type?({:var, var} = parent, parents, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} ->
|
||||
false
|
||||
|
||||
%{^var => type} ->
|
||||
if type in parents do
|
||||
not Enum.all?(parents, &match?({:var, _}, &1))
|
||||
else
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recursive_type?({:list, type} = parent, parents, context) do
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
|
||||
defp recursive_type?({:union, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:map, pairs} = parent, parents, context) do
|
||||
Enum.any?(pairs, fn {_kind, key, value} ->
|
||||
recursive_type?(key, [parent | parents], context) or
|
||||
recursive_type?(value, [parent | parents], context)
|
||||
end)
|
||||
end
|
||||
|
||||
defp recursive_type?(_other, _parents, _context) do
|
||||
false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collects all type vars recursively.
|
||||
"""
|
||||
def collect_var_indexes(type, context, acc \\ %{})
|
||||
|
||||
def collect_var_indexes({:var, var}, context, acc) do
|
||||
case acc do
|
||||
%{^var => _} ->
|
||||
acc
|
||||
|
||||
%{} ->
|
||||
case context.types do
|
||||
%{^var => :unbound} -> Map.put(acc, var, true)
|
||||
%{^var => type} -> collect_var_indexes(type, context, Map.put(acc, var, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def collect_var_indexes({:tuple, _, args}, context, acc),
|
||||
do: Enum.reduce(args, acc, &collect_var_indexes(&1, context, &2))
|
||||
|
||||
def collect_var_indexes({:union, args}, context, acc),
|
||||
do: Enum.reduce(args, acc, &collect_var_indexes(&1, context, &2))
|
||||
|
||||
def collect_var_indexes({:list, arg}, context, acc),
|
||||
do: collect_var_indexes(arg, context, acc)
|
||||
|
||||
def collect_var_indexes({:map, pairs}, context, acc) do
|
||||
Enum.reduce(pairs, acc, fn {_, key, value}, acc ->
|
||||
collect_var_indexes(value, context, collect_var_indexes(key, context, acc))
|
||||
end)
|
||||
end
|
||||
|
||||
def collect_var_indexes(_type, _context, acc), do: acc
|
||||
|
||||
@doc """
|
||||
Checks if the type has a type var.
|
||||
"""
|
||||
def has_unbound_var?({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> true
|
||||
%{^var => type} -> has_unbound_var?(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def has_unbound_var?({:tuple, _, args}, context),
|
||||
do: Enum.any?(args, &has_unbound_var?(&1, context))
|
||||
|
||||
def has_unbound_var?({:union, args}, context),
|
||||
do: Enum.any?(args, &has_unbound_var?(&1, context))
|
||||
|
||||
def has_unbound_var?({:list, arg}, context),
|
||||
do: has_unbound_var?(arg, context)
|
||||
|
||||
def has_unbound_var?({:map, pairs}, context) do
|
||||
Enum.any?(pairs, fn {_, key, value} ->
|
||||
has_unbound_var?(key, context) or has_unbound_var?(value, context)
|
||||
end)
|
||||
end
|
||||
|
||||
def has_unbound_var?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Returns true if it is a singleton type.
|
||||
|
||||
Only atoms are singleton types. Unbound vars are not
|
||||
considered singleton types.
|
||||
"""
|
||||
def singleton?({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> singleton?(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def singleton?({:atom, _}, _context), do: true
|
||||
def singleton?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
|
||||
This function assumes that:
|
||||
|
||||
* unbound variables are not subtype of anything
|
||||
|
||||
* dynamic is not considered a subtype of all other types but the top type.
|
||||
This allows this function can be used for ordering, in other cases, you
|
||||
may need to check for both sides
|
||||
|
||||
"""
|
||||
def subtype?(type, type, _context), do: true
|
||||
|
||||
def subtype?({:var, var}, other, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> subtype?(type, other, context)
|
||||
end
|
||||
end
|
||||
|
||||
def subtype?(other, {:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> subtype?(other, type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def subtype?(_, :dynamic, _context), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
|
||||
# Composite
|
||||
|
||||
def subtype?({:tuple, _, _}, :tuple, _context), do: true
|
||||
|
||||
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
|
||||
left_types
|
||||
|> Enum.zip(right_types)
|
||||
|> Enum.all?(fn {left, right} -> subtype?(left, right, context) end)
|
||||
end
|
||||
|
||||
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
|
||||
Enum.all?(left_pairs, fn
|
||||
{:required, left_key, left_value} ->
|
||||
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
|
||||
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
|
||||
end)
|
||||
|
||||
{:optional, _, _} ->
|
||||
true
|
||||
end)
|
||||
end
|
||||
|
||||
def subtype?({:list, left}, {:list, right}, context) do
|
||||
subtype?(left, right, context)
|
||||
end
|
||||
|
||||
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right_union, context))
|
||||
end
|
||||
|
||||
def subtype?(left, {:union, right_types}, context) do
|
||||
Enum.any?(right_types, &subtype?(left, &1, context))
|
||||
end
|
||||
|
||||
def subtype?({:union, left_types}, right, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right, context))
|
||||
end
|
||||
|
||||
def subtype?(_left, _right, _context), do: false
|
||||
|
||||
@doc """
|
||||
Returns a "simplified" union using `subtype?/3` to remove redundant types.
|
||||
|
||||
Due to limitations in `subtype?/3` some overlapping types may still be
|
||||
included. For example unions with overlapping non-concrete types such as
|
||||
`{boolean()} | {atom()}` will not be merged or types with variables that
|
||||
are distinct but equivalent such as `a | b when a ~ b`.
|
||||
"""
|
||||
def to_union([type], _context), do: type
|
||||
|
||||
def to_union(types, context) when types != [] do
|
||||
flat_types = flatten_union(types)
|
||||
|
||||
case unique_super_types(flat_types, context) do
|
||||
[type] -> type
|
||||
types -> {:union, types}
|
||||
end
|
||||
end
|
||||
|
||||
defp flatten_union(types) do
|
||||
Enum.flat_map(types, fn
|
||||
{:union, types} -> flatten_union(types)
|
||||
type -> [type]
|
||||
end)
|
||||
end
|
||||
|
||||
# Filter subtypes
|
||||
#
|
||||
# `boolean() | atom()` => `atom()`
|
||||
# `:foo | atom()` => `atom()`
|
||||
#
|
||||
# Does not merge `true | false` => `boolean()`
|
||||
defp unique_super_types([type | types], context) do
|
||||
types = Enum.reject(types, &subtype?(&1, type, context))
|
||||
|
||||
if Enum.any?(types, &subtype?(type, &1, context)) do
|
||||
unique_super_types(types, context)
|
||||
else
|
||||
[type | unique_super_types(types, context)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unique_super_types([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
## Type lifting
|
||||
|
||||
@doc """
|
||||
Lifts type variables to their inferred types from the context.
|
||||
"""
|
||||
def lift_types(types, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
lifted_types: %{},
|
||||
lifted_counter: 0
|
||||
}
|
||||
|
||||
{types, _context} = Enum.map_reduce(types, context, &lift_type/2)
|
||||
types
|
||||
end
|
||||
|
||||
# Lift type variable to its inferred (hopefully concrete) types from the context
|
||||
defp lift_type({:var, var}, context) do
|
||||
case context.lifted_types do
|
||||
%{^var => lifted_var} ->
|
||||
{{:var, lifted_var}, context}
|
||||
|
||||
%{} ->
|
||||
case context.types do
|
||||
%{^var => :unbound} ->
|
||||
new_lifted_var(var, context)
|
||||
|
||||
%{^var => type} ->
|
||||
if recursive_type?(type, [], context) do
|
||||
new_lifted_var(var, context)
|
||||
else
|
||||
# Remove visited types to avoid infinite loops
|
||||
# then restore after we are done recursing on vars
|
||||
types = context.types
|
||||
context = put_in(context.types[var], :unbound)
|
||||
{type, context} = lift_type(type, context)
|
||||
{type, %{context | types: types}}
|
||||
end
|
||||
|
||||
%{} ->
|
||||
new_lifted_var(var, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp lift_type({:union, types}, context) do
|
||||
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
|
||||
{{:union, types}, context}
|
||||
end
|
||||
|
||||
defp lift_type({:tuple, n, types}, context) do
|
||||
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
|
||||
{{:tuple, n, types}, context}
|
||||
end
|
||||
|
||||
defp lift_type({:map, pairs}, context) do
|
||||
{pairs, context} =
|
||||
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
|
||||
{key, context} = lift_type(key, context)
|
||||
{value, context} = lift_type(value, context)
|
||||
{{kind, key, value}, context}
|
||||
end)
|
||||
|
||||
{{:map, pairs}, context}
|
||||
end
|
||||
|
||||
defp lift_type({:list, type}, context) do
|
||||
{type, context} = lift_type(type, context)
|
||||
{{:list, type}, context}
|
||||
end
|
||||
|
||||
defp lift_type(other, context) do
|
||||
{other, context}
|
||||
end
|
||||
|
||||
defp new_lifted_var(original_var, context) do
|
||||
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
|
||||
counter = context.lifted_counter + 1
|
||||
|
||||
type = {:var, context.lifted_counter}
|
||||
context = %{context | lifted_types: types, lifted_counter: counter}
|
||||
{type, context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats types.
|
||||
|
||||
The second argument says when complex types such as maps and
|
||||
structs should be simplified and not shown.
|
||||
"""
|
||||
def format_type({:map, pairs}, true) do
|
||||
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
|
||||
{:required, {:atom, :__struct__}, {:atom, struct}} ->
|
||||
"%#{inspect(struct)}{}"
|
||||
|
||||
_ ->
|
||||
"map()"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:union, types}, simplify?) do
|
||||
"#{Enum.map_join(types, " | ", &format_type(&1, simplify?))}"
|
||||
end
|
||||
|
||||
def format_type({:tuple, _, types}, simplify?) do
|
||||
"{#{Enum.map_join(types, ", ", &format_type(&1, simplify?))}}"
|
||||
end
|
||||
|
||||
def format_type({:list, type}, simplify?) do
|
||||
"[#{format_type(type, simplify?)}]"
|
||||
end
|
||||
|
||||
def format_type({:map, pairs}, false) do
|
||||
case List.keytake(pairs, {:atom, :__struct__}, 1) do
|
||||
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
|
||||
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
|
||||
|
||||
_ ->
|
||||
"%{#{format_map_pairs(pairs)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:atom, literal}, _simplify?) do
|
||||
inspect(literal)
|
||||
end
|
||||
|
||||
def format_type({:var, index}, _simplify?) do
|
||||
"var#{index + 1}"
|
||||
end
|
||||
|
||||
def format_type(atom, _simplify?) when is_atom(atom) do
|
||||
"#{atom}()"
|
||||
end
|
||||
|
||||
defp format_map_pairs(pairs) do
|
||||
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
|
||||
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
|
||||
|
||||
Enum.map_join(atoms ++ required ++ optional, ", ", fn
|
||||
{:required, {:atom, atom}, right} ->
|
||||
"#{atom}: #{format_type(right, false)}"
|
||||
|
||||
{:required, left, right} ->
|
||||
"#{format_type(left, false)} => #{format_type(right, false)}"
|
||||
|
||||
{:optional, left, right} ->
|
||||
"optional(#{format_type(left, false)}) => #{format_type(right, false)}"
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -106,7 +106,7 @@ defmodule Node do
|
||||
|
||||
For more information, see `:erlang.monitor_node/2`.
|
||||
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
|
||||
"""
|
||||
@spec monitor(t, boolean) :: true
|
||||
def monitor(node, flag) do
|
||||
@@ -119,7 +119,7 @@ defmodule Node do
|
||||
|
||||
For more information, see `:erlang.monitor_node/3`.
|
||||
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
|
||||
"""
|
||||
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
|
||||
def monitor(node, flag, options) do
|
||||
@@ -219,7 +219,7 @@ defmodule Node do
|
||||
|
||||
If `node` does not exist, a useless PID is returned.
|
||||
|
||||
For the list of available options, see `:erlang.spawn/4`.
|
||||
For the list of available options, see `:erlang.spawn/5`.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
|
||||
@@ -428,32 +428,34 @@ defmodule OptionParser do
|
||||
end
|
||||
end
|
||||
|
||||
# Handles -a, -abc, -abc=something, -n2
|
||||
# Handles -a, -abc, -abc=something
|
||||
defp next_with_config(["-" <> option | rest] = argv, config) do
|
||||
%{allow_nonexistent_atoms?: allow_nonexistent_atoms?} = config
|
||||
{option, value} = split_option(option)
|
||||
original = "-" <> option
|
||||
letters = String.graphemes(option)
|
||||
|
||||
cond do
|
||||
is_nil(value) and starts_with_number?(option) ->
|
||||
is_nil(value) and negative_number?(original) ->
|
||||
{:error, argv}
|
||||
|
||||
String.contains?(option, ["-", "_"]) ->
|
||||
{:undefined, original, value, rest}
|
||||
|
||||
String.length(option) == 1 ->
|
||||
tl(letters) == [] ->
|
||||
# We have a regular one-letter alias here
|
||||
tagged = tag_oneletter_alias(option, config)
|
||||
next_tagged(tagged, value, original, rest, config)
|
||||
|
||||
true ->
|
||||
key = get_option_key(option, config.allow_nonexistent_atoms?)
|
||||
key = get_option_key(option, allow_nonexistent_atoms?)
|
||||
option_key = config.aliases[key]
|
||||
|
||||
if key && option_key do
|
||||
IO.warn("multi-letter aliases are deprecated, got: #{inspect(key)}")
|
||||
next_tagged({:default, option_key}, value, original, rest, config)
|
||||
else
|
||||
next_with_config(expand_multiletter_alias(option, value) ++ rest, config)
|
||||
next_with_config(expand_multiletter_alias(letters, value) ++ rest, config)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -705,25 +707,13 @@ defmodule OptionParser do
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_multiletter_alias(options, value) do
|
||||
{options, maybe_integer} =
|
||||
options
|
||||
|> String.to_charlist()
|
||||
|> Enum.split_while(&(&1 not in ?0..?9))
|
||||
|
||||
defp expand_multiletter_alias(letters, value) do
|
||||
{last, expanded} =
|
||||
options
|
||||
|> List.to_string()
|
||||
|> String.graphemes()
|
||||
letters
|
||||
|> Enum.map(&("-" <> &1))
|
||||
|> List.pop_at(-1)
|
||||
|
||||
expanded ++
|
||||
[
|
||||
last <>
|
||||
if(maybe_integer != [], do: "=#{maybe_integer}", else: "") <>
|
||||
if(value, do: "=#{value}", else: "")
|
||||
]
|
||||
expanded ++ [last <> if(value, do: "=" <> value, else: "")]
|
||||
end
|
||||
|
||||
defp normalize_tag(:negated, option, value, switches) do
|
||||
@@ -764,7 +754,7 @@ defmodule OptionParser do
|
||||
|
||||
defp value_in_tail?(["-" | _]), do: true
|
||||
defp value_in_tail?(["- " <> _ | _]), do: true
|
||||
defp value_in_tail?(["-" <> arg | _]), do: starts_with_number?(arg)
|
||||
defp value_in_tail?(["-" <> arg | _]), do: negative_number?("-" <> arg)
|
||||
defp value_in_tail?([]), do: false
|
||||
defp value_in_tail?(_), do: true
|
||||
|
||||
@@ -796,8 +786,9 @@ defmodule OptionParser do
|
||||
end
|
||||
end
|
||||
|
||||
defp starts_with_number?(<<char, _::binary>>) when char in ?0..?9, do: true
|
||||
defp starts_with_number?(_), do: false
|
||||
defp negative_number?(arg) do
|
||||
match?({_, ""}, Float.parse(arg))
|
||||
end
|
||||
|
||||
defp format_errors([_ | _] = errors, opts) do
|
||||
types = opts[:switches] || opts[:strict]
|
||||
|
||||
+37
-29
@@ -66,7 +66,7 @@ defmodule Path do
|
||||
|
||||
case type(path) do
|
||||
:relative ->
|
||||
absname_join([relative_to, path])
|
||||
absname_join(relative_to, path)
|
||||
|
||||
:absolute ->
|
||||
absname_join([path])
|
||||
@@ -80,11 +80,11 @@ defmodule Path do
|
||||
# Absolute path on current drive
|
||||
defp absname_vr(["/" | rest], [volume | _], _relative), do: absname_join([volume | rest])
|
||||
|
||||
# Relative to current directory on current drive
|
||||
# Relative to current directory on current drive.
|
||||
defp absname_vr([<<x, ?:>> | rest], [<<x, _::binary>> | _], relative),
|
||||
do: absname(absname_join(rest), relative)
|
||||
|
||||
# Relative to current directory on another drive
|
||||
# Relative to current directory on another drive.
|
||||
defp absname_vr([<<x, ?:>> | name], _, _relative) do
|
||||
cwd =
|
||||
case :file.get_cwd([x, ?:]) do
|
||||
@@ -97,25 +97,25 @@ defmodule Path do
|
||||
|
||||
@slash [?/, ?\\]
|
||||
|
||||
defp absname_join([]), do: ""
|
||||
defp absname_join(list), do: absname_join(list, major_os_type())
|
||||
# Joins a list
|
||||
defp absname_join([name1, name2 | rest]), do: absname_join([absname_join(name1, name2) | rest])
|
||||
|
||||
defp absname_join([name1, name2 | rest], os_type) do
|
||||
joined = do_absname_join(IO.chardata_to_string(name1), relative(name2), [], os_type)
|
||||
absname_join([joined | rest], os_type)
|
||||
end
|
||||
defp absname_join([name]),
|
||||
do: do_absname_join(IO.chardata_to_string(name), <<>>, [], major_os_type())
|
||||
|
||||
defp absname_join([name], os_type) do
|
||||
do_absname_join(IO.chardata_to_string(name), <<>>, [], os_type)
|
||||
end
|
||||
# Joins two paths
|
||||
defp absname_join(left, right),
|
||||
do: do_absname_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
|
||||
|
||||
defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32)
|
||||
when uc_letter in ?A..?Z,
|
||||
do: do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)
|
||||
when uc_letter in ?A..?Z do
|
||||
do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)
|
||||
end
|
||||
|
||||
defp do_absname_join(<<c1, c2, rest::binary>>, relativename, [], :win32)
|
||||
when c1 in @slash and c2 in @slash,
|
||||
do: do_absname_join(rest, relativename, '//', :win32)
|
||||
when c1 in @slash and c2 in @slash do
|
||||
do_absname_join(rest, relativename, '//', :win32)
|
||||
end
|
||||
|
||||
defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32),
|
||||
do: do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)
|
||||
@@ -152,8 +152,8 @@ defmodule Path do
|
||||
|
||||
## Examples
|
||||
|
||||
Path.expand("/foo/bar/../baz")
|
||||
#=> "/foo/baz"
|
||||
Path.expand("/foo/bar/../bar")
|
||||
#=> "/foo/bar"
|
||||
|
||||
"""
|
||||
@spec expand(t) :: binary
|
||||
@@ -313,9 +313,6 @@ defmodule Path do
|
||||
iex> Path.relative_to("/usr/local/foo", "/etc")
|
||||
"/usr/local/foo"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/usr/local/foo")
|
||||
"."
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t) :: binary
|
||||
def relative_to(path, from) do
|
||||
@@ -323,10 +320,6 @@ defmodule Path do
|
||||
relative_to(split(path), split(from), path)
|
||||
end
|
||||
|
||||
defp relative_to(path, path, _original) do
|
||||
"."
|
||||
end
|
||||
|
||||
defp relative_to([h | t1], [h | t2], original) do
|
||||
relative_to(t1, t2, original)
|
||||
end
|
||||
@@ -572,6 +565,9 @@ defmodule Path do
|
||||
"""
|
||||
@spec split(t) :: [binary]
|
||||
|
||||
# Work around a bug in Erlang on Unix-like operating systems
|
||||
def split(""), do: []
|
||||
|
||||
def split(path) do
|
||||
:filename.split(IO.chardata_to_string(path))
|
||||
end
|
||||
@@ -580,15 +576,27 @@ defmodule Path do
|
||||
@moduledoc false
|
||||
|
||||
def read_link_info(file) do
|
||||
:file.read_link_info(file)
|
||||
call({:read_link_info, file})
|
||||
end
|
||||
|
||||
def list_dir(dir) do
|
||||
case :file.list_dir(dir) do
|
||||
{:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
|
||||
other -> other
|
||||
case call({:list_dir, dir}) do
|
||||
{:ok, files} ->
|
||||
{:ok, for(file <- files, hd(file) != ?., do: file)}
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, call: 1}
|
||||
|
||||
defp call(tuple) do
|
||||
x = :erlang.dt_spread_tag(true)
|
||||
y = :gen_server.call(:file_server_2, tuple)
|
||||
:erlang.dt_restore_tag(x)
|
||||
y
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -29,7 +29,7 @@ defmodule Port do
|
||||
|
||||
After sending those two messages, we invoked the IEx helper `flush()`,
|
||||
which printed all messages received from the port, in this case we got
|
||||
"hello" and "world" back. Note that the messages are in binary because we
|
||||
"hello" and "world" back. Notice the messages are in binary because we
|
||||
passed the `:binary` option when opening the port in `Port.open/2`. Without
|
||||
such option, it would have yielded a list of bytes.
|
||||
|
||||
@@ -180,7 +180,7 @@ defmodule Port do
|
||||
@type name ::
|
||||
{:spawn, charlist | binary}
|
||||
| {:spawn_driver, charlist | binary}
|
||||
| {:spawn_executable, :file.name_all()}
|
||||
| {:spawn_executable, charlist | atom}
|
||||
| {:fd, non_neg_integer, non_neg_integer}
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -448,7 +448,7 @@ defmodule Process do
|
||||
If the process is already dead when calling `Process.monitor/1`, a
|
||||
`:DOWN` message is delivered immediately.
|
||||
|
||||
See ["The need for monitoring"](https://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring)
|
||||
See [the need for monitoring](https://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring)
|
||||
for an example. See `:erlang.monitor/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -705,8 +705,7 @@ defmodule Process do
|
||||
"""
|
||||
@spec flag(:error_handler, module) :: module
|
||||
@spec flag(:max_heap_size, heap_size) :: heap_size
|
||||
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
|
||||
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
|
||||
@spec flag(:message_queue_data, :erlang.message_queue_data()) :: :erlang.message_queue_data()
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
|
||||
+55
-64
@@ -4,11 +4,11 @@ defmodule Protocol do
|
||||
|
||||
A protocol specifies an API that should be defined by its
|
||||
implementations. A protocol is defined with `Kernel.defprotocol/2`
|
||||
and its implementations with `Kernel.defimpl/3`.
|
||||
and its implementations with `Kernel.defimpl/2`.
|
||||
|
||||
## A real case
|
||||
## Examples
|
||||
|
||||
In Elixir, we have two nouns for checking how many items there
|
||||
In Elixir, we have two verbs for checking how many items there
|
||||
are in a data structure: `length` and `size`. `length` means the
|
||||
information must be computed. For example, `length(list)` needs to
|
||||
traverse the whole list to calculate its length. On the other hand,
|
||||
@@ -43,17 +43,13 @@ defmodule Protocol do
|
||||
def size(tuple), do: tuple_size(tuple)
|
||||
end
|
||||
|
||||
Note that we didn't implement it for lists as we don't have the
|
||||
Notice we didn't implement it for lists as we don't have the
|
||||
`size` information on lists, rather its value needs to be
|
||||
computed with `length`.
|
||||
|
||||
The data structure you are implementing the protocol for
|
||||
must be the first argument to all functions defined in the
|
||||
protocol.
|
||||
|
||||
It is possible to implement protocols for all Elixir types:
|
||||
|
||||
* Structs (see the "Protocols and Structs" section below)
|
||||
* Structs (see below)
|
||||
* `Tuple`
|
||||
* `Atom`
|
||||
* `List`
|
||||
@@ -65,7 +61,7 @@ defmodule Protocol do
|
||||
* `Map`
|
||||
* `Port`
|
||||
* `Reference`
|
||||
* `Any` (see the "Fallback to `Any`" section below)
|
||||
* `Any` (see below)
|
||||
|
||||
## Protocols and Structs
|
||||
|
||||
@@ -79,7 +75,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
When implementing a protocol for a struct, the `:for` option can
|
||||
be omitted if the `defimpl/3` call is inside the module that defines
|
||||
be omitted if the `defimpl` call is inside the module that defines
|
||||
the struct:
|
||||
|
||||
defmodule User do
|
||||
@@ -134,13 +130,13 @@ defmodule Protocol do
|
||||
def reverse(term), do: Enum.reverse(term)
|
||||
end
|
||||
|
||||
Inside `defimpl/3`, you can use `@protocol` to access the protocol
|
||||
Inside `defimpl/2`, you can use `@protocol` to access the protocol
|
||||
being implemented and `@for` to access the module it is being
|
||||
defined for.
|
||||
|
||||
## Types
|
||||
|
||||
Defining a protocol automatically defines a zero-arity type named `t`, which
|
||||
Defining a protocol automatically defines a type named `t`, which
|
||||
can be used as follows:
|
||||
|
||||
@spec print_size(Size.t()) :: :ok
|
||||
@@ -177,7 +173,7 @@ defmodule Protocol do
|
||||
* `impl_for/1` - returns the module that implements the protocol for the given argument,
|
||||
`nil` otherwise
|
||||
|
||||
* `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
|
||||
* `impl_for!/1` - same as above but raises an error if an implementation is
|
||||
not found
|
||||
|
||||
For example, for the `Enumerable` protocol we have:
|
||||
@@ -191,17 +187,15 @@ defmodule Protocol do
|
||||
iex> Enumerable.impl_for(42)
|
||||
nil
|
||||
|
||||
In addition, every protocol implementation module contains the `__impl__/1`
|
||||
function. The function takes one of the following atoms:
|
||||
In addition, every protocol implementation module contains the `__impl__/1` function. The
|
||||
function takes one of the following atoms:
|
||||
|
||||
* `:for` - returns the module responsible for the data structure of the
|
||||
protocol implementation
|
||||
* `:for` - returns the module responsible for the data structure of the protocol implementation
|
||||
|
||||
* `:protocol` - returns the protocol module for which this implementation
|
||||
is provided
|
||||
* `:protocol` - returns the protocol module for which this implementation is provided
|
||||
|
||||
For example, the module implementing the `Enumerable` protocol for lists is
|
||||
`Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:
|
||||
For example, the module implementing the `Enumerable` protocol for lists is `Enumerable.List`.
|
||||
Therefore, we can invoke `__impl__/1` on this module:
|
||||
|
||||
iex(1)> Enumerable.List.__impl__(:for)
|
||||
List
|
||||
@@ -211,17 +205,22 @@ defmodule Protocol do
|
||||
|
||||
## Consolidation
|
||||
|
||||
In order to speed up protocol dispatching, whenever all protocol implementations
|
||||
are known up-front, typically after all Elixir code in a project is compiled,
|
||||
Elixir provides a feature called *protocol consolidation*. Consolidation directly
|
||||
links protocols to their implementations in a way that invoking a function from a
|
||||
In order to cope with code loading in development, protocols in
|
||||
Elixir provide a slow implementation of protocol dispatching specific
|
||||
to development.
|
||||
|
||||
In order to speed up dispatching in production environments, where
|
||||
all implementations are known up-front, Elixir provides a feature
|
||||
called *protocol consolidation*. Consolidation directly links protocols
|
||||
to their implementations in a way that invoking a function from a
|
||||
consolidated protocol is equivalent to invoking two remote functions.
|
||||
|
||||
Protocol consolidation is applied by default to all Mix projects during compilation.
|
||||
This may be an issue during test. For instance, if you want to implement a protocol
|
||||
during test, the implementation will have no effect, as the protocol has already been
|
||||
consolidated. One possible solution is to include compilation directories that are
|
||||
specific to your test environment in your mix.exs:
|
||||
Protocol consolidation is applied by default to all Mix projects during
|
||||
compilation. This may be an issue during test. For instance, if you want
|
||||
to implement a protocol during test, the implementation will have no
|
||||
effect, as the protocol has already been consolidated. One possible
|
||||
solution is to include compilation directories that are specific to your
|
||||
test environment in your mix.exs:
|
||||
|
||||
def project do
|
||||
...
|
||||
@@ -308,11 +307,9 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp assert_protocol!(module, extra) do
|
||||
try do
|
||||
Code.ensure_compiled!(module)
|
||||
rescue
|
||||
e in ArgumentError ->
|
||||
raise ArgumentError, e.message <> extra
|
||||
case Code.ensure_compiled(module) do
|
||||
{:module, ^module} -> :ok
|
||||
_ -> raise ArgumentError, "#{inspect(module)} is not available" <> extra
|
||||
end
|
||||
|
||||
try do
|
||||
@@ -339,11 +336,9 @@ defmodule Protocol do
|
||||
defp assert_impl!(protocol, base, extra) do
|
||||
impl = Module.concat(protocol, base)
|
||||
|
||||
try do
|
||||
Code.ensure_compiled!(impl)
|
||||
rescue
|
||||
e in ArgumentError ->
|
||||
raise ArgumentError, e.message <> extra
|
||||
case Code.ensure_compiled(impl) do
|
||||
{:module, ^impl} -> :ok
|
||||
_ -> raise ArgumentError, "#{inspect(impl)} is not available" <> extra
|
||||
end
|
||||
|
||||
try do
|
||||
@@ -398,19 +393,17 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
Derivable.ok(%ImplStruct{})
|
||||
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
|
||||
{:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
Explicit derivations can now be called via `__deriving__/3`:
|
||||
Explicit derivations can now be called via `__deriving__`:
|
||||
|
||||
# Explicitly derived via `__deriving__/3`
|
||||
# Explicitly derived via `__deriving__`
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
# Explicitly derived by API via `__deriving__/3`
|
||||
# Explicitly derived by API via `__deriving__`
|
||||
require Protocol
|
||||
Protocol.derive(Derivable, ImplStruct, :oops)
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
|
||||
|
||||
"""
|
||||
defmacro derive(protocol, module, options \\ []) do
|
||||
@@ -442,7 +435,7 @@ defmodule Protocol do
|
||||
@spec extract_protocols([charlist | String.t()]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute(paths, 'Elixir.', fn module, attributes ->
|
||||
case attributes[:__protocol__] do
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: _] -> module
|
||||
_ -> nil
|
||||
end
|
||||
@@ -473,7 +466,7 @@ defmodule Protocol do
|
||||
prefix = Atom.to_charlist(protocol) ++ '.'
|
||||
|
||||
extract_matching_by_attribute(paths, prefix, fn _mod, attributes ->
|
||||
case attributes[:__impl__] do
|
||||
case attributes[:protocol_impl] do
|
||||
[protocol: ^protocol, for: for] -> for
|
||||
_ -> nil
|
||||
end
|
||||
@@ -564,7 +557,7 @@ defmodule Protocol do
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:__protocol__] do
|
||||
case attributes[:protocol] do
|
||||
[fallback_to_any: any] ->
|
||||
{:ok, {any, definitions}, specs, {info, chunks}}
|
||||
|
||||
@@ -684,16 +677,14 @@ defmodule Protocol do
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel,
|
||||
except: [
|
||||
def: 1,
|
||||
def: 2,
|
||||
defp: 1,
|
||||
defp: 2,
|
||||
defguard: 1,
|
||||
defguardp: 1,
|
||||
defmacrop: 1,
|
||||
defmacrop: 2,
|
||||
defmacro: 1,
|
||||
defmacro: 2,
|
||||
defmacrop: 1,
|
||||
defmacrop: 2
|
||||
defp: 1,
|
||||
defp: 2,
|
||||
def: 1,
|
||||
def: 2
|
||||
]
|
||||
|
||||
# Import the new dsl that holds the new def
|
||||
@@ -800,8 +791,8 @@ defmodule Protocol do
|
||||
|
||||
# Store information as an attribute so it
|
||||
# can be read without loading the module.
|
||||
Module.register_attribute(__MODULE__, :__protocol__, persist: true)
|
||||
@__protocol__ [fallback_to_any: !!@fallback_to_any]
|
||||
Module.register_attribute(__MODULE__, :protocol, persist: true)
|
||||
@protocol [fallback_to_any: !!@fallback_to_any]
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@@ -860,8 +851,8 @@ defmodule Protocol do
|
||||
|
||||
unquote(block)
|
||||
|
||||
Module.register_attribute(__MODULE__, :__impl__, persist: true)
|
||||
@__impl__ [protocol: @protocol, for: @for]
|
||||
Module.register_attribute(__MODULE__, :protocol_impl, persist: true)
|
||||
@protocol_impl [protocol: @protocol, for: @for]
|
||||
|
||||
unquote(impl)
|
||||
end
|
||||
@@ -902,8 +893,8 @@ defmodule Protocol do
|
||||
else
|
||||
quoted =
|
||||
quote do
|
||||
Module.register_attribute(__MODULE__, :__impl__, persist: true)
|
||||
@__impl__ [protocol: unquote(protocol), for: unquote(for)]
|
||||
Module.register_attribute(__MODULE__, :protocol_impl, persist: true)
|
||||
@protocol_impl [protocol: unquote(protocol), for: unquote(for)]
|
||||
|
||||
@doc false
|
||||
@spec __impl__(:target) :: unquote(impl)
|
||||
|
||||
+60
-225
@@ -1,75 +1,23 @@
|
||||
defmodule Range do
|
||||
@moduledoc """
|
||||
Ranges represent a sequence of zero, one or many, ascending
|
||||
or descending integers with a common difference called step.
|
||||
Ranges represent a sequence of one or many, ascending
|
||||
or descending, consecutive integers.
|
||||
|
||||
Ranges are always inclusive and they may have custom steps.
|
||||
The most common form of creating and matching on ranges is
|
||||
via the [`first..last`](`../2`) and [`first..last//step`](`..///3`)
|
||||
notations, auto-imported from `Kernel`:
|
||||
Ranges can be either increasing (`first <= last`) or
|
||||
decreasing (`first > last`). Ranges are also always
|
||||
inclusive.
|
||||
|
||||
iex> Enum.to_list(1..3)
|
||||
[1, 2, 3]
|
||||
iex> Enum.to_list(1..3//2)
|
||||
[1, 3]
|
||||
iex> Enum.to_list(3..1//-1)
|
||||
[3, 2, 1]
|
||||
A range is represented internally as a struct. However,
|
||||
the most common form of creating and matching on ranges
|
||||
is via the `../2` macro, auto-imported from `Kernel`:
|
||||
|
||||
Ranges may also have a single element:
|
||||
|
||||
iex> Enum.to_list(1..1)
|
||||
[1]
|
||||
iex> Enum.to_list(1..1//2)
|
||||
[1]
|
||||
|
||||
Or even no elements at all:
|
||||
|
||||
iex> Enum.to_list(10..0//1)
|
||||
[]
|
||||
iex> Enum.to_list(0..10//-1)
|
||||
[]
|
||||
|
||||
When defining a range without a step, the step will be
|
||||
defined based on the first and last position of the
|
||||
range, If `first >= last`, it will be an increasing range
|
||||
with a step of 1. Otherwise, it is a decreasing range.
|
||||
Note however implicitly decreasing ranges are deprecated.
|
||||
Therefore, if you need a decreasing range from `3` to `1`,
|
||||
prefer to write `3..1//-1` instead.
|
||||
|
||||
## Definition
|
||||
|
||||
An increasing range `first..last//step` is a range from
|
||||
`first` to `last` increasing by `step` where all values
|
||||
`v` must be `first <= v and v <= last`. Therefore, a range
|
||||
`10..0//1` is an empty range because there is no value `v`
|
||||
that is `10 <= v and v <= 0`.
|
||||
|
||||
Similarly, a decreasing range `first..last//-step` is a range
|
||||
from `first` to `last` decreasing by `step` where all values
|
||||
`v` must be `first >= v and v >= last`. Therefore, a range
|
||||
`0..10//-1` is an empty range because there is no value `v`
|
||||
that is `0 >= v and v >= 10`.
|
||||
|
||||
## Representation
|
||||
|
||||
Internally, ranges are represented as structs:
|
||||
|
||||
iex> range = 1..9//2
|
||||
1..9//2
|
||||
iex> first..last//step = range
|
||||
iex> range = 1..3
|
||||
1..3
|
||||
iex> first..last = range
|
||||
iex> first
|
||||
1
|
||||
iex> last
|
||||
9
|
||||
iex> step
|
||||
2
|
||||
iex> range.step
|
||||
2
|
||||
|
||||
You can access the range fields (`first`, `last`, and `step`)
|
||||
directly but you should not modify nor create ranges by hand.
|
||||
Instead use the proper operators or `new/2` and `new/3`.
|
||||
3
|
||||
|
||||
A range implements the `Enumerable` protocol, which means
|
||||
functions in the `Enum` module can be used to work with
|
||||
@@ -92,37 +40,23 @@ defmodule Range do
|
||||
not materialize the whole list of integers.
|
||||
"""
|
||||
|
||||
@enforce_keys [:first, :last, :step]
|
||||
defstruct first: nil, last: nil, step: nil
|
||||
defstruct first: nil, last: nil
|
||||
|
||||
@type limit :: integer
|
||||
@type step :: pos_integer | neg_integer
|
||||
@type t :: %__MODULE__{first: limit, last: limit, step: step}
|
||||
@type t(first, last) :: %__MODULE__{first: first, last: last, step: step}
|
||||
@type t :: %__MODULE__{first: integer, last: integer}
|
||||
@type t(first, last) :: %__MODULE__{first: first, last: last}
|
||||
|
||||
@doc """
|
||||
Creates a new range.
|
||||
|
||||
If `first` is less than `last`, the range will be increasing from
|
||||
`first` to `last`. If `first` is equal to `last`, the range will contain
|
||||
one element, which is the number itself.
|
||||
|
||||
If `first` is greater than `last`, the range will be decreasing from `first`
|
||||
to `last`, albeit this behaviour is deprecated. Therefore, it is advised to
|
||||
explicitly list the step with `new/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.new(-100, 100)
|
||||
-100..100
|
||||
|
||||
"""
|
||||
|
||||
@spec new(limit, limit) :: t
|
||||
@spec new(integer, integer) :: t
|
||||
def new(first, last) when is_integer(first) and is_integer(last) do
|
||||
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.17
|
||||
step = if first <= last, do: 1, else: -1
|
||||
%Range{first: first, last: last, step: step}
|
||||
%Range{first: first, last: last}
|
||||
end
|
||||
|
||||
def new(first, last) do
|
||||
@@ -131,60 +65,6 @@ defmodule Range do
|
||||
"got: #{inspect(first)}..#{inspect(last)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new range with `step`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.new(-100, 100, 2)
|
||||
-100..100//2
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec new(limit, limit, step) :: t
|
||||
def new(first, last, step)
|
||||
when is_integer(first) and is_integer(last) and is_integer(step) and step != 0 do
|
||||
%Range{first: first, last: last, step: step}
|
||||
end
|
||||
|
||||
def new(first, last, step) do
|
||||
raise ArgumentError,
|
||||
"ranges (first..last//step) expect both sides to be integers and the step to be a " <>
|
||||
"non-zero integer, got: #{inspect(first)}..#{inspect(last)}//#{inspect(step)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the size of `range`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.size(1..10)
|
||||
10
|
||||
iex> Range.size(1..10//2)
|
||||
5
|
||||
iex> Range.size(1..10//3)
|
||||
4
|
||||
iex> Range.size(1..10//-1)
|
||||
0
|
||||
|
||||
iex> Range.size(10..1)
|
||||
10
|
||||
iex> Range.size(10..1//-1)
|
||||
10
|
||||
iex> Range.size(10..1//-2)
|
||||
5
|
||||
iex> Range.size(10..1//-3)
|
||||
4
|
||||
iex> Range.size(10..1//1)
|
||||
0
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
def size(range)
|
||||
def size(first..last//step) when step > 0 and first > last, do: 0
|
||||
def size(first..last//step) when step < 0 and first < last, do: 0
|
||||
def size(first..last//step), do: abs(div(last - first, step)) + 1
|
||||
|
||||
@doc """
|
||||
Checks if two ranges are disjoint.
|
||||
|
||||
@@ -199,135 +79,90 @@ defmodule Range do
|
||||
iex> Range.disjoint?(1..5, 2..7)
|
||||
false
|
||||
|
||||
Steps are also considered when computing the ranges to be disjoint:
|
||||
|
||||
iex> Range.disjoint?(1..10//2, 2..10//2)
|
||||
true
|
||||
|
||||
# First element in common in all below is 29
|
||||
iex> Range.disjoint?(2..100//3, 9..100//5)
|
||||
false
|
||||
iex> Range.disjoint?(101..2//-3, 99..9//-5)
|
||||
false
|
||||
iex> Range.disjoint?(1..100//14, 8..100//21)
|
||||
false
|
||||
iex> Range.disjoint?(57..-1//-14, 8..100//21)
|
||||
false
|
||||
iex> Range.disjoint?(1..100//14, 51..8//-21)
|
||||
false
|
||||
|
||||
# If 29 is out of range
|
||||
iex> Range.disjoint?(1..28//14, 8..28//21)
|
||||
true
|
||||
iex> Range.disjoint?(2..28//3, 9..28//5)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec disjoint?(t, t) :: boolean
|
||||
def disjoint?(first1..last1//step1 = range1, first2..last2//step2 = range2) do
|
||||
if size(range1) == 0 or size(range2) == 0 do
|
||||
true
|
||||
else
|
||||
{first1, last1, step1} = normalize(first1, last1, step1)
|
||||
{first2, last2, step2} = normalize(first2, last2, step2)
|
||||
|
||||
cond do
|
||||
last2 < first1 or last1 < first2 ->
|
||||
true
|
||||
|
||||
abs(step1) == 1 and abs(step2) == 1 ->
|
||||
false
|
||||
|
||||
true ->
|
||||
# We need to find the first intersection of two arithmetic
|
||||
# progressions and see if they belong within the ranges
|
||||
# https://math.stackexchange.com/questions/1656120/formula-to-find-the-first-intersection-of-two-arithmetic-progressions
|
||||
{gcd, u, v} = Integer.extended_gcd(-step1, step2)
|
||||
c = first1 - first2 + step2 - step1
|
||||
t1 = -c / step1 * u
|
||||
t2 = -c / step2 * v
|
||||
t = max(floor(t1) + 1, floor(t2) + 1)
|
||||
x = div(c * u + t * step2, gcd) - 1
|
||||
y = div(c * v + t * step1, gcd) - 1
|
||||
|
||||
x < 0 or first1 + x * step1 > last1 or
|
||||
y < 0 or first2 + y * step2 > last2
|
||||
end
|
||||
end
|
||||
def disjoint?(first1..last1 = _range1, first2..last2 = _range2) do
|
||||
{first1, last1} = normalize(first1, last1)
|
||||
{first2, last2} = normalize(first2, last2)
|
||||
last2 < first1 or last1 < first2
|
||||
end
|
||||
|
||||
@compile inline: [normalize: 3]
|
||||
defp normalize(first, last, step) when first > last, do: {last, first, -step}
|
||||
defp normalize(first, last, step), do: {first, last, step}
|
||||
@compile inline: [normalize: 2]
|
||||
defp normalize(first, last) when first > last, do: {last, first}
|
||||
defp normalize(first, last), do: {first, last}
|
||||
|
||||
@doc false
|
||||
@deprecated "Pattern match on first..last//step instead"
|
||||
@deprecated "Pattern match on first..last instead"
|
||||
def range?(term)
|
||||
def range?(first..last) when is_integer(first) and is_integer(last), do: true
|
||||
def range?(_), do: false
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: Range do
|
||||
def reduce(first..last//step, acc, fun) do
|
||||
reduce(first, last, acc, fun, step)
|
||||
def reduce(first..last, acc, fun) do
|
||||
reduce(first, last, acc, fun, _up? = last >= first)
|
||||
end
|
||||
|
||||
defp reduce(_first, _last, {:halt, acc}, _fun, _step) do
|
||||
defp reduce(_first, _last, {:halt, acc}, _fun, _up?) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp reduce(first, last, {:suspend, acc}, fun, step) do
|
||||
{:suspended, acc, &reduce(first, last, &1, fun, step)}
|
||||
defp reduce(first, last, {:suspend, acc}, fun, up?) do
|
||||
{:suspended, acc, &reduce(first, last, &1, fun, up?)}
|
||||
end
|
||||
|
||||
defp reduce(first, last, {:cont, acc}, fun, step)
|
||||
when step > 0 and first <= last
|
||||
when step < 0 and first >= last do
|
||||
reduce(first + step, last, fun.(first, acc), fun, step)
|
||||
defp reduce(first, last, {:cont, acc}, fun, _up? = true) when first <= last do
|
||||
reduce(first + 1, last, fun.(first, acc), fun, _up? = true)
|
||||
end
|
||||
|
||||
defp reduce(first, last, {:cont, acc}, fun, _up? = false) when first >= last do
|
||||
reduce(first - 1, last, fun.(first, acc), fun, _up? = false)
|
||||
end
|
||||
|
||||
defp reduce(_, _, {:cont, acc}, _fun, _up) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
def member?(first..last//step = range, value) when is_integer(value) do
|
||||
cond do
|
||||
Range.size(range) == 0 ->
|
||||
{:ok, false}
|
||||
|
||||
first <= last ->
|
||||
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
||||
|
||||
true ->
|
||||
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
||||
def member?(first..last, value) when is_integer(value) do
|
||||
if first <= last do
|
||||
{:ok, first <= value and value <= last}
|
||||
else
|
||||
{:ok, last <= value and value <= first}
|
||||
end
|
||||
end
|
||||
|
||||
def member?(_, _value) do
|
||||
def member?(_.._, _value) do
|
||||
{:ok, false}
|
||||
end
|
||||
|
||||
def count(range) do
|
||||
{:ok, Range.size(range)}
|
||||
def count(first..last) do
|
||||
if first <= last do
|
||||
{:ok, last - first + 1}
|
||||
else
|
||||
{:ok, first - last + 1}
|
||||
end
|
||||
end
|
||||
|
||||
def slice(first.._//step = range) do
|
||||
{:ok, Range.size(range), &slice(first + &1 * step, step, &2)}
|
||||
def slice(first..last) do
|
||||
if first <= last do
|
||||
{:ok, last - first + 1, &slice_asc(first + &1, &2)}
|
||||
else
|
||||
{:ok, first - last + 1, &slice_desc(first - &1, &2)}
|
||||
end
|
||||
end
|
||||
|
||||
defp slice(current, _step, 1), do: [current]
|
||||
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
|
||||
defp slice_asc(current, 1), do: [current]
|
||||
defp slice_asc(current, remaining), do: [current | slice_asc(current + 1, remaining - 1)]
|
||||
|
||||
defp slice_desc(current, 1), do: [current]
|
||||
defp slice_desc(current, remaining), do: [current | slice_desc(current - 1, remaining - 1)]
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Range do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(first..last//1, opts) do
|
||||
def inspect(first..last, opts) 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
|
||||
end
|
||||
|
||||
+27
-32
@@ -401,7 +401,6 @@ defmodule Record do
|
||||
# Using {} here is safe, since it's not valid AST
|
||||
default = if Macro.Env.in_match?(caller), do: {:_, [], nil}, else: {}
|
||||
{default, keyword} = Keyword.pop(keyword, :_, default)
|
||||
{keyword, exprs} = hoist_expressions(keyword, caller)
|
||||
|
||||
{elements, remaining} =
|
||||
Enum.map_reduce(fields, keyword, fn {key, field_default}, remaining ->
|
||||
@@ -414,7 +413,6 @@ defmodule Record do
|
||||
case remaining do
|
||||
[] ->
|
||||
quote(do: {unquote(tag), unquote_splicing(elements)})
|
||||
|> maybe_prepend_reversed_exprs(exprs)
|
||||
|
||||
[{key, _} | _] ->
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
@@ -427,45 +425,27 @@ defmodule Record do
|
||||
raise ArgumentError, "cannot invoke update style macro inside match"
|
||||
end
|
||||
|
||||
{keyword, exprs} = hoist_expressions(keyword, caller)
|
||||
|
||||
if Keyword.has_key?(keyword, :_) do
|
||||
message = "updating a record with a default (:_) is equivalent to creating a new record"
|
||||
IO.warn(message, Macro.Env.stacktrace(caller))
|
||||
create(tag, fields, keyword, caller)
|
||||
else
|
||||
updates =
|
||||
Enum.map(keyword, fn {key, value} ->
|
||||
if index = find_index(fields, key, 2) do
|
||||
{index, value}
|
||||
else
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
end
|
||||
end)
|
||||
case build_updates(keyword, fields, [], [], []) do
|
||||
{updates, [], []} ->
|
||||
build_update(updates, var)
|
||||
|
||||
build_update(updates, var) |> maybe_prepend_reversed_exprs(exprs)
|
||||
{updates, vars, exprs} ->
|
||||
quote do
|
||||
{unquote_splicing(:lists.reverse(vars))} = {unquote_splicing(:lists.reverse(exprs))}
|
||||
unquote(build_update(updates, var))
|
||||
end
|
||||
|
||||
{:error, key} ->
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp hoist_expressions(keyword, %{context: nil, module: module}) do
|
||||
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
|
||||
if simple_argument?(expr) do
|
||||
{{key, expr}, acc}
|
||||
else
|
||||
var = Macro.unique_var(key, module)
|
||||
{{key, var}, [{:=, [], [var, expr]} | acc]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp hoist_expressions(keyword, _), do: {keyword, []}
|
||||
|
||||
defp maybe_prepend_reversed_exprs(expr, []),
|
||||
do: expr
|
||||
|
||||
defp maybe_prepend_reversed_exprs(expr, exprs),
|
||||
do: {:__block__, [], :lists.reverse([expr | exprs])}
|
||||
|
||||
defp build_update(updates, initial) do
|
||||
updates
|
||||
|> Enum.sort(fn {left, _}, {right, _} -> right <= left end)
|
||||
@@ -474,6 +454,21 @@ defmodule Record do
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_updates([{key, value} | rest], fields, updates, vars, exprs) do
|
||||
if index = find_index(fields, key, 2) do
|
||||
if simple_argument?(value) do
|
||||
build_updates(rest, fields, [{index, value} | updates], vars, exprs)
|
||||
else
|
||||
var = Macro.var(key, __MODULE__)
|
||||
build_updates(rest, fields, [{index, var} | updates], [var | vars], [value | exprs])
|
||||
end
|
||||
else
|
||||
{:error, key}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_updates([], _fields, updates, vars, exprs), do: {updates, vars, exprs}
|
||||
|
||||
defp simple_argument?({name, _, ctx}) when is_atom(name) and is_atom(ctx), do: true
|
||||
defp simple_argument?(other), do: Macro.quoted_literal?(other)
|
||||
|
||||
|
||||
+40
-44
@@ -4,7 +4,7 @@ defmodule Regex do
|
||||
|
||||
Regex is based on PCRE (Perl Compatible Regular Expressions) and
|
||||
built on top of Erlang's `:re` module. More information can be found
|
||||
in the [`:re` module documentation](`:re`).
|
||||
in the [`:re` module documentation](http://www.erlang.org/doc/man/re.html).
|
||||
|
||||
Regular expressions in Elixir can be created using the sigils
|
||||
`~r` (see `Kernel.sigil_r/2`) or `~R` (see `Kernel.sigil_R/2`):
|
||||
@@ -16,14 +16,13 @@ defmodule Regex do
|
||||
~r/foo/iu
|
||||
|
||||
Regular expressions created via sigils are pre-compiled and stored
|
||||
in the `.beam` file. Note that this may be a problem if you are precompiling
|
||||
in the `.beam` file. Notice this may be a problem if you are precompiling
|
||||
Elixir, see the "Precompilation" section for more information.
|
||||
|
||||
A Regex is represented internally as the `Regex` struct. Therefore,
|
||||
`%Regex{}` can be used whenever there is a need to match on them.
|
||||
Keep in mind that all of the structs fields are private. There is
|
||||
also not guarantee two regular expressions from the same source are
|
||||
equal, for example:
|
||||
Keep in mind it is not guaranteed two regular expressions from the
|
||||
same source are equal, for example:
|
||||
|
||||
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
|
||||
|
||||
@@ -38,10 +37,9 @@ defmodule Regex do
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
|
||||
* `unicode` (u) - enables Unicode specific patterns like `\p` and causes
|
||||
character classes like `\w`, `\W`, `\s`, etc. to also match on Unicode
|
||||
(see examples below in "Character classes"). It expects valid Unicode
|
||||
strings to be given on match
|
||||
* `unicode` (u) - enables Unicode specific patterns like `\p` and change
|
||||
modifiers like `\w`, `\W`, `\s` and friends to also match on Unicode.
|
||||
It expects valid Unicode strings to be given on match
|
||||
|
||||
* `caseless` (i) - adds case insensitivity
|
||||
|
||||
@@ -107,6 +105,7 @@ defmodule Regex do
|
||||
|
||||
* alnum - Letters and digits
|
||||
* alpha - Letters
|
||||
* ascii - Character codes 0-127
|
||||
* blank - Space or tab only
|
||||
* cntrl - Control characters
|
||||
* digit - Decimal digits (same as \\d)
|
||||
@@ -119,11 +118,6 @@ defmodule Regex do
|
||||
* word - "Word" characters (same as \w)
|
||||
* xdigit - Hexadecimal digits
|
||||
|
||||
There is another character class, `ascii`, that erroneously matches
|
||||
Latin-1 characters instead of the 0-127 range specified by POSIX. This
|
||||
cannot be fixed without altering the behaviour of other classes, so we
|
||||
recommend matching the range with `[\\0-\x7f]` instead.
|
||||
|
||||
Note the behaviour of those classes may change according to the Unicode
|
||||
and other modifiers:
|
||||
|
||||
@@ -131,10 +125,6 @@ defmodule Regex do
|
||||
false
|
||||
iex> String.match?("josé", ~r/^[[:lower:]]+$/u)
|
||||
true
|
||||
iex> Regex.replace(~r/\s/, "Unicode\u00A0spaces", "-")
|
||||
"Unicode spaces"
|
||||
iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-")
|
||||
"Unicode-spaces"
|
||||
|
||||
## Precompilation
|
||||
|
||||
@@ -228,7 +218,7 @@ defmodule Regex do
|
||||
and recompiles the regex in case of version mismatch.
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec recompile(t) :: {:ok, t} | {:error, any}
|
||||
@spec recompile(t) :: t
|
||||
def recompile(%Regex{} = regex) do
|
||||
version = version()
|
||||
|
||||
@@ -283,9 +273,17 @@ defmodule Regex do
|
||||
@doc """
|
||||
Returns `true` if the given `term` is a regex.
|
||||
Otherwise returns `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.regex?(~r/foo/)
|
||||
true
|
||||
|
||||
iex> Regex.regex?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
# TODO: deprecate permanently on Elixir v1.15
|
||||
@doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead"
|
||||
@spec regex?(any) :: boolean
|
||||
def regex?(term)
|
||||
def regex?(%Regex{}), do: true
|
||||
def regex?(_), do: false
|
||||
@@ -300,8 +298,6 @@ defmodule Regex do
|
||||
Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
|
||||
Defaults to zero.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -321,9 +317,8 @@ defmodule Regex do
|
||||
def run(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
offset = Keyword.get(options, :offset, 0)
|
||||
|
||||
case safe_run(regex, string, [{:capture, captures, return}, {:offset, offset}]) do
|
||||
case safe_run(regex, string, [{:capture, captures, return}]) do
|
||||
:nomatch -> nil
|
||||
:match -> []
|
||||
{:match, results} -> results
|
||||
@@ -432,8 +427,6 @@ defmodule Regex do
|
||||
Defaults to `:binary`.
|
||||
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
|
||||
Defaults to zero.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -459,8 +452,7 @@ defmodule Regex do
|
||||
def scan(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
return = Keyword.get(options, :return, :binary)
|
||||
captures = Keyword.get(options, :capture, :all)
|
||||
offset = Keyword.get(options, :offset, 0)
|
||||
options = [{:capture, captures, return}, :global, {:offset, offset}]
|
||||
options = [{:capture, captures, return}, :global]
|
||||
|
||||
case safe_run(regex, string, options) do
|
||||
:match -> []
|
||||
@@ -653,8 +645,20 @@ defmodule Regex do
|
||||
|
||||
"""
|
||||
@spec replace(t, String.t(), String.t() | (... -> String.t()), [term]) :: String.t()
|
||||
def replace(%Regex{} = regex, string, replacement, options \\ [])
|
||||
when is_binary(string) and is_list(options) do
|
||||
def replace(regex, string, replacement, options \\ [])
|
||||
|
||||
def replace(regex, string, replacement, options)
|
||||
when is_binary(string) and is_binary(replacement) and is_list(options) do
|
||||
do_replace(regex, string, precompile_replacement(replacement), options)
|
||||
end
|
||||
|
||||
def replace(regex, string, replacement, options)
|
||||
when is_binary(string) and is_function(replacement) and is_list(options) do
|
||||
{:arity, arity} = Function.info(replacement, :arity)
|
||||
do_replace(regex, string, {replacement, arity}, options)
|
||||
end
|
||||
|
||||
defp do_replace(%Regex{} = regex, string, replacement, options) do
|
||||
opts = if Keyword.get(options, :global) != false, do: [:global], else: []
|
||||
opts = [{:capture, :all, :index} | opts]
|
||||
|
||||
@@ -663,20 +667,13 @@ defmodule Regex do
|
||||
string
|
||||
|
||||
{:match, [mlist | t]} when is_list(mlist) ->
|
||||
apply_list(string, precompile_replacement(replacement), [mlist | t])
|
||||
|> IO.iodata_to_binary()
|
||||
apply_list(string, replacement, [mlist | t]) |> IO.iodata_to_binary()
|
||||
|
||||
{:match, slist} ->
|
||||
apply_list(string, precompile_replacement(replacement), [slist])
|
||||
|> IO.iodata_to_binary()
|
||||
apply_list(string, replacement, [slist]) |> IO.iodata_to_binary()
|
||||
end
|
||||
end
|
||||
|
||||
defp precompile_replacement(replacement) when is_function(replacement) do
|
||||
{:arity, arity} = Function.info(replacement, :arity)
|
||||
{replacement, arity}
|
||||
end
|
||||
|
||||
defp precompile_replacement(""), do: []
|
||||
|
||||
defp precompile_replacement(<<?\\, ?g, ?{, rest::binary>>) when byte_size(rest) > 0 do
|
||||
@@ -762,12 +759,12 @@ defmodule Regex do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_index(_string, {pos, _length}) when pos < 0 do
|
||||
defp get_index(_string, {pos, _len}) when pos < 0 do
|
||||
""
|
||||
end
|
||||
|
||||
defp get_index(string, {pos, length}) do
|
||||
<<_::size(pos)-binary, res::size(length)-binary, _::binary>> = string
|
||||
defp get_index(string, {pos, len}) do
|
||||
<<_::size(pos)-binary, res::size(len)-binary, _::binary>> = string
|
||||
res
|
||||
end
|
||||
|
||||
@@ -828,7 +825,6 @@ defmodule Regex do
|
||||
|
||||
@doc false
|
||||
# Unescape map function used by Macro.unescape_string.
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
|
||||
+23
-117
@@ -3,7 +3,7 @@ defmodule Registry do
|
||||
A local, decentralized and scalable key-value process storage.
|
||||
|
||||
It allows developers to lookup one or more processes with a given key.
|
||||
If the registry has `:unique` keys, a key points to 0 or 1 process.
|
||||
If the registry has `:unique` keys, a key points to 0 or 1 processes.
|
||||
If the registry allows `:duplicate` keys, a single key may point to any
|
||||
number of processes. In both cases, different keys could identify the
|
||||
same process.
|
||||
@@ -261,8 +261,9 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unregister_name({registry, key}), do: unregister(registry, key)
|
||||
def unregister_name({registry, key, _value}), do: unregister(registry, key)
|
||||
def unregister_name({registry, key}) do
|
||||
unregister(registry, key)
|
||||
end
|
||||
|
||||
## Registry API
|
||||
|
||||
@@ -354,20 +355,13 @@ defmodule Registry do
|
||||
"expected :listeners to be a list of named processes, got: #{inspect(listeners)}"
|
||||
end
|
||||
|
||||
compressed = Keyword.get(options, :compressed, false)
|
||||
|
||||
unless is_boolean(compressed) do
|
||||
raise ArgumentError,
|
||||
"expected :compressed to be a boolean, got: #{inspect(compressed)}"
|
||||
end
|
||||
|
||||
# The @info format must be kept in sync with Registry.Partition optimization.
|
||||
entries = [
|
||||
{@all_info, {keys, partitions, nil, nil, listeners}},
|
||||
{@key_info, {keys, partitions, nil}} | meta
|
||||
]
|
||||
|
||||
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries, compressed)
|
||||
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -731,64 +725,6 @@ defmodule Registry do
|
||||
acc
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the values for the given `key` for `pid` in `registry`.
|
||||
|
||||
For unique registries, it is either an empty list or a list
|
||||
with a single element. For duplicate registries, it is a list
|
||||
with zero, one, or multiple elements.
|
||||
|
||||
## Examples
|
||||
|
||||
In the example below we register the current process and look it up
|
||||
both from itself and other processes:
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest)
|
||||
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
|
||||
[]
|
||||
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
|
||||
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
|
||||
[:world]
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", self()) end) |> Task.await()
|
||||
[]
|
||||
iex> parent = self()
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", parent) end) |> Task.await()
|
||||
[:world]
|
||||
|
||||
The same applies to duplicate registries:
|
||||
|
||||
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest)
|
||||
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
|
||||
[]
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
|
||||
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
|
||||
[:world]
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
|
||||
iex> Enum.sort(Registry.values(Registry.DuplicateLookupTest, "hello", self()))
|
||||
[:another, :world]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec values(registry, key, pid) :: [value]
|
||||
def values(registry, key, pid) when is_atom(registry) do
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
|
||||
case safe_lookup_second(key_ets, key) do
|
||||
{^pid, value} ->
|
||||
[value]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
|
||||
{:duplicate, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, pid, partitions)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unregisters all entries for the given `key` associated to the current
|
||||
process in `registry`.
|
||||
@@ -848,8 +784,7 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unregisters entries for keys matching a pattern associated to the current
|
||||
process in `registry`.
|
||||
Unregister entries for a given key matching a pattern.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -987,7 +922,7 @@ defmodule Registry do
|
||||
key_ets = key_ets || key_ets!(registry, key_partition)
|
||||
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
|
||||
|
||||
# Note that we write first to the pid_ets table because it will
|
||||
# Notice we write first to the pid_ets table because it will
|
||||
# always be able to do the cleanup. If we register first to the
|
||||
# key one and the process crashes, the key will stay there forever.
|
||||
Process.link(pid_server)
|
||||
@@ -1023,7 +958,7 @@ defmodule Registry do
|
||||
if :ets.insert_new(key_ets, entry) do
|
||||
:ok
|
||||
else
|
||||
# Note that we have to call register_key recursively
|
||||
# Notice we have to call register_key recursively
|
||||
# because we are always at odds of a race condition.
|
||||
case :ets.lookup(key_ets, key) do
|
||||
[{^key, {pid, _}} = current] ->
|
||||
@@ -1098,34 +1033,6 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes registry metadata for the given `key` in `registry`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.DeleteMetaTest)
|
||||
iex> Registry.put_meta(Registry.DeleteMetaTest, :custom_key, "custom_value")
|
||||
:ok
|
||||
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
|
||||
{:ok, "custom_value"}
|
||||
iex> Registry.delete_meta(Registry.DeleteMetaTest, :custom_key)
|
||||
:ok
|
||||
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec delete_meta(registry, meta_key) :: :ok
|
||||
def delete_meta(registry, key) when is_atom(registry) and (is_atom(key) or is_tuple(key)) do
|
||||
try do
|
||||
:ets.delete(registry, key)
|
||||
:ok
|
||||
catch
|
||||
:error, :badarg ->
|
||||
raise ArgumentError, "unknown registry: #{inspect(registry)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of registered keys in a registry.
|
||||
It runs in constant time.
|
||||
@@ -1404,12 +1311,12 @@ defmodule Registry.Supervisor do
|
||||
@moduledoc false
|
||||
use Supervisor
|
||||
|
||||
def start_link(kind, registry, partitions, listeners, entries, compressed) do
|
||||
arg = {kind, registry, partitions, listeners, entries, compressed}
|
||||
def start_link(kind, registry, partitions, listeners, entries) do
|
||||
arg = {kind, registry, partitions, listeners, entries}
|
||||
Supervisor.start_link(__MODULE__, arg, name: registry)
|
||||
end
|
||||
|
||||
def init({kind, registry, partitions, listeners, entries, compressed}) do
|
||||
def init({kind, registry, partitions, listeners, entries}) do
|
||||
^registry = :ets.new(registry, [:set, :public, :named_table, read_concurrency: true])
|
||||
true = :ets.insert(registry, entries)
|
||||
|
||||
@@ -1417,7 +1324,7 @@ defmodule Registry.Supervisor do
|
||||
for i <- 0..(partitions - 1) do
|
||||
key_partition = Registry.Partition.key_name(registry, i)
|
||||
pid_partition = Registry.Partition.pid_name(registry, i)
|
||||
arg = {kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}
|
||||
arg = {kind, registry, i, partitions, key_partition, pid_partition, listeners}
|
||||
|
||||
%{
|
||||
id: pid_partition,
|
||||
@@ -1477,9 +1384,9 @@ defmodule Registry.Partition do
|
||||
|
||||
## Callbacks
|
||||
|
||||
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
|
||||
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners}) do
|
||||
Process.flag(:trap_exit, true)
|
||||
key_ets = init_key_ets(kind, key_partition, compressed)
|
||||
key_ets = init_key_ets(kind, key_partition)
|
||||
pid_ets = init_pid_ets(kind, pid_partition)
|
||||
|
||||
# If we have only one partition, we do an optimization which
|
||||
@@ -1500,18 +1407,17 @@ defmodule Registry.Partition do
|
||||
|
||||
# The key partition is a set for unique keys,
|
||||
# duplicate bag for duplicate ones.
|
||||
defp init_key_ets(:unique, key_partition, compressed) do
|
||||
opts = [:set, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
defp init_key_ets(:unique, key_partition) do
|
||||
:ets.new(key_partition, [:set, :public, read_concurrency: true, write_concurrency: true])
|
||||
end
|
||||
|
||||
defp init_key_ets(:duplicate, key_partition, compressed) do
|
||||
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp compression_opt(opts, compressed) do
|
||||
if compressed, do: [:compressed] ++ opts, else: opts
|
||||
defp init_key_ets(:duplicate, key_partition) do
|
||||
:ets.new(key_partition, [
|
||||
:duplicate_bag,
|
||||
:public,
|
||||
read_concurrency: true,
|
||||
write_concurrency: true
|
||||
])
|
||||
end
|
||||
|
||||
# A process can always have multiple keys, so the
|
||||
|
||||
+44
-154
@@ -21,7 +21,7 @@ defmodule Stream do
|
||||
iex> Enum.map(stream, &(&1 + 1))
|
||||
[3, 5, 7]
|
||||
|
||||
Note that we started with a range and then we created a stream that is
|
||||
Notice we started with a range and then we created a stream that is
|
||||
meant to multiply each element in the range by 2. At this point, no
|
||||
computation was done. Only when `Enum.map/2` is called we actually
|
||||
enumerate over each element in the range, multiplying it by 2 and adding 1.
|
||||
@@ -46,7 +46,7 @@ defmodule Stream do
|
||||
6
|
||||
#=> [2, 4, 6]
|
||||
|
||||
Note that we first printed each element in the list, then multiplied each
|
||||
Notice that we first printed each element in the list, then multiplied each
|
||||
element by 2 and finally printed each new value. In this example, the list
|
||||
was enumerated three times. Let's see an example with streams:
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Stream do
|
||||
its double. In this example, the list was enumerated just once!
|
||||
|
||||
That's what we meant when we said earlier that streams are composable,
|
||||
lazy enumerables. Note that we could call `Stream.map/2` multiple times,
|
||||
lazy enumerables. Notice we could call `Stream.map/2` multiple times,
|
||||
effectively composing the streams and keeping them lazy. The computations
|
||||
are only performed when you call a function from the `Enum` module.
|
||||
|
||||
@@ -1105,9 +1105,9 @@ defmodule Stream do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips two enumerables together, lazily.
|
||||
Zips two collections together, lazily.
|
||||
|
||||
The zipping finishes as soon as either enumerable completes.
|
||||
The zipping finishes as soon as any enumerable completes.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1118,9 +1118,7 @@ defmodule Stream do
|
||||
|
||||
"""
|
||||
@spec zip(Enumerable.t(), Enumerable.t()) :: Enumerable.t()
|
||||
def zip(enumerable1, enumerable2) do
|
||||
zip_with(enumerable1, enumerable2, fn left, right -> {left, right} end)
|
||||
end
|
||||
def zip(left, right), do: zip([left, right])
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from a finite collection of enumerables
|
||||
@@ -1139,175 +1137,69 @@ defmodule Stream do
|
||||
@doc since: "1.4.0"
|
||||
@spec zip(enumerables) :: Enumerable.t() when enumerables: [Enumerable.t()] | Enumerable.t()
|
||||
def zip(enumerables) do
|
||||
zip_with(enumerables, &List.to_tuple(&1))
|
||||
&prepare_zip(enumerables, &1, &2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lazily zips corresponding elements from two enumerables into a new one, transforming them with
|
||||
the `zip_fun` function as it goes.
|
||||
|
||||
The `zip_fun` will be called with the first element from `enumerable1` and the first
|
||||
element from `enumerable2`, then with the second element from each, and so on until
|
||||
either one of the enumerables completes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> concat = Stream.concat(1..3, 4..6)
|
||||
iex> Stream.zip_with(concat, concat, fn a, b -> a + b end) |> Enum.to_list()
|
||||
[2, 4, 6, 8, 10, 12]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(Enumerable.t(), Enumerable.t(), (term, term -> term)) :: Enumerable.t()
|
||||
def zip_with(enumerable1, enumerable2, zip_fun)
|
||||
when is_list(enumerable1) and is_list(enumerable2) and is_function(zip_fun, 2) do
|
||||
&zip_pair(enumerable1, enumerable2, &1, &2, zip_fun)
|
||||
end
|
||||
|
||||
def zip_with(enumerable1, enumerable2, zip_fun) when is_function(zip_fun, 2) do
|
||||
zip_with([enumerable1, enumerable2], fn [left, right] -> zip_fun.(left, right) end)
|
||||
end
|
||||
|
||||
defp zip_pair(_list1, _list2, {:halt, acc}, _fun, _zip_fun) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp zip_pair(list1, list2, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &zip_pair(list1, list2, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp zip_pair([], _list2, {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
|
||||
defp zip_pair(_list1, [], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
|
||||
|
||||
defp zip_pair([head1 | tail1], [head2 | tail2], {:cont, acc}, fun, zip_fun) do
|
||||
zip_pair(tail1, tail2, fun.(zip_fun.(head1, head2), acc), fun, zip_fun)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Lazily zips corresponding elements from a finite collection of enumerables into a new
|
||||
enumerable, transforming them with the `zip_fun` function as it goes.
|
||||
|
||||
The first element from each of the enums in `enumerables` will be put into a list which is then passed to
|
||||
the 1-arity `zip_fun` function. Then, the second elements from each of the enums are put into a list and passed to
|
||||
`zip_fun`, and so on until any one of the enums in `enumerables` completes.
|
||||
|
||||
Returns a new enumerable with the results of calling `zip_fun`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> concat = Stream.concat(1..3, 4..6)
|
||||
iex> Stream.zip_with([concat, concat], fn [a, b] -> a + b end) |> Enum.to_list()
|
||||
[2, 4, 6, 8, 10, 12]
|
||||
|
||||
iex> concat = Stream.concat(1..3, 4..6)
|
||||
iex> Stream.zip_with([concat, concat, 1..3], fn [a, b, c] -> a + b + c end) |> Enum.to_list()
|
||||
[3, 6, 9]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(enumerables, (Enumerable.t() -> term)) :: Enumerable.t()
|
||||
when enumerables: [Enumerable.t()] | Enumerable.t()
|
||||
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
|
||||
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
|
||||
&zip_list(enumerables, &1, &2, zip_fun)
|
||||
else
|
||||
&zip_enum(enumerables, &1, &2, zip_fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
|
||||
case zip_list_heads_tails(enumerables, [], []) do
|
||||
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
|
||||
:error -> {:done, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
|
||||
zip_list_heads_tails(rest, [head | heads], [tail | tails])
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([], heads, tails) do
|
||||
{:lists.reverse(heads), :lists.reverse(tails)}
|
||||
end
|
||||
|
||||
defp zip_enum(enumerables, acc, fun, zip_fun) do
|
||||
step = fn x, acc ->
|
||||
{:suspend, :lists.reverse([x | acc])}
|
||||
end
|
||||
defp prepare_zip(enumerables, acc, fun) do
|
||||
step = &do_zip_step(&1, &2)
|
||||
|
||||
enum_funs =
|
||||
Enum.map(enumerables, fn enum ->
|
||||
{&Enumerable.reduce(enum, &1, step), [], :cont}
|
||||
end)
|
||||
|
||||
do_zip_enum(enum_funs, acc, fun, zip_fun)
|
||||
do_zip(enum_funs, acc, fun)
|
||||
end
|
||||
|
||||
# This implementation of do_zip_enum/4 works for any number of streams to zip
|
||||
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
|
||||
# This implementation of do_zip/3 works for any number of
|
||||
# streams to zip, even if right now zip/2 only zips two streams.
|
||||
|
||||
defp do_zip(zips, {:halt, acc}, _fun) do
|
||||
do_zip_close(zips)
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
|
||||
defp do_zip(zips, {:suspend, acc}, fun) do
|
||||
{:suspended, acc, &do_zip(zips, &1, fun)}
|
||||
end
|
||||
|
||||
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
|
||||
defp do_zip([], {:cont, acc}, _callback) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
|
||||
defp do_zip(zips, {:cont, acc}, callback) do
|
||||
try do
|
||||
do_zip_next(zips, acc, callback, [], [], zip_fun)
|
||||
do_zip_next_tuple(zips, acc, callback, [], [])
|
||||
catch
|
||||
kind, reason ->
|
||||
do_zip_close(zips)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:next, buffer, acc} ->
|
||||
do_zip_enum(buffer, acc, callback, zip_fun)
|
||||
do_zip(buffer, acc, callback)
|
||||
|
||||
{:done, _acc} = other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
# do_zip_next/6 computes the next tuple formed by
|
||||
# do_zip_next_tuple/5 computes the next tuple formed by
|
||||
# the next element of each zipped stream.
|
||||
defp do_zip_next(
|
||||
[{_, [], :halt} | zips],
|
||||
acc,
|
||||
_callback,
|
||||
_yielded_elems,
|
||||
buffer,
|
||||
_zip_fun
|
||||
) do
|
||||
|
||||
defp do_zip_next_tuple([{_, [], :halt} | zips], acc, _callback, _yielded_elems, buffer) do
|
||||
do_zip_close(:lists.reverse(buffer, zips))
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
defp do_zip_next_tuple([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer) do
|
||||
case fun.({:cont, []}) do
|
||||
{:suspended, [elem | next_acc], fun} ->
|
||||
next_buffer = [{fun, next_acc, :cont} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer)
|
||||
|
||||
{_, [elem | next_acc]} ->
|
||||
next_buffer = [{fun, next_acc, :halt} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer)
|
||||
|
||||
{_, []} ->
|
||||
# The current zipped stream terminated, so we close all the streams
|
||||
@@ -1317,30 +1209,28 @@ defmodule Stream do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_next(
|
||||
[{fun, zip_acc, zip_op} | zips],
|
||||
acc,
|
||||
callback,
|
||||
yielded_elems,
|
||||
buffer,
|
||||
zip_fun
|
||||
) do
|
||||
defp do_zip_next_tuple([{fun, zip_acc, zip_op} | zips], acc, callback, yielded_elems, buffer) do
|
||||
[elem | rest] = zip_acc
|
||||
next_buffer = [{fun, rest, zip_op} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], next_buffer)
|
||||
end
|
||||
|
||||
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
defp do_zip_next_tuple([] = _zips, acc, callback, yielded_elems, buffer) do
|
||||
# "yielded_elems" is a reversed list of results for the current iteration of
|
||||
# zipping. That is to say, the nth element from each of the enums being zipped.
|
||||
# It needs to be reversed and passed to the zipping function so it can do it's thing.
|
||||
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
|
||||
# zipping: it needs to be reversed and converted to a tuple to have the next
|
||||
# tuple in the list resulting from zipping.
|
||||
zipped = List.to_tuple(:lists.reverse(yielded_elems))
|
||||
{:next, :lists.reverse(buffer), callback.(zipped, acc)}
|
||||
end
|
||||
|
||||
defp do_zip_close(zips) do
|
||||
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
|
||||
end
|
||||
|
||||
defp do_zip_step(x, acc) do
|
||||
{:suspend, :lists.reverse([x | acc])}
|
||||
end
|
||||
|
||||
## Sources
|
||||
|
||||
@doc """
|
||||
@@ -1358,7 +1248,7 @@ defmodule Stream do
|
||||
def cycle(enumerable)
|
||||
|
||||
def cycle([]) do
|
||||
raise ArgumentError, "cannot cycle over an empty enumerable"
|
||||
raise ArgumentError, "cannot cycle over empty enumerable"
|
||||
end
|
||||
|
||||
def cycle(enumerable) when is_list(enumerable) do
|
||||
@@ -1407,7 +1297,7 @@ defmodule Stream do
|
||||
fn acc ->
|
||||
case reduce.(acc) do
|
||||
{state, []} when state in [:done, :halted] ->
|
||||
raise ArgumentError, "cannot cycle over an empty enumerable"
|
||||
raise ArgumentError, "cannot cycle over empty enumerable"
|
||||
|
||||
other ->
|
||||
other
|
||||
@@ -1443,9 +1333,9 @@ defmodule Stream do
|
||||
## Examples
|
||||
|
||||
# Although not necessary, let's seed the random algorithm
|
||||
iex> :rand.seed(:exsss, {1, 2, 3})
|
||||
iex> :rand.seed(:exrop, {1, 2, 3})
|
||||
iex> Stream.repeatedly(&:rand.uniform/0) |> Enum.take(3)
|
||||
[0.5455598952593053, 0.6039309974353404, 0.6684893034823949]
|
||||
[0.7498295129076106, 0.06161655489244533, 0.7924073127680873]
|
||||
|
||||
"""
|
||||
@spec repeatedly((() -> element)) :: Enumerable.t()
|
||||
@@ -1694,7 +1584,7 @@ defmodule Stream do
|
||||
end
|
||||
|
||||
defimpl Enumerable, for: Stream do
|
||||
@compile :inline_list_funcs
|
||||
@compile :inline_list_funs
|
||||
|
||||
def count(_lazy), do: {:error, __MODULE__}
|
||||
|
||||
@@ -1731,7 +1621,7 @@ defimpl Enumerable, for: Stream do
|
||||
defp do_done({reason, [acc | _]}, nil), do: {reason, acc}
|
||||
|
||||
defp do_done({reason, [acc | t]}, {done, fun}) do
|
||||
[h | _] = :lists.reverse(t)
|
||||
[h | _] = Enum.reverse(t)
|
||||
|
||||
case done.([acc, h], fun) do
|
||||
{:cont, [acc | _]} -> {reason, acc}
|
||||
@@ -1745,7 +1635,7 @@ defimpl Inspect, for: Stream do
|
||||
import Inspect.Algebra
|
||||
|
||||
def inspect(%{enum: enum, funs: funs}, opts) do
|
||||
inner = [enum: enum, funs: :lists.reverse(funs)]
|
||||
inner = [enum: enum, funs: Enum.reverse(funs)]
|
||||
concat(["#Stream<", to_doc(inner, opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
+126
-214
@@ -60,7 +60,7 @@ defmodule String do
|
||||
Note it is generally not advised to use `\xNN` in Elixir
|
||||
strings, as introducing an invalid byte sequence would
|
||||
make the string invalid. If you have to introduce a
|
||||
character by its hexadecimal representation, it is best
|
||||
character by its hexdecimal representation, it is best
|
||||
to work with Unicode code points, such as `\uNNNN`. In fact,
|
||||
understanding Unicode code points can be essential when doing
|
||||
low-level manipulations of string, so let's explore them in
|
||||
@@ -69,7 +69,7 @@ defmodule String do
|
||||
## Code points and grapheme cluster
|
||||
|
||||
The functions in this module act according to the Unicode
|
||||
Standard, version 13.0.0.
|
||||
Standard, version 12.1.0.
|
||||
|
||||
As per the standard, a code point is a single Unicode Character,
|
||||
which may be represented by one or more bytes.
|
||||
@@ -133,7 +133,7 @@ defmodule String do
|
||||
* `Kernel.bit_size/1` and `Kernel.byte_size/1` - size related functions
|
||||
* `Kernel.is_bitstring/1` and `Kernel.is_binary/1` - type-check function
|
||||
* Plus a number of functions for working with binaries (bytes)
|
||||
in the [`:binary` module](`:binary`)
|
||||
in the [`:binary` module](http://www.erlang.org/doc/man/binary.html)
|
||||
|
||||
There are many situations where using the `String` module can
|
||||
be avoided in favor of binary functions or pattern matching.
|
||||
@@ -193,15 +193,14 @@ defmodule String do
|
||||
|
||||
## Integer code points
|
||||
|
||||
Although code points are represented as integers, this module
|
||||
represents code points in their encoded format as strings.
|
||||
For example:
|
||||
Although code points could be represented as integers, this
|
||||
module represents all code points as strings. For example:
|
||||
|
||||
iex> String.codepoints("olá")
|
||||
["o", "l", "á"]
|
||||
|
||||
There are a couple of ways to retrieve the character code point.
|
||||
One may use the `?` construct:
|
||||
There are a couple of ways to retrieve a character integer
|
||||
code point. One may use the `?` construct:
|
||||
|
||||
iex> ?o
|
||||
111
|
||||
@@ -221,9 +220,9 @@ defmodule String do
|
||||
iex> "ol\u00E1"
|
||||
"olá"
|
||||
|
||||
Finally, to convert a String into a list of integer
|
||||
code points, known as "charlists" in Elixir, you can call
|
||||
`String.to_charlist`:
|
||||
Finally, to convert a String into a list of integers
|
||||
code points, usually known as "char lists", you can call
|
||||
`Strig.to_charlist`:
|
||||
|
||||
iex> String.to_charlist("olá")
|
||||
[111, 108, 225]
|
||||
@@ -248,7 +247,7 @@ defmodule String do
|
||||
## Compile binary patterns
|
||||
|
||||
Many functions in this module work with patterns. For example,
|
||||
`String.split/3` can split a string into multiple strings given
|
||||
`String.split/2` can split a string into multiple strings given
|
||||
a pattern. This pattern can be a string, a list of strings or
|
||||
a compiled pattern:
|
||||
|
||||
@@ -283,10 +282,10 @@ defmodule String do
|
||||
@typedoc "Multiple code points that may be perceived as a single character by readers"
|
||||
@type grapheme :: t
|
||||
|
||||
@typedoc "Pattern used in functions like `replace/4` and `split/3`"
|
||||
@typedoc "Pattern used in functions like `replace/3` and `split/2`"
|
||||
@type pattern :: t | [t] | :binary.cp()
|
||||
|
||||
@conditional_mappings [:greek, :turkic]
|
||||
@conditional_mappings [:greek]
|
||||
|
||||
@doc """
|
||||
Checks if a string contains only printable characters up to `character_limit`.
|
||||
@@ -374,10 +373,9 @@ defmodule String do
|
||||
@doc ~S"""
|
||||
Divides a string into parts based on a pattern.
|
||||
|
||||
Returns a list of these parts.
|
||||
|
||||
The `pattern` may be a string, a list of strings, a regular expression, or a
|
||||
compiled pattern.
|
||||
Returns a list of these parts. The pattern can
|
||||
be a string, a list of strings, a regular expression,
|
||||
or a compiled pattern.
|
||||
|
||||
The string is split into as many parts as possible by
|
||||
default, but can be controlled via the `:parts` option.
|
||||
@@ -470,11 +468,11 @@ defmodule String do
|
||||
@spec split(t, pattern | Regex.t(), keyword) :: [t]
|
||||
def split(string, pattern, options \\ [])
|
||||
|
||||
def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
|
||||
def split(string, %Regex{} = pattern, options) when is_binary(string) do
|
||||
Regex.split(pattern, string, options)
|
||||
end
|
||||
|
||||
def split(string, "", options) when is_binary(string) and is_list(options) do
|
||||
def split(string, "", options) when is_binary(string) do
|
||||
parts = Keyword.get(options, :parts, :infinity)
|
||||
index = parts_to_index(parts)
|
||||
trim = Keyword.get(options, :trim, false)
|
||||
@@ -486,7 +484,7 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
def split(string, pattern, options) when is_binary(string) and is_list(options) do
|
||||
def split(string, pattern, options) when is_binary(string) do
|
||||
parts = Keyword.get(options, :parts, :infinity)
|
||||
trim = Keyword.get(options, :trim, false)
|
||||
|
||||
@@ -559,7 +557,7 @@ defmodule String do
|
||||
@spec splitter(t, pattern, keyword) :: Enumerable.t()
|
||||
def splitter(string, pattern, options \\ [])
|
||||
|
||||
def splitter(string, "", options) when is_binary(string) and is_list(options) do
|
||||
def splitter(string, "", options) do
|
||||
if Keyword.get(options, :trim, false) do
|
||||
Stream.unfold(string, &next_grapheme/1)
|
||||
else
|
||||
@@ -567,7 +565,7 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
def splitter(string, pattern, options) when is_binary(string) and is_list(options) do
|
||||
def splitter(string, pattern, options) do
|
||||
pattern = maybe_compile_pattern(pattern)
|
||||
trim = Keyword.get(options, :trim, false)
|
||||
Stream.unfold(string, &do_splitter(&1, pattern, trim))
|
||||
@@ -626,13 +624,11 @@ defmodule String do
|
||||
@spec split_at(t, integer) :: {t, t}
|
||||
def split_at(string, position)
|
||||
|
||||
def split_at(string, position)
|
||||
when is_binary(string) and is_integer(position) and position >= 0 do
|
||||
def split_at(string, position) when is_integer(position) and position >= 0 do
|
||||
do_split_at(string, position)
|
||||
end
|
||||
|
||||
def split_at(string, position)
|
||||
when is_binary(string) and is_integer(position) and position < 0 do
|
||||
def split_at(string, position) when is_integer(position) and position < 0 do
|
||||
position = length(string) + position
|
||||
|
||||
case position >= 0 do
|
||||
@@ -647,7 +643,7 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns `true` if `string1` is canonically equivalent to `string2`.
|
||||
Returns `true` if `string1` is canonically equivalent to 'string2'.
|
||||
|
||||
It performs Normalization Form Canonical Decomposition (NFD) on the
|
||||
strings before comparing them. This function is equivalent to:
|
||||
@@ -674,7 +670,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec equivalent?(t, t) :: boolean
|
||||
def equivalent?(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
def equivalent?(string1, string2) do
|
||||
normalize(string1, :nfd) == normalize(string2, :nfd)
|
||||
end
|
||||
|
||||
@@ -683,15 +679,9 @@ defmodule String do
|
||||
form identified by `form`.
|
||||
|
||||
Invalid Unicode codepoints are skipped and the remaining of
|
||||
the string is converted. If you want the algorithm to stop
|
||||
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`,
|
||||
`:unicode.characters_to_nfc_binary/1`, `:unicode.characters_to_nfkd_binary/1`,
|
||||
and `:unicode.characters_to_nfkc_binary/1` instead.
|
||||
|
||||
Normalization forms `:nfkc` and `:nfkd` should not be blindly applied
|
||||
to arbitrary text. Because they erase many formatting distinctions,
|
||||
they will prevent round-trip conversion to and from many legacy
|
||||
character sets.
|
||||
the string is converted. If you want the algorith to stop
|
||||
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`
|
||||
and `:unicode.characters_to_nfc_binary/1` instead.
|
||||
|
||||
## Forms
|
||||
|
||||
@@ -705,14 +695,6 @@ defmodule String do
|
||||
* `:nfc` - Normalization Form Canonical Composition.
|
||||
Characters are decomposed and then recomposed by canonical equivalence.
|
||||
|
||||
* `:nfkd` - Normalization Form Compatibility Decomposition.
|
||||
Characters are decomposed by compatibility equivalence, and
|
||||
multiple combining characters are arranged in a specific
|
||||
order.
|
||||
|
||||
* `:nfkc` - Normalization Form Compatibility Composition.
|
||||
Characters are decomposed and then recomposed by compatibility equivalence.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.normalize("yêṩ", :nfd)
|
||||
@@ -721,50 +703,30 @@ defmodule String do
|
||||
iex> String.normalize("leña", :nfc)
|
||||
"leña"
|
||||
|
||||
iex> String.normalize("fi", :nfkd)
|
||||
"fi"
|
||||
|
||||
iex> String.normalize("fi", :nfkc)
|
||||
"fi"
|
||||
|
||||
"""
|
||||
def normalize(string, form)
|
||||
|
||||
def normalize(string, :nfd) when is_binary(string) do
|
||||
def normalize(string, :nfd) do
|
||||
case :unicode.characters_to_nfd_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfd)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfc) when is_binary(string) do
|
||||
def normalize(string, :nfc) do
|
||||
case :unicode.characters_to_nfc_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfc)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfkd) when is_binary(string) do
|
||||
case :unicode.characters_to_nfkd_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkd)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfkc) when is_binary(string) do
|
||||
case :unicode.characters_to_nfkc_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkc)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts all characters in the given string to uppercase according to `mode`.
|
||||
|
||||
`mode` may be `:default`, `:ascii`, `:greek` or `:turkic`. The `:default` mode considers
|
||||
`mode` may be `:default`, `:ascii` or `:greek`. The `:default` mode considers
|
||||
all non-conditional transformations outlined in the Unicode standard. `:ascii`
|
||||
uppercases only the letters a to z. `:greek` includes the context sensitive
|
||||
mappings found in Greek. `:turkic` properly handles the letter i with the dotless variant.
|
||||
mappings found in Greek.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -784,16 +746,8 @@ defmodule String do
|
||||
iex> String.upcase("olá", :ascii)
|
||||
"OLá"
|
||||
|
||||
And `:turkic` properly handles the letter i with the dotless variant:
|
||||
|
||||
iex> String.upcase("ıi")
|
||||
"II"
|
||||
|
||||
iex> String.upcase("ıi", :turkic)
|
||||
"Iİ"
|
||||
|
||||
"""
|
||||
@spec upcase(t, :default | :ascii | :greek | :turkic) :: t
|
||||
@spec upcase(t, :default | :ascii | :greek) :: t
|
||||
def upcase(string, mode \\ :default)
|
||||
|
||||
def upcase("", _mode) do
|
||||
@@ -808,7 +762,7 @@ defmodule String do
|
||||
IO.iodata_to_binary(upcase_ascii(string))
|
||||
end
|
||||
|
||||
def upcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
|
||||
def upcase(string, mode) when mode in @conditional_mappings do
|
||||
String.Casing.upcase(string, [], mode)
|
||||
end
|
||||
|
||||
@@ -821,10 +775,10 @@ defmodule String do
|
||||
@doc """
|
||||
Converts all characters in the given string to lowercase according to `mode`.
|
||||
|
||||
`mode` may be `:default`, `:ascii`, `:greek` or `:turkic`. The `:default` mode considers
|
||||
`mode` may be `:default`, `:ascii` or `:greek`. The `:default` mode considers
|
||||
all non-conditional transformations outlined in the Unicode standard. `:ascii`
|
||||
lowercases only the letters A to Z. `:greek` includes the context sensitive
|
||||
mappings found in Greek. `:turkic` properly handles the letter i with the dotless variant.
|
||||
mappings found in Greek.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -844,7 +798,7 @@ defmodule String do
|
||||
iex> String.downcase("OLÁ", :ascii)
|
||||
"olÁ"
|
||||
|
||||
The `:greek` mode properly handles the context sensitive sigma in Greek:
|
||||
And `:greek` properly handles the context sensitive sigma in Greek:
|
||||
|
||||
iex> String.downcase("ΣΣ")
|
||||
"σσ"
|
||||
@@ -852,16 +806,8 @@ defmodule String do
|
||||
iex> String.downcase("ΣΣ", :greek)
|
||||
"σς"
|
||||
|
||||
And `:turkic` properly handles the letter i with the dotless variant:
|
||||
|
||||
iex> String.downcase("Iİ")
|
||||
"ii̇"
|
||||
|
||||
iex> String.downcase("Iİ", :turkic)
|
||||
"ıi"
|
||||
|
||||
"""
|
||||
@spec downcase(t, :default | :ascii | :greek | :turkic) :: t
|
||||
@spec downcase(t, :default | :ascii | :greek) :: t
|
||||
def downcase(string, mode \\ :default)
|
||||
|
||||
def downcase("", _mode) do
|
||||
@@ -876,7 +822,7 @@ defmodule String do
|
||||
IO.iodata_to_binary(downcase_ascii(string))
|
||||
end
|
||||
|
||||
def downcase(string, mode) when is_binary(string) and mode in @conditional_mappings do
|
||||
def downcase(string, mode) when mode in @conditional_mappings do
|
||||
String.Casing.downcase(string, [], mode)
|
||||
end
|
||||
|
||||
@@ -890,10 +836,10 @@ defmodule String do
|
||||
Converts the first character in the given string to
|
||||
uppercase and the remainder to lowercase according to `mode`.
|
||||
|
||||
`mode` may be `:default`, `:ascii`, `:greek` or `:turkic`. The `:default` mode considers
|
||||
`mode` may be `:default`, `:ascii` or `:greek`. The `:default` mode considers
|
||||
all non-conditional transformations outlined in the Unicode standard. `:ascii`
|
||||
capitalizes only the letters A to Z. `:greek` includes the context sensitive
|
||||
mappings found in Greek. `:turkic` properly handles the letter i with the dotless variant.
|
||||
lowercases only the letters A to Z. `:greek` includes the context sensitive
|
||||
mappings found in Greek.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -907,7 +853,7 @@ defmodule String do
|
||||
"Olá"
|
||||
|
||||
"""
|
||||
@spec capitalize(t, :default | :ascii | :greek | :turkic) :: t
|
||||
@spec capitalize(t, :default | :ascii | :greek) :: t
|
||||
def capitalize(string, mode \\ :default)
|
||||
|
||||
def capitalize(<<char, rest::binary>>, :ascii) do
|
||||
@@ -1181,8 +1127,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec trim_leading(t, t) :: t
|
||||
def trim_leading(string, to_trim)
|
||||
when is_binary(string) and is_binary(to_trim) do
|
||||
def trim_leading(string, to_trim) do
|
||||
replace_leading(string, to_trim, "")
|
||||
end
|
||||
|
||||
@@ -1212,8 +1157,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec trim_trailing(t, t) :: t
|
||||
def trim_trailing(string, to_trim)
|
||||
when is_binary(string) and is_binary(to_trim) do
|
||||
def trim_trailing(string, to_trim) do
|
||||
replace_trailing(string, to_trim, "")
|
||||
end
|
||||
|
||||
@@ -1228,7 +1172,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec trim(t) :: t
|
||||
def trim(string) when is_binary(string) do
|
||||
def trim(string) do
|
||||
string
|
||||
|> trim_leading()
|
||||
|> trim_trailing()
|
||||
@@ -1245,7 +1189,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec trim(t, t) :: t
|
||||
def trim(string, to_trim) when is_binary(string) and is_binary(to_trim) do
|
||||
def trim(string, to_trim) do
|
||||
string
|
||||
|> trim_leading(to_trim)
|
||||
|> trim_trailing(to_trim)
|
||||
@@ -1336,12 +1280,12 @@ defmodule String do
|
||||
end
|
||||
|
||||
defp pad(kind, string, count, padding) do
|
||||
string_length = length(string)
|
||||
string_len = length(string)
|
||||
|
||||
if string_length >= count do
|
||||
if string_len >= count do
|
||||
string
|
||||
else
|
||||
filler = build_filler(count - string_length, padding, padding, 0, [])
|
||||
filler = build_filler(count - string_len, padding, padding, 0, [])
|
||||
|
||||
case kind do
|
||||
:leading -> [filler | string]
|
||||
@@ -1377,26 +1321,26 @@ defmodule String do
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_leading/2 instead"
|
||||
def rjust(subject, length) do
|
||||
rjust(subject, length, ?\s)
|
||||
def rjust(subject, len) do
|
||||
rjust(subject, len, ?\s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_leading/3 with a binary padding instead"
|
||||
def rjust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
|
||||
pad(:leading, subject, length, [<<pad::utf8>>])
|
||||
def rjust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
|
||||
pad(:leading, subject, len, [<<pad::utf8>>])
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_trailing/2 instead"
|
||||
def ljust(subject, length) do
|
||||
ljust(subject, length, ?\s)
|
||||
def ljust(subject, len) do
|
||||
ljust(subject, len, ?\s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_trailing/3 with a binary padding instead"
|
||||
def ljust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
|
||||
pad(:trailing, subject, length, [<<pad::utf8>>])
|
||||
def ljust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
|
||||
pad(:trailing, subject, len, [<<pad::utf8>>])
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -1405,8 +1349,7 @@ defmodule String do
|
||||
|
||||
The `subject` is always a string.
|
||||
|
||||
The `pattern` may be a string, a list of strings, a regular expression, or a
|
||||
compiled pattern.
|
||||
The `pattern` may be a string, a regular expression, or a compiled pattern.
|
||||
|
||||
The `replacement` may be a string or a function that receives the matched
|
||||
pattern and must return the replacement as a string or iodata.
|
||||
@@ -1441,7 +1384,7 @@ defmodule String do
|
||||
iex> String.replace("a,b,c", ~r/,(.)/, ",\\1\\g{1}")
|
||||
"a,bb,cc"
|
||||
|
||||
Note that we had to escape the backslash escape character (i.e., we used `\\N`
|
||||
Notice we had to escape the backslash escape character (i.e., we used `\\N`
|
||||
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
|
||||
giving `\0`, one can inject the whole match in the replacement string.
|
||||
|
||||
@@ -1582,7 +1525,7 @@ defmodule String do
|
||||
one single grapheme.
|
||||
"""
|
||||
@spec reverse(t) :: t
|
||||
def reverse(string) when is_binary(string) do
|
||||
def reverse(string) do
|
||||
do_reverse(next_grapheme(string), [])
|
||||
end
|
||||
|
||||
@@ -1595,7 +1538,7 @@ defmodule String do
|
||||
@compile {:inline, duplicate: 2}
|
||||
|
||||
@doc """
|
||||
Returns a string `subject` repeated `n` times.
|
||||
Returns a string `subject` duplicated `n` times.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -1612,17 +1555,14 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec duplicate(t, non_neg_integer) :: t
|
||||
def duplicate(subject, n) when is_binary(subject) and is_integer(n) and n >= 0 do
|
||||
def duplicate(subject, n) do
|
||||
:binary.copy(subject, n)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Returns a list of code points encoded as strings.
|
||||
@doc """
|
||||
Returns all code points in the string.
|
||||
|
||||
To retrieve code points in their natural integer
|
||||
representation, see `to_charlist/1`. For details about
|
||||
code points and graphemes, see the `String` module
|
||||
documentation.
|
||||
For details about code points and graphemes, see the `String` module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1712,12 +1652,11 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec valid?(t) :: boolean
|
||||
def valid?(<<string::binary>>), do: valid_utf8?(string)
|
||||
def valid?(_), do: false
|
||||
def valid?(string)
|
||||
|
||||
defp valid_utf8?(<<_::utf8, rest::bits>>), do: valid_utf8?(rest)
|
||||
defp valid_utf8?(<<>>), do: true
|
||||
defp valid_utf8?(_), do: false
|
||||
def valid?(<<_::utf8, t::binary>>), do: valid?(t)
|
||||
def valid?(<<>>), do: true
|
||||
def valid?(_), do: false
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.valid?/1 instead"
|
||||
@@ -1762,7 +1701,7 @@ defmodule String do
|
||||
|
||||
def chunk("", _), do: []
|
||||
|
||||
def chunk(string, trait) when is_binary(string) and trait in [:valid, :printable] do
|
||||
def chunk(string, trait) when trait in [:valid, :printable] do
|
||||
{cp, _} = next_codepoint(string)
|
||||
pred_fn = make_chunk_pred(trait)
|
||||
do_chunk(string, pred_fn.(cp), pred_fn)
|
||||
@@ -1825,12 +1764,9 @@ defmodule String do
|
||||
iex> String.next_grapheme("olá")
|
||||
{"o", "lá"}
|
||||
|
||||
iex> String.next_grapheme("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec next_grapheme(t) :: {grapheme, t} | nil
|
||||
def next_grapheme(binary) when is_binary(binary) do
|
||||
def next_grapheme(binary) do
|
||||
case next_grapheme_size(binary) do
|
||||
{size, rest} -> {binary_part(binary, 0, size), rest}
|
||||
nil -> nil
|
||||
@@ -1838,9 +1774,9 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the size (in bytes) of the next grapheme.
|
||||
Returns the size of the next grapheme.
|
||||
|
||||
The result is a tuple with the next grapheme size in bytes and
|
||||
The result is a tuple with the next grapheme size and
|
||||
the remainder of the string or `nil` in case the string
|
||||
reached its end.
|
||||
|
||||
@@ -1849,9 +1785,6 @@ defmodule String do
|
||||
iex> String.next_grapheme_size("olá")
|
||||
{1, "lá"}
|
||||
|
||||
iex> String.next_grapheme_size("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec next_grapheme_size(t) :: {pos_integer, t} | nil
|
||||
defdelegate next_grapheme_size(string), to: String.Unicode
|
||||
@@ -1868,12 +1801,9 @@ defmodule String do
|
||||
iex> String.first("եոգլի")
|
||||
"ե"
|
||||
|
||||
iex> String.first("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec first(t) :: grapheme | nil
|
||||
def first(string) when is_binary(string) do
|
||||
def first(string) do
|
||||
case next_grapheme(string) do
|
||||
{char, _} -> char
|
||||
nil -> nil
|
||||
@@ -1894,7 +1824,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec last(t) :: grapheme | nil
|
||||
def last(string) when is_binary(string) do
|
||||
def last(string) do
|
||||
do_last(next_grapheme(string), nil)
|
||||
end
|
||||
|
||||
@@ -1943,11 +1873,11 @@ defmodule String do
|
||||
"""
|
||||
@spec at(t, integer) :: grapheme | nil
|
||||
|
||||
def at(string, position) when is_binary(string) and is_integer(position) and position >= 0 do
|
||||
def at(string, position) when is_integer(position) and position >= 0 do
|
||||
do_at(string, position)
|
||||
end
|
||||
|
||||
def at(string, position) when is_binary(string) and is_integer(position) and position < 0 do
|
||||
def at(string, position) when is_integer(position) and position < 0 do
|
||||
position = length(string) + position
|
||||
|
||||
case position >= 0 do
|
||||
@@ -1964,7 +1894,8 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a substring starting at the offset `start`, and of the given `length`.
|
||||
Returns a substring starting at the offset `start`, and of
|
||||
length `len`.
|
||||
|
||||
If the offset is greater than string length, then it returns `""`.
|
||||
|
||||
@@ -2005,26 +1936,22 @@ defmodule String do
|
||||
""
|
||||
end
|
||||
|
||||
def slice(string, start, length)
|
||||
when is_binary(string) and is_integer(start) and is_integer(length) and start >= 0 and
|
||||
length >= 0 do
|
||||
def slice(string, start, len) when start >= 0 and len >= 0 do
|
||||
case String.Unicode.split_at(string, start) do
|
||||
{_, nil} ->
|
||||
""
|
||||
|
||||
{start_bytes, rest} ->
|
||||
{len_bytes, _} = String.Unicode.split_at(rest, length)
|
||||
{len_bytes, _} = String.Unicode.split_at(rest, len)
|
||||
binary_part(string, start_bytes, len_bytes)
|
||||
end
|
||||
end
|
||||
|
||||
def slice(string, start, length)
|
||||
when is_binary(string) and is_integer(start) and is_integer(length) and start < 0 and
|
||||
length >= 0 do
|
||||
def slice(string, start, len) when start < 0 and len >= 0 do
|
||||
start = length(string) + start
|
||||
|
||||
case start >= 0 do
|
||||
true -> slice(string, start, length)
|
||||
true -> slice(string, start, len)
|
||||
false -> ""
|
||||
end
|
||||
end
|
||||
@@ -2052,24 +1979,19 @@ defmodule String do
|
||||
iex> String.slice("elixir", 1..10)
|
||||
"lixir"
|
||||
|
||||
iex> String.slice("elixir", 10..3)
|
||||
""
|
||||
|
||||
iex> String.slice("elixir", -4..-1)
|
||||
"ixir"
|
||||
|
||||
iex> String.slice("elixir", 2..-1)
|
||||
"ixir"
|
||||
|
||||
iex> String.slice("elixir", -4..6)
|
||||
"ixir"
|
||||
|
||||
For ranges where `start > stop`, you need to explicit
|
||||
mark them as increasing:
|
||||
|
||||
iex> String.slice("elixir", 2..-1//1)
|
||||
"ixir"
|
||||
|
||||
iex> String.slice("elixir", 1..-2//1)
|
||||
"lixi"
|
||||
|
||||
If values are out of bounds, it returns an empty string:
|
||||
|
||||
iex> String.slice("elixir", 10..3)
|
||||
iex> String.slice("elixir", -1..-4)
|
||||
""
|
||||
|
||||
iex> String.slice("elixir", -10..-7)
|
||||
@@ -2083,31 +2005,22 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec slice(t, Range.t()) :: t
|
||||
def slice(string, first..last//step = range) when is_binary(string) do
|
||||
# TODO: Deprecate negative steps on Elixir v1.16
|
||||
# TODO: There are two features we can add to slicing ranges:
|
||||
# 1. We can allow the step to be any positive number
|
||||
# 2. We can allow slice and reverse at the same time. However, we can't
|
||||
# implement so right now. First we will have to raise if a decreasing
|
||||
# range is given on Elixir v2.0.
|
||||
if step == 1 or (step == -1 and first > last) do
|
||||
slice_range(string, first, last)
|
||||
else
|
||||
raise ArgumentError,
|
||||
"String.slice/2 does not accept ranges with custom steps, got: #{inspect(range)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp slice_range("", _, _), do: ""
|
||||
def slice(string, range)
|
||||
|
||||
defp slice_range(string, first, -1) when first >= 0 do
|
||||
def slice("", _.._), do: ""
|
||||
|
||||
def slice(string, first..-1) when first >= 0 do
|
||||
case String.Unicode.split_at(string, first) do
|
||||
{_, nil} -> ""
|
||||
{start_bytes, _} -> binary_part(string, start_bytes, byte_size(string) - start_bytes)
|
||||
{_, nil} ->
|
||||
""
|
||||
|
||||
{start_bytes, _} ->
|
||||
binary_part(string, start_bytes, byte_size(string) - start_bytes)
|
||||
end
|
||||
end
|
||||
|
||||
defp slice_range(string, first, last) when first >= 0 and last >= 0 do
|
||||
def slice(string, first..last) when first >= 0 and last >= 0 do
|
||||
if last >= first do
|
||||
slice(string, first, last - first + 1)
|
||||
else
|
||||
@@ -2115,8 +2028,9 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
defp slice_range(string, first, last) do
|
||||
{bytes, length} = acc_bytes(next_grapheme_size(string), [], 0)
|
||||
def slice(string, first..last) do
|
||||
{bytes, length} = do_acc_bytes(next_grapheme_size(string), [], 0)
|
||||
|
||||
first = add_if_negative(first, length)
|
||||
last = add_if_negative(last, length)
|
||||
|
||||
@@ -2126,25 +2040,21 @@ defmodule String do
|
||||
last = min(last + 1, length)
|
||||
bytes = Enum.drop(bytes, length - last)
|
||||
first = last - first
|
||||
{length_bytes, start_bytes} = split_bytes(bytes, 0, first)
|
||||
binary_part(string, start_bytes, length_bytes)
|
||||
{length_bytes, start_bytes} = Enum.split(bytes, first)
|
||||
binary_part(string, Enum.sum(start_bytes), Enum.sum(length_bytes))
|
||||
end
|
||||
end
|
||||
|
||||
defp acc_bytes({size, rest}, bytes, length) do
|
||||
acc_bytes(next_grapheme_size(rest), [size | bytes], length + 1)
|
||||
end
|
||||
|
||||
defp acc_bytes(nil, bytes, length) do
|
||||
{bytes, length}
|
||||
end
|
||||
|
||||
defp add_if_negative(value, to_add) when value < 0, do: value + to_add
|
||||
defp add_if_negative(value, _to_add), do: value
|
||||
|
||||
defp split_bytes(rest, acc, 0), do: {acc, Enum.sum(rest)}
|
||||
defp split_bytes([], acc, _), do: {acc, 0}
|
||||
defp split_bytes([head | tail], acc, count), do: split_bytes(tail, head + acc, count - 1)
|
||||
defp do_acc_bytes({size, rest}, bytes, length) do
|
||||
do_acc_bytes(next_grapheme_size(rest), [size | bytes], length + 1)
|
||||
end
|
||||
|
||||
defp do_acc_bytes(nil, bytes, length) do
|
||||
{bytes, length}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if `string` starts with any of the prefixes given.
|
||||
@@ -2256,7 +2166,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec match?(t, Regex.t()) :: boolean
|
||||
def match?(string, regex) when is_binary(string) do
|
||||
def match?(string, regex) do
|
||||
Regex.match?(regex, string)
|
||||
end
|
||||
|
||||
@@ -2323,7 +2233,7 @@ defmodule String do
|
||||
strings.
|
||||
|
||||
In case you need to work with bytes, take a look at the
|
||||
[`:binary` module](`:binary`).
|
||||
[`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2367,7 +2277,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec to_atom(String.t()) :: atom
|
||||
def to_atom(string) when is_binary(string) do
|
||||
def to_atom(string) do
|
||||
:erlang.binary_to_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@@ -2384,9 +2294,12 @@ defmodule String do
|
||||
iex> String.to_existing_atom("my_atom")
|
||||
:my_atom
|
||||
|
||||
iex> String.to_existing_atom("this_atom_will_never_exist")
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec to_existing_atom(String.t()) :: atom
|
||||
def to_existing_atom(string) when is_binary(string) do
|
||||
def to_existing_atom(string) do
|
||||
:erlang.binary_to_existing_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@@ -2412,7 +2325,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec to_integer(String.t()) :: integer
|
||||
def to_integer(string) when is_binary(string) do
|
||||
def to_integer(string) do
|
||||
:erlang.binary_to_integer(string)
|
||||
end
|
||||
|
||||
@@ -2428,7 +2341,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec to_integer(String.t(), 2..36) :: integer
|
||||
def to_integer(string, base) when is_binary(string) and is_integer(base) do
|
||||
def to_integer(string, base) do
|
||||
:erlang.binary_to_integer(string, base)
|
||||
end
|
||||
|
||||
@@ -2454,7 +2367,7 @@ defmodule String do
|
||||
|
||||
"""
|
||||
@spec to_float(String.t()) :: float
|
||||
def to_float(string) when is_binary(string) do
|
||||
def to_float(string) do
|
||||
:erlang.binary_to_float(string)
|
||||
end
|
||||
|
||||
@@ -2491,7 +2404,7 @@ defmodule String do
|
||||
def bag_distance(_string, ""), do: 0.0
|
||||
def bag_distance("", _string), do: 0.0
|
||||
|
||||
def bag_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
def bag_distance(string1, string2) do
|
||||
{bag1, length1} = string_to_bag(string1, %{}, 0)
|
||||
{bag2, length2} = string_to_bag(string2, %{}, 0)
|
||||
|
||||
@@ -2557,7 +2470,7 @@ defmodule String do
|
||||
def jaro_distance(_string, ""), do: 0.0
|
||||
def jaro_distance("", _string), do: 0.0
|
||||
|
||||
def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
def jaro_distance(string1, string2) do
|
||||
{chars1, len1} = chars_and_length(string1)
|
||||
{chars2, len2} = chars_and_length(string2)
|
||||
|
||||
@@ -2642,9 +2555,8 @@ defmodule String do
|
||||
[eq: "fox ", del: "ho", ins: "jum", eq: "ps over the ", del: "dog", ins: "lazy cat"]
|
||||
|
||||
"""
|
||||
@doc since: "1.3.0"
|
||||
@spec myers_difference(t, t) :: [{:eq | :ins | :del, t}]
|
||||
def myers_difference(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
def myers_difference(string1, string2) do
|
||||
graphemes(string1)
|
||||
|> List.myers_difference(graphemes(string2))
|
||||
|> Enum.map(fn {kind, chars} -> {kind, IO.iodata_to_binary(chars)} end)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user