Compare commits

..
Author SHA1 Message Date
jamauro 372836d623 Change variable from bk to bucket (#13921)
Simple change from `bk` to `bucket` for consistency with the rest of the doc
2024-10-21 18:00:21 +02:00
Jean Klingler 444cd7e6bd Fix crash in Macro.to_string/1 on invalid sigils (#13905) 2024-10-17 15:39:57 +09:00
Jean Klingler cacb175ab3 Add @doc false to Inspect.Algebra.no_limit/1 (#13893) 2024-10-10 18:21:49 +09:00
Jean Klingler bfce3ed7a9 Fix another dialyzer Kernel.with edge case (#13869)
Close https://github.com/elixir-lang/elixir/issues/13868
2024-09-30 20:11:05 +02:00
Vasily Suvorov 06811cbdc8 Fix typedoc for init_option (#13870)
Change `c:init/1` to`init/2` in the `typedoc`'s text
2024-09-30 15:41:35 +02:00
Igor Goryachev 7a6b35422c Fix logger translator tests under Erlang 27.1 (#13839) 2024-09-21 13:36:09 +02:00
José Valim 78f63d0831 Release v1.17.3 2024-09-18 14:33:39 +02:00
Urmas Talimaa 86ec46a5c4 Fix parsing of fractional durations (#13832)
The parsing of fractional durations checked for non-negativity by
testing second > 0, which reports false for not only negative integers
but also for 0.

Note that changing `if second > 0` to `if second >= 0` would fix
behaviour for "PT0,6S", but would break "PT-0,6S".
2024-09-18 09:03:51 +02:00
José Valim c99631876c Exclude latest tags 2024-09-17 18:01:17 +02:00
José Valim 46815ef554 Fix docs node generation 2024-09-17 16:40:01 +02:00
José Valim 256a55a53e Do not verify modules twice 2024-09-17 16:22:38 +02:00
Jean Klingler a78c92043f Fix: Macro.expand/2 should not expand __ENV__ on :match context (#13807) 2024-09-06 17:58:28 +09:00
José Valim b3bfa519a7 Pass proper path to consolidation in IEx recompile 2024-08-20 11:15:30 +02:00
Steve Cohen b734da3f62 Ensure :native is a valid parameter type (#13781)
Several of the functions in System that deal with time had a typespec
that only allowed the time_unit type even though they accepted the
:native time unit. This confuses dialyzer if :native is passed to
these functions
2024-08-17 10:53:36 +09:00
José Valim 10f86595b5 Raise clearer error message if struct is missing on type checker 2024-08-09 13:05:28 +02:00
José Valim 1da385776f More docs 2024-08-07 01:06:57 +02:00
José Valim a45e08eeda Keep labels with at least one occurrence on xref graph cycles and stats.
Closes #13762.
2024-08-07 00:57:21 +02:00
Artur Plysiuk bb61b953d7 Fix spec for to_timeout/1 (#13755) 2024-08-02 12:15:17 +02:00
José Valim 1e126e88b7 Do not attempt to group module warnings with context
Otherwise, the larger the context, the more expensive
grouping the warnings would get.

Closes #13742
2024-07-28 20:12:15 +02:00
José Valim 47abe2d107 Release v1.17.2 2024-07-06 23:10:25 +02:00
José Valim e254e7e68e Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:54 +02:00
Wojtek Mach d3c0b38c5d Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

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

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

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

  * Update generated `rel/env.bat.exs`
2024-07-02 15:21:38 +02:00
José Valim 662dd6b8d9 Add CHANGELOG entry on jaro_distance, closes #13696 2024-06-27 09:14:53 +02:00
José Valim cadd501cbd Clarify cookie generation, closes #13694 2024-06-26 13:28:57 +02:00
Alexey Nikitin ddf84aba7e Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:47:21 +02:00
Jean Klingler 2d808ec652 Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:56 +09:00
José Valim 6bfb95ab88 Release v1.17.1 2024-06-18 13:11:00 +02:00
José Valim d53977002d Print type violation slogan 2024-06-18 12:46:59 +02:00
José Valim 776e5a34ea Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:29 +02:00
Jean Klingler ae5707f90a Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:37 +09:00
José Valim 67617e5d8c Do not warn for assignment with blocks in EEx 2024-06-15 11:05:45 +02:00
José Valim 6f5fc94276 Clarify docs, closes #13666 2024-06-15 10:20:40 +02:00
José Valim 6c935ca6ec Clarify charlists intro, closes #13663 2024-06-14 17:34:27 +02:00
José Valim d1076e487c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

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

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:07:02 +02:00
Jonatan Kłosko b9e447757d Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-13 16:15:45 +02:00
Jean Klingler b7943e113d Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 16:15:32 +02:00
Ben Murden 1a320bb60d Minor grammatical fixes on warning message (#13654) 2024-06-13 16:15:25 +02:00
Ian Harris 64e63b1b9d Update compatibility-and-deprecations.md (#13657)
Fixed link for changelog to v1.17.
2024-06-12 19:12:55 +02:00
José Valim 5a8b28427c Update CHANGELOG 2024-06-12 15:20:57 +02:00
José Valim a1eb59d035 Remove /main link from CHANGELOG 2024-06-12 13:54:10 +02:00
José Valim 0b8623a032 Release v1.17.0 2024-06-12 12:23:37 +02:00
José Valim 1f187b529e Clarify typespecs handling 2024-06-11 21:13:57 +02:00
José Valim bde5b2d2fc s/detail/details 2024-06-10 22:38:23 +02:00
Maksym Verbovyi 357ffc497d Correct example in the docs (#13638) 2024-06-10 16:38:08 +02:00
Lukasz Samson 48338bc093 Fix crash when capturing ambiguous import 2024-06-10 16:37:34 +02:00
José Valim d547721c41 Add detail and span to typing diagnostics
Closes #13646.
2024-06-10 16:14:27 +02:00
Aleksei Matiushkin 615bb55e81 format_elixir: true option for file generation (#13644) 2024-06-06 18:35:54 +02:00
Jean Klingler 278cf65bb1 Revert types from #13552 (#13645) 2024-06-06 23:03:39 +09:00
Jean Klingler 125d55527d Add more information to anonymous rescue hint (#13641) 2024-06-06 21:15:48 +09:00
Travis Vander Hoop 9ffe44758c Update case.ex with correct :since for register_test/6 (#13642)
while addressing the `ExUnit.Case.register_test/3` deprecation in https://github.com/elixir-wallaby/wallaby/pull/776, I wondered how long `register_test/6` had been around. In looking at the source and the docs, I uncovered what I believe is an incorrect `since` attribute
2024-06-06 14:05:44 +02:00
Theodor Fiedler e2032e8dc4 Extend Duration shift documentation (#13640) 2024-06-06 00:05:22 +02:00
José Valim ac64fba4eb Release v1.17.0-rc.1 2024-06-03 14:40:04 +02:00
José Valim 78b7239eb7 Update CHANGELOG 2024-06-03 14:01:25 +02:00
Jean Klingler 021a698d5d Add :report option to aggregate results over multiple processes (#13632) 2024-06-03 20:48:15 +09:00
José Valim b3c3e1399d Use __info__ callback directly (#13630) 2024-06-02 16:17:27 +02:00
Łukasz Samson 18200af534 Allow to override the info callback in Macro.Env.define_import (#13628) 2024-06-02 16:17:24 +02:00
José Valim b63312ac2b Optimize :term (#13625) 2024-06-02 15:00:08 +02:00
José Valim db89cbf7e6 Avoid discarding information on conversion to #elixir_ex 2024-06-01 17:57:54 +02:00
José Valim 53c93b91da Avoid nesting of capture inside macros
Unfortunately this makes it so the unused capture
warnings emit false positives, so this particular
warning was removed.

Closes #13609.
2024-06-01 17:09:47 +02:00
José Valim a7bf120756 Fix race in registry tests from duplicate names 2024-05-31 22:37:50 +02:00
José Valim b56a907297 Type check the function type on fun.() 2024-05-30 20:37:41 +02:00
José Valim c7dfc33705 Simplify iso conversion, prefer Duration.new! 2024-05-29 00:06:37 +02:00
José Valim 4b1b852880 Remove code comment from release example 2024-05-29 00:06:34 +02:00
Tangui f9939a7029 Implement Duration.to_iso8601/1 (#13604) 2024-05-29 00:06:27 +02:00
Jean Klingler 0d6ae23e39 Add sort :per_call option to tprof (#13611) 2024-05-29 07:02:20 +09:00
Theodor Fiedler cf84b7b042 Support plus sign prefix in Calendar.ISO.parse_duration/1 (#13613) 2024-05-28 21:13:12 +02:00
Theodor Fiedler b84dd54ca1 Support negative period prefix in Calendar.ISO.parse_duration/1 (#13608) 2024-05-27 23:05:57 +02:00
Jean Klingler bb779221ff Implement mix profile.tprof (#13605) 2024-05-27 21:33:06 +02:00
Łukasz Samson be6ed1709d Keep position meta on & capture special variables in expanded AST (#13607) 2024-05-27 21:32:55 +02:00
Theodor Fiedler 79c19dcb04 Add Duration.from_iso8601/1 (#13473) 2024-05-27 11:48:07 +02:00
José Valim 772c7b0884 Add Keyword.intersect/2-3 2024-05-26 10:59:18 +02:00
José Valim 6250a17d8c Update CHANGELOG.md 2024-05-26 09:50:37 +02:00
José Valim 73d7762563 Avoid double tracing on captures 2024-05-25 11:13:29 +02:00
Yuri Pereira Constante ebe347410c Generate cover HTML files in parallel (#13596) 2024-05-24 20:27:34 +02:00
Juergen Braungardt d74e6314e4 Fix documentation of option :stderr_to_stdout for System.cmd (#13597) 2024-05-24 19:58:12 +02:00
José Valim c9a2afc0ce Use Mix.shell().error() for compiler tasks warnings 2024-05-24 19:57:24 +02:00
José Valim e4a455eedc Update CHANGELOG.md 2024-05-24 14:33:37 +02:00
José Valim a2600eace1 Release v1.17.0-rc.0 2024-05-24 12:38:20 +02:00
José Valim 27b6ea82ae Branch out v1.17 2024-05-24 12:37:32 +02:00
691 changed files with 25538 additions and 87510 deletions
+2 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
[ [
inputs: [ inputs: [
"lib/*/{lib,scripts,unicode,test}/**/*.{ex,exs}", "lib/*/{lib,scripts,unicode,test}/**/*.{ex,exs}",
@@ -21,5 +17,6 @@
# Float tests # Float tests
float_assert: 1 float_assert: 1
] ],
normalize_bitstring_modifiers: false
] ]
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
lib/elixir/test/elixir/fixtures/*.txt text eol=lf lib/elixir/test/elixir/fixtures/*.txt text eol=lf
*.ex diff=elixir *.ex diff=elixir
*.exs diff=elixir *.exs diff=elixir
-3
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
--- ---
blank_issues_enabled: true blank_issues_enabled: true
+7 -15
View File
@@ -1,9 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
--- ---
name: Report an issue name: Report an issue
description: Tell us about something that is not working the way we (probably) intend description:
Tell us about something that is not working the way we (probably) intend
body: body:
- type: markdown - type: markdown
attributes: attributes:
@@ -12,17 +10,11 @@ body:
Please, do not use this form for guidance, questions or support. Please, do not use this form for guidance, questions or support.
Try instead in [Elixir Forum](https://elixirforum.com) or any of Try instead in [Elixir Forum](https://elixirforum.com),
our online communities (Slack, Discord, etc). the [IRC Chat](https://web.libera.chat/#elixir),
[Stack Overflow](https://stackoverflow.com/questions/tagged/elixir),
- type: checkboxes [Slack](https://elixir-slackin.herokuapp.com),
id: existing-issue [Discord](https://discord.gg/elixir) or in other online communities.
attributes:
label: Existing issue
description: Please search [existing issues](https://github.com/elixir-lang/elixir/issues) before continuing.
options:
- label: I have searched existing issues and could not find a duplicate.
required: true
- type: textarea - type: textarea
id: elixir-and-otp-version id: elixir-and-otp-version
-5
View File
@@ -1,11 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
cooldown:
default-days: 7
-4
View File
@@ -1,4 +0,0 @@
* Describe here the reasons behind the pull request.
* Make sure you have read the CONTRIBUTING.md file.
* Make sure any relevant documentation and tests have been added/updated.
* Do not submit Draft pull requests unless previously asked/agreed.
+134
View File
@@ -0,0 +1,134 @@
name: builds.hex.pm
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
concurrency: builds_txt
jobs:
release_pre_built:
strategy:
fail-fast: true
max-parallel: 1
matrix:
include:
- otp: 25
otp_version: "25.3"
upload_generic_version: upload_generic_version
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- name: Get tags
run: git fetch --tags origin
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: Utils.sh
run: |
cat << 'EOF' > utils.sh
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
EOF
chmod +x utils.sh
- name: Upload Docs to S3
if: ${{ matrix.build_docs }}
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
for f in doc/*; do
if [ -d "$f" ]; then
app=`echo $f | sed s/"doc\/"//`
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge "${surrogate_key}"
fi
done
- name: Upload Precompiled to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}
fi
- name: Update builds txt
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
run: |
source utils.sh
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
ref_name=${{ github.ref_name }}
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
touch builds.txt
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
purge builds/elixir/txt
+34
View File
@@ -0,0 +1,34 @@
name: CI for Markdown content
on:
push:
branches:
- 'main'
paths:
- 'lib/**/*.md'
pull_request:
paths:
- 'lib/**/*.md'
workflow_dispatch:
jobs:
lint:
name: Lint Markdown content
strategy:
fail-fast: false
runs-on: ubuntu-20.04
steps:
- name: Check out the repository
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v16.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+47 -78
View File
@@ -1,17 +1,17 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: CI name: CI
on: on:
push: push:
paths-ignore:
- "lib/**/*.md"
pull_request: pull_request:
workflow_dispatch: paths-ignore:
- "lib/**/*.md"
env: env:
ELIXIR_ASSERT_TIMEOUT: 2000 ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors" ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8 LANG: C.UTF-8
permissions: permissions:
@@ -19,133 +19,102 @@ permissions:
jobs: jobs:
test_linux: test_linux:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }} name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- otp_version: "29.0"
deterministic: true
- otp_version: "28.4"
docs: true
coverage: true
- otp_version: "28.1"
- otp_version: "27.3"
- otp_version: "27.0" - otp_version: "27.0"
otp_latest: true
- otp_version: "26.0"
- otp_version: "25.3"
- otp_version: "25.0"
- otp_version: master - otp_version: master
development: true development: true
- otp_version: maint - otp_version: maint
development: true development: true
runs-on: ubuntu-20.04
env:
ERLC_OPTS: "warnings_as_errors"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@v4
with: with:
persist-credentials: false fetch-depth: 50
- uses: erlef/setup-beam@v1
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir - name: Compile Elixir
run: | run: |
make compile make compile
echo "$PWD/bin" >> $GITHUB_PATH echo "$PWD/bin" >> $GITHUB_PATH
- name: Build info - name: Build info
run: bin/elixir --version run: bin/elixir --version
- name: Check format - name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted." run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite - name: Erlang test suite
run: make test_erlang run: make test_erlang
continue-on-error: ${{ matrix.development == true }} continue-on-error: ${{ matrix.development }}
- name: Elixir test suite - name: Elixir test suite
run: make test_elixir run: make test_elixir
continue-on-error: ${{ matrix.development == true }} continue-on-error: ${{ matrix.development }}
env:
COVER: "${{ matrix.coverage }}"
- name: Build docs (ExDoc main) - name: Build docs (ExDoc main)
if: ${{ matrix.docs }} if: ${{ matrix.otp_latest }}
run: | run: |
cd .. cd ..
git clone https://github.com/elixir-lang/ex_doc.git --depth 1 git clone https://github.com/elixir-lang/ex_doc.git --depth 1
cd ex_doc cd ex_doc
../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile ../elixir/bin/mix do local.rebar --force + local.hex --force + deps.get + compile
cd ../elixir/ cd ../elixir/
git fetch --tags make docs
DOCS_OPTIONS="--warnings-as-errors" make docs
- name: "Calculate Coverage"
if: ${{ matrix.coverage }}
run: make cover | tee "$GITHUB_STEP_SUMMARY"
- name: "Upload Coverage Artifact"
if: ${{ matrix.coverage }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: TestCoverage
path: cover/*
- name: Check reproducible builds - name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: taskset 1 make check_reproducible
- name: Check git is not required
if: ${{ matrix.deterministic }}
run: | run: |
rm -rf .git rm -rf .git
cd lib/elixir # Recompile System without .git
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex" cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
if: ${{ matrix.otp_latest }}
test_windows: test_windows:
name: Windows Server 2022, OTP ${{ matrix.otp_version }} name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
runs-on: windows-2022
strategy: strategy:
matrix: matrix:
otp_version: otp_version: ["25.3", "26.0"]
- "29.0" runs-on: windows-2019
- "28.1"
- "27.3"
steps: steps:
- name: Configure Git - name: Configure Git
run: git config --global core.autocrlf input run: git config --global core.autocrlf input
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false fetch-depth: 50
- uses: erlef/setup-beam@v1
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with: with:
otp-version: ${{ matrix.otp_version }} otp-version: ${{ matrix.otp_version }}
- name: Compile Elixir - name: Compile Elixir
run: | run: |
Remove-Item -Recurse -Force '.git' Remove-Item -Recurse -Force '.git'
make compile make compile
- name: Build info - name: Build info
run: bin/elixir --version run: bin/elixir --version
- name: Check format - name: Check format
run: make test_formatted && echo "All Elixir source code files are properly formatted." run: make test_formatted && echo "All Elixir source code files are properly formatted."
- name: Erlang test suite - name: Erlang test suite
run: make test_erlang run: make --keep-going test_erlang
- name: Elixir test suite - name: Elixir test suite
run: | run: |
Remove-Item 'c:/Windows/System32/drivers/etc/hosts' Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
make test_elixir make --keep-going test_elixir
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
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"
-55
View File
@@ -1,55 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2026 The Elixir Team
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "29 8 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze (${{ matrix.language }})
runs-on: "ubuntu-latest"
permissions:
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: actions
build-mode: none
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:${{matrix.language}}"
zizmor:
name: Zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
-41
View File
@@ -1,41 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: License Compliance
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
license_compliance:
name: Check License Compliance
runs-on: ubuntu-24.04
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run OSS Review Toolkit
id: ort
uses: ./.github/workflows/ort
with:
upload-reports: true
fail-on-violation: true
report-formats: "WebApp"
version: "${{ github.sha }}"
-41
View File
@@ -1,41 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Markdown Content
on:
push:
branches:
- "main"
paths: &paths-filter
- "**/*.md"
- .github/workflows/markdown.yml
- .markdownlint-cli2.jsonc
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
lint:
name: Lint Markdown content
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@21c1be1b93ad9ed58fa840aacc3f279cde2a72ff # v24.2.0
+1 -5
View File
@@ -1,8 +1,4 @@
# #!/usr/bin/env elixir # #!/usr/bin/env elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
[tag] = System.argv() [tag] = System.argv()
Mix.install([ Mix.install([
@@ -74,6 +70,6 @@ unless System.get_env("DRYRUN") do
"api-username" => "Elixir" "api-username" => "Elixir"
} }
resp = Req.post!("https://forum.elixirforum.com/posts.json", {:json, post}, headers: headers) resp = Req.post!("https://elixirforum.com/posts.json", {:json, post}, headers: headers)
IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}") IO.puts("#{resp.status} Elixir Forum\n#{inspect(resp.body)}")
end end
+28
View File
@@ -0,0 +1,28 @@
name: Notify
on:
release:
types:
- published
permissions:
contents: read
jobs:
notify:
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs ${{ github.ref_name }}
-112
View File
@@ -1,112 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: "Run OSS Review Toolkit"
description: "Runs OSS Review Toolkit & generates SBoMs"
inputs:
report-formats:
description: "ORT Report Formats"
required: true
fail-on-violation:
description: "Whether to fail on violation."
required: false
default: false
upload-reports:
description: "Whether to upload all reports"
required: false
default: false
version:
description: "Elixir Version (Tag / SHA)"
required: true
outputs:
results-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-path }}"
results-sbom-cyclonedx-xml-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
results-sbom-cyclonedx-json-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
results-sbom-spdx-yml-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
results-sbom-spdx-json-path:
description: "See oss-review-toolkit/ort-ci-github-action action"
value: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
runs:
using: "composite"
steps:
- name: Fetch Default ORT Config
id: fetch-default-ort-config
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: oss-review-toolkit/ort-config
ref: "main"
path: ".ort-config"
persist-credentials: false
- name: Setup ORT Config
id: setup-ort-config
shell: bash
run: |
mkdir -p "/$HOME/.ort/"
# Move Fetched Default Config into Place
mv .ort-config "$HOME/.ort/config"
# Append Global ORT Config
cat .ort/config/config.yml >> "$HOME/.ort/config/config.yml"
# Override Default Evaluator Rules
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
# Add Package Configurations
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
for FILE in .ort/package-configurations/*.yml; do
COMPONENT="$(basename "$FILE")"
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
sed -i -E \
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
done
# Set Version in SPDX & Config
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
sed -i "s|https://github.com/elixir-lang/elixir.git|${ELIXIR_REPO}@${ELIXIR_VERSION}|" project.spdx.yml
env:
ELIXIR_VERSION: "${{ inputs.version }}"
ELIXIR_REPO: "${{ github.server_url }}/${{ github.repository }}.git"
- name: "Cache ScanCode"
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
with:
path: "~/.cache/scancode-tk"
key: ${{ runner.os }}-scancode
- name: Run OSS Review Toolkit
id: ort
uses: oss-review-toolkit/ort-ci-github-action@086d928d24ef1653dc0777296b312fda5faaaf52 # v1.2.0
with:
image: ghcr.io/oss-review-toolkit/ort:92.2.0
run: >-
labels,
cache-dependencies,
cache-scan-results,
analyzer,
scanner,
advisor,
evaluator,
reporter,
${{ inputs.upload-reports == 'true' && 'upload-results' || '' }}
fail-on: "${{ inputs.fail-on-violation == 'true' && 'violations,issues' || '' }}"
report-formats: "${{ inputs.report-formats }}"
ort-cli-report-args: >-
-O CycloneDX=output.file.formats=json,xml
-O SpdxDocument=outputFileFormats=JSON,YAML
ort-cli-scan-args: >-
--scanners Provenant
sw-version: "${{ inputs.version }}"
-53
View File
@@ -1,53 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
name: POSIX Compliance
on:
push:
paths: &paths-filter
- .github/workflows/posix_compliance.yml
- bin/elixir
- bin/elixirc
- bin/iex
pull_request:
paths: *paths-filter
workflow_dispatch:
permissions:
contents: read
env:
LANG: C.UTF-8
jobs:
check_posix_compliance:
name: Check POSIX compliance
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install ShellCheck
run: |
sudo apt update
sudo apt install -y shellcheck
- name: Run ShellCheck on bin/ dir
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"
+30 -407
View File
@@ -1,446 +1,69 @@
# SPDX-License-Identifier: Apache-2.0 name: Release
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Releases
on: on:
push: push:
branches:
- main
- v*.*
tags: tags:
- v* - v*
workflow_dispatch:
env: env:
ELIXIR_OPTS: "--warnings-as-errors" ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8 LANG: C.UTF-8
permissions: permissions:
contents: read contents: write
jobs: jobs:
create_draft_release: create_draft_release:
name: Create draft release runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
permissions:
contents: write
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps: steps:
- name: Create draft release - name: Create draft release
if: github.ref_type != 'branch'
run: | run: |
gh release create \ gh release create \
--repo "$GITHUB_REPOSITORY" \ --repo ${{ github.repository }} \
--title "$GITHUB_REF_NAME" \ --title ${{ github.ref_name }} \
--notes '' \ --notes '' \
--draft \ --draft \
"$GITHUB_REF_NAME" ${{ github.ref_name }}
release_pre_built:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 needs: create_draft_release
# zizmor: ignore[artipacked]
if: github.ref_type == 'branch'
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
run: |
ref_name="${GITHUB_REF_NAME}-latest"
if ! gh release view "$ref_name"; then
gh release create \
--latest=false \
--title "$ref_name" \
--notes "Automated release for latest ${GITHUB_REF_NAME}." \
"$ref_name"
fi
git tag "$ref_name" --force
git push origin "$ref_name" --force
build:
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
runs-on: ubuntu-24.04
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
include: include:
- otp: 25
otp_version: "25.3"
- otp: 26
otp_version: "26.0"
- otp: 27 - otp: 27
otp_version: "27.0" otp_version: "27.0"
- otp: 28
otp_version: "28.0"
build_docs: build_docs build_docs: build_docs
runs-on: ubuntu-22.04
- otp: 29
otp_version: "29.0"
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@v4
with: with:
persist-credentials: false fetch-depth: 50
- uses: ./.github/workflows/release_pre_built
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
with: with:
otp_version: ${{ matrix.otp_version }} otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }} otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }} build_docs: ${{ matrix.build_docs }}
- name: Upload Pre-built
- name: Create Docs Hashes
if: matrix.build_docs
run: |
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload Windows release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: matrix.build_docs
with:
name: Docs
path: Docs.zip*
sign:
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [27, 28, 29]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
steps:
- name: "Download build"
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3.0.1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Create Release Hashes
if: matrix.flavor == 'windows'
shell: pwsh
run: |
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
- name: Create Release Hashes
if: matrix.flavor == 'linux'
shell: bash
run: |
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload Linux release artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
sbom:
name: Generate SBoM
needs: [build, sign]
runs-on: ubuntu-24.04
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Use HTTPS instead of SSH for Git cloning
id: git-config
shell: bash
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
- name: Checkout project
id: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Download Build Artifacts"
id: download-build-artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
path: /tmp/build-artifacts/
- name: "Run OSS Review Toolkit"
id: ort
uses: ./.github/workflows/ort
with:
report-formats: "CycloneDx,SpdxDocument"
version: "${{ github.ref_type == 'tag' && github.ref_name || github.sha }}"
- name: Attest Distribution Assets with SBoM
id: attest-sbom
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: |
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
sbom-path: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Copy SBoM provenance"
id: sbom-provenance
shell: bash
run: |
mkdir attestations
for FILE in /tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}; do
cp "$ATTESTATION" "attestations/$(basename "$FILE").sigstore"
done
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_XML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_CYCLONEDX_JSON").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_YML").sigstore"
cp "$ATTESTATION" "attestations/$(basename "$SBOM_SPDX_JSON").sigstore"
env:
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
SBOM_CYCLONEDX_XML: "${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}"
SBOM_CYCLONEDX_JSON: "${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}"
SBOM_SPDX_YML: "${{ steps.ort.outputs.results-sbom-spdx-yml-path }}"
SBOM_SPDX_JSON: "${{ steps.ort.outputs.results-sbom-spdx-json-path }}"
- name: "Assemble Release SBoM Artifacts"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "SBoM"
path: |
${{ steps.ort.outputs.results-sbom-cyclonedx-xml-path }}
${{ steps.ort.outputs.results-sbom-cyclonedx-json-path }}
${{ steps.ort.outputs.results-sbom-spdx-yml-path }}
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
- name: "Assemble Distribution Attestations"
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: "Attestations"
path: "attestations/*.sigstore"
upload-release:
name: Upload release
needs: [create_draft_release, build, sign, sbom]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
merge-multiple: true
- name: Upload Pre-build
shell: bash
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
if [ "$GITHUB_REF_TYPE" == "branch" ]; then gh release upload --clobber "${{ github.ref_name }}" \
tag="${GITHUB_REF_NAME}-latest" elixir-otp-${{ matrix.otp }}.zip \
else elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
tag="$GITHUB_REF_NAME" elixir-otp-${{ matrix.otp }}.exe \
fi elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
- name: Upload Docs to GitHub
gh release upload \ if: ${{ matrix.build_docs }}
--repo "$GITHUB_REPOSITORY" \
--clobber \
"$tag" \
elixir-otp-*.zip \
elixir-otp-*.zip.sha{1,256}sum \
elixir-otp-*.zip.sigstore \
elixir-otp-*.exe \
elixir-otp-*.exe.sha{1,256}sum \
elixir-otp-*.exe.sigstore \
Docs.zip \
Docs.zip.sha{1,256}sum \
Docs.zip.sigstore \
bom.*
upload-builds-hex-pm:
name: Upload builds to hex.pm
runs-on: ubuntu-24.04
needs: [build, sign]
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Init purge keys file
run: |
touch purge_keys.txt
- name: Upload Precompiled to S3
run: |
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${GITHUB_REF_NAME}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${oldest_otp}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${GITHUB_REF_NAME}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${GITHUB_REF_NAME}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${GITHUB_REF_NAME} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo "$GITHUB_REF_NAME" | sed -e 's/^v//g')
unzip Docs.zip
for f in doc/*; do
if [ -d "$f" ]; then
app=$(echo "$f" | sed s/"doc\/"//)
tarball="${app}-${version}.tar.gz"
surrogate_key="docs/${app}-${version}"
tar -czf "${tarball}" -C "doc/${app}" .
aws s3 cp "${tarball}" "s3://${AWS_S3_BUCKET}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "${surrogate_key}" >> ../purge_keys.txt
fi
done
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name="$GITHUB_REF_NAME"
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
done
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
--cache-control "public,max-age=3600" \
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
echo 'builds/elixir/txt' >> purge_keys.txt
- name: Flush cache
if: github.repository == 'elixir-lang/elixir'
run: |
function purge_key() {
curl \
-X POST \
-H "Fastly-Key: ${FASTLY_KEY}" \
-H "Accept: application/json" \
-H "Content-Length: 0" \
"https://api.fastly.com/service/$1/purge/$2"
}
function purge() {
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
sleep 2
purge_key ${FASTLY_REPO_SERVICE_ID} $1
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
}
for key in $(cat purge_keys.txt); do
purge "${key}"
done
env: env:
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }} run: |
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }} gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
@@ -1,34 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
name: Release Notifications
on:
release:
types:
- published
permissions:
contents: read
jobs:
notify:
runs-on: ubuntu-latest
name: Notify
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
ELIXIR_LANG_ANN_TOKEN: ${{ secrets.ELIXIR_LANG_ANN_TOKEN }}
run: |
elixir .github/workflows/notify.exs "$GITHUB_REF_NAME"
+18 -30
View File
@@ -1,70 +1,57 @@
# SPDX-License-Identifier: Apache-2.0 name: "Release pre built"
# SPDX-FileCopyrightText: 2021 The Elixir Team description: "Builds elixir release, ExDoc and generates docs"
name: Release Pre-build
description: "Builds Elixir release, ExDoc and generates docs"
inputs: inputs:
otp: otp:
description: "The major OTP version" description: "The major OTP version"
otp_version: otp_version:
description: "The exact OTP version (major.minor[.patch])" description: "The exact OTP version (major.minor[.patch])"
build_docs: build_docs:
description: "Whether docs have to be built" description: "If docs have to be built or not"
runs: runs:
using: "composite" using: "composite"
steps: steps:
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2 - uses: erlef/setup-beam@v1
with: with:
otp-version: ${{ inputs.otp_version }} otp-version: ${{ inputs.otp_version }}
version-type: strict version-type: strict
- name: Build Elixir Release - name: Build Elixir Release
shell: bash shell: bash
run: | # zizmor: ignore[github-env] run: |
make Precompiled.zip make Precompiled.zip
mv Precompiled.zip "elixir-otp-${INPUT_OTP}.zip" mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH echo "$PWD/bin" >> $GITHUB_PATH
env:
INPUT_OTP: ${{ inputs.otp }}
- name: Install NSIS - name: Install NSIS
shell: bash shell: bash
run: | run: |
sudo apt update sudo apt update
sudo apt install -y nsis sudo apt install -y nsis
- name: Build Elixir Windows Installer - name: Build Elixir Windows Installer
shell: bash shell: bash
run: | run: |
export OTP_VERSION="$INPUT_OTP_VERSION" export OTP_VERSION=${{ inputs.otp_version }}
export ELIXIR_ZIP="$PWD/elixir-otp-${INPUT_OTP}.zip" export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh) (cd lib/elixir/scripts/windows_installer && ./build.sh)
mv "lib/elixir/scripts/windows_installer/tmp/elixir-otp-${INPUT_OTP}.exe" . mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
env: shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
INPUT_OTP: ${{ inputs.otp }} shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
INPUT_OTP_VERSION: ${{ inputs.otp_version }}
- name: Get ExDoc ref - name: Get ExDoc ref
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
run: | # zizmor: ignore[github-env] run: |
if [ "$GITHUB_REF_NAME" = "main" ]; then if [ "${{ github.ref_name }}" = "main" ]; then
ref=main ref=main
else else
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version') ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
fi fi
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@v3
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
with: with:
repository: elixir-lang/ex_doc repository: elixir-lang/ex_doc
ref: ${{ env.EX_DOC_REF }} ref: ${{ env.EX_DOC_REF }}
path: ex_doc path: ex_doc
persist-credentials: false
- name: Build ex_doc - name: Build ex_doc
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
@@ -77,5 +64,6 @@ runs:
if: ${{ inputs.build_docs }} if: ${{ inputs.build_docs }}
shell: bash shell: bash
run: | run: |
git fetch --tags
make Docs.zip make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
+2 -8
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
/doc/ /doc/
/lib/*/ebin/ /lib/*/ebin/
/lib/*/_build/ /lib/*/_build/
@@ -10,10 +6,8 @@
/lib/elixir/test/ebin/ /lib/elixir/test/ebin/
/man/elixir.1 /man/elixir.1
/man/iex.1 /man/iex.1
/Docs.zip /Docs-v*.zip
/Precompiled.zip /Precompiled-v*.zip
/.eunit /.eunit
.elixir.plt .elixir.plt
erl_crash.dump erl_crash.dump
/cover/
.tool-versions
-63
View File
@@ -1,63 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Elixir Team
{
"globs": [
"**/*.md"
],
"ignores": [
".git/**",
".github/**"
],
"gitignore": true,
"config": {
// Consecutive header levels (h1 -> h2 -> h3).
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content.
"MD024": {
// Duplication is allowed for headings with different parents.
"siblings_only": true
},
// Trailing punctuation in heading.
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
// the default values.
"MD026": {
"punctuation": ".,;:。,;:!"
},
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false,
// Our tables are too large to align.
"MD060": false
}
}
+42
View File
@@ -0,0 +1,42 @@
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false
}
-123
View File
@@ -1,123 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
excludes:
paths:
- pattern: "man/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: ".github/**/*"
reason: "BUILD_TOOL_OF"
comment: "Documentation"
- pattern: ".ort/**/*"
reason: "BUILD_TOOL_OF"
comment: "Documentation"
# Unfortunately we'll have to repeat all package level excludes here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
- pattern: "lib/*/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/*/test/**/*"
reason: "TEST_OF"
comment: "Tests"
- pattern: "lib/*/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/*/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
curations:
license_findings:
# Version File
- path: "VERSION"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to VERSION file"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".github/pull_request_template.md"
reason: "NOT_DETECTED"
comment: "Apply default license to GitHub pull request template"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Wrongly Identified
- path: ".gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: ".gitattributes"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
- path: "CONTRIBUTING.md"
reason: "INCORRECT"
comment: "Wrongly identified TSL license"
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
concluded_license: "Apache-2.0"
- path: "OPEN_SOURCE_POLICY.md"
reason: "INCORRECT"
comment: "Wrongly identified NOASSERTION"
detected_license: "NOASSERTION"
concluded_license: "Apache-2.0"
# Unfortunately we'll have to repeat all package level license curations here
# Make sure to keep them in sync with the package configuration in
# .ort/package-configurations
# Test Fixtures
- path: "lib/*/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-21
View File
@@ -1,21 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
ort:
enableRepositoryPackageCurations: true
enableRepositoryPackageConfigurations: true
scanner:
skipConcluded: false
includeFilesWithoutFindings: true
analyzer:
allowDynamicVersions: true
enabledPackageManagers: [SpdxDocumentFile]
reporter:
reporters:
SpdxDocument:
options:
creationInfoOrganization: The Elixir Team
documentName: "Elixir Source SPDX Document"
-88
View File
@@ -1,88 +0,0 @@
/*
* Copyright (C) 2019 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
* Copyright (c) 2021 The Elixir Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
// Docs: https://oss-review-toolkit.org/ort/docs/configuration/evaluator-rules
val whitelistedLicenses = listOf(
// License for Elixir & Imported Erlang Projects
"Apache-2.0",
// License for the Elixir Logo
"LicenseRef-elixir-trademark-policy",
"LicenseRef-scancode-elixir-trademark-policy",
// License for included Unicode Files
"LicenseRef-scancode-unicode",
// DCO for committers
"LicenseRef-scancode-dco-1.1"
).map { SpdxSingleLicenseExpression.parse(it) }.toSet()
fun PackageRule.howToFixDefault() = """
* Check if this license violation is intended
* Adjust evaluation rules in `.ort/config/evaluator.rules.kts`
""".trimIndent()
fun PackageRule.LicenseRule.isHandled() =
object : RuleMatcher {
override val description = "isHandled($license)"
override fun matches() = license in whitelistedLicenses
}
fun RuleSet.unhandledLicenseRule() = packageRule("UNHANDLED_LICENSE") {
// Do not trigger this rule on packages that have been excluded in the .ort.yml.
require {
-isExcluded()
}
// Define a rule that is executed for each license of the package.
licenseRule("UNHANDLED_LICENSE", LicenseView.CONCLUDED_OR_DECLARED_AND_DETECTED) {
require {
-isExcluded()
-isHandled()
}
// Throw an error message including guidance how to fix the issue.
error(
"The license $license is currently not covered by policy rules. " +
"The license was ${licenseSource.name.lowercase()} in package " +
"${pkg.metadata.id.toCoordinates()}.",
howToFixDefault()
)
}
}
fun RuleSet.unmappedDeclaredLicenseRule() = packageRule("UNMAPPED_DECLARED_LICENSE") {
require {
-isExcluded()
}
resolvedLicenseInfo.licenseInfo.declaredLicenseInfo.processed.unmapped.forEach { unmappedLicense ->
warning(
"The declared license '$unmappedLicense' could not be mapped to a valid license or parsed as an SPDX " +
"expression. The license was found in package ${pkg.metadata.id.toCoordinates()}.",
howToFixDefault()
)
}
}
val ruleSet = ruleSet(ortResult, licenseInfoResolver, resolutionProvider) {
unhandledLicenseRule()
unmappedDeclaredLicenseRule()
}
ruleViolations += ruleSet.violations
-15
View File
@@ -1,15 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:eex:"
path_excludes:
- pattern: "lib/eex/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/eex/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-60
View File
@@ -1,60 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:elixir:"
path_excludes:
- pattern: "lib/elixir/pages/**/*"
reason: "DOCUMENTATION_OF"
comment: "Documentation"
- pattern: "lib/elixir/scripts/**/*"
reason: "BUILD_TOOL_OF"
comment: "Build Tool"
- pattern: "lib/elixir/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Logos
- path: "lib/elixir/pages/images/logo.png"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
reason: "NOT_DETECTED"
comment: "Apply Trademark Policy to Elixir Logo"
detected_license: "NONE"
concluded_license: "LicenseRef-elixir-trademark-policy"
# Documentation Images
- path: "lib/elixir/pages/images/**/*.png"
reason: "NOT_DETECTED"
comment: "Apply default license to all images"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Test Fixtures
- path: "lib/elixir/test/elixir/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
# Unicode
- path: "lib/elixir/unicode/*.txt"
reason: "NOT_DETECTED"
comment: "Apply default license to unicode files"
detected_license: "NONE"
concluded_license: "LicenseRef-scancode-unicode"
# Wrongly Identified
- path: "lib/elixir/pages/references/library-guidelines.md"
reason: "INCORRECT"
comment: |
The guide mentions multiple licenses for users to choose from.
It however is not licensed itself by the mentioned licenses.
concluded_license: "Apache-2.0"
- path: "lib/elixir/scripts/windows_installer/.gitignore"
reason: "INCORRECT"
comment: "Ignored by ScanCode"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-18
View File
@@ -1,18 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:exunit:"
path_excludes:
- pattern: "lib/ex_unit/examples/**/*"
reason: "EXAMPLE_OF"
comment: "Example"
- pattern: "lib/ex_unit/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/ex_unit/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-8
View File
@@ -1,8 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:logger:"
path_excludes:
- pattern: "lib/logger/test/**/*"
reason: "TEST_OF"
comment: "Tests"
-15
View File
@@ -1,15 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
id: "SpdxDocumentFile:The Elixir Team:mix:"
path_excludes:
- pattern: "lib/mix/test/**/*"
reason: "TEST_OF"
comment: "Tests"
license_finding_curations:
# Test Fixtures
- path: "lib/mix/test/fixtures/**/*"
reason: "NOT_DETECTED"
comment: "Apply default license to test fixtures"
detected_license: "NONE"
concluded_license: "Apache-2.0"
-1
View File
@@ -1 +0,0 @@
CONTRIBUTING.md
+201 -79
View File
@@ -1,111 +1,233 @@
<!-- # Changelog for Elixir v1.17
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Changelog for Elixir v1.21 This release includes type inference of patterns to provide warnings for an initial set of constructs (binaries, maps, and atoms) within the same function. It also includes a new Duration data type to interact with Calendar types, support for Erlang/OTP 27, and many other improvements.
## v1.21.0-dev ## Warnings from gradual set-theoretic types
### 1. Enhancements This release introduces gradual set-theoretic types to infer types from patterns and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
#### EEx At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
* [EEx] Support splitting middle expressions across EEx clauses * `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type.
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
We focused on atoms and maps on this initial release as they are respectively the simplest and the most complex types representations, so we can stress the performance of the type system and quality of error messages. Modelling these types will also provide the most immediate benefits to Elixir developers. Assuming there is a variable named `user`, holding a `%User{}` struct with an `address` field, Elixir v1.17 will emit the following warnings at compile-time:
* Pattern matching against a map or a struct that does not have the given key, such as `%{adress: ...} = user` (notice `address` vs `adress`)
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0`
* Attempting to invoke to call an anonymous function without an actual function, such as `user.()`
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
* Performing structural comparisons between non-overlapping types, such as `integer >= string`
* Building and pattern matching on binaries without the relevant specifiers, such as `<<name>>` (this warns because by default it expects an integer, it should have been `<<name::binary>>` instead)
* Attempting to rescue an undefined exception or a struct that is not an exception
* Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
## Erlang/OTP support
This release adds support for Erlang/OTP 27 and drops support for Erlang/OTP 24. We recommend Elixir developers to migrate to Erlang/OTP 26 or later, especially on Windows. Support for WERL (a graphical user interface for the Erlang terminal on Windows) will be removed in Elixir v1.18.
## Adding `Duration` and `shift/2` functions
Elixir introduces the `Duration` data type and APIs to shift dates, times, and date times by a given duration, considering different calendars and time zones.
```elixir
iex> Date.shift(~D[2016-01-31], month: 2)
~D[2016-03-31]
```
Note the operation is called `shift` (instead of `add`) since working with durations does not obey properties such as associativity. For instance, adding one month and then one month does not give the same result as adding two months:
```elixir
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
```
Still, durations are essential for building intervals, recurring events, and modelling scheduling complexities found in the world around us. For `DateTime`s, Elixir will correctly deal with time zone changes (such as Daylight Saving Time), but provisions are also available in case you want to surface conflicts (for example, you shifted to a wall clock that does not exist, because the clock has been moved forward by one hour). See `DateTime.shift/2` for examples.
Finally, a new `Kernel.to_timeout/1` function has been added, which helps developers normalize durations and integers to a timeout used by Process APIs. For example, to send a message after one hour, one can now write:
```elixir
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
```
## v1.17.3 (2024-09-18)
### 1. Bug fixes
#### Elixir #### Elixir
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1` * [Duration] Fix parsing of fractional durations with non-positive seconds
* [Code] Add support for the `:erlc_options` compiler option * [Kernel] Do not attempt to group module warnings when they have a large context
* [Code.Formatter] Add a `:migrate_atom_interpolations` option
* [Kernel] Improve performance of type constructors and complex intersections #### IEx
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
* [Kernel.ParallelCompiler] Add a hint when spawned processes cannot load modules defined during compilation * [IEx.Helpers] Properly reconsolidate protocols on `recompile`
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1` #### Mix
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
* [Path] Add `Path.safe_join/2` * [mix compile.elixir] Do not verify modules twice
* [Registry] Optimize exact key matching in lookups * [mix xref] Respect the `--label` option on stats and cycles
* [String] Optimize `String.bag_distance/2`
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1` ## v1.17.2 (2024-07-06)
* [URI] Optimize percent-decoding and `URI.to_string/1`
### 1. Bug fixes
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
## v1.17.1 (2024-06-18)
### 1. Enhancements
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
### 2. Bug fixes
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit #### ExUnit
* [ExUnit.Assertions] Add `trace/3` helper * [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements
#### Elixir
* [Access] Add `Access.find/1` that mirrors `Enum.find/2`
* [Code] Support cursor inside fn/rescue/catch/else/after inside `Code.Fragment.container_cursor_to_quoted/2`
* [Date] Add `Date.shift/2` to shift dates with duration and calendar-specific semantics
* [Date] Allow `Date` to accept years outside of `-9999..9999` range
* [DateTime] Add `DateTime.shift/2` to shift datetimes with duration and calendar-specific semantics
* [Duration] Add a new `Duration` data type
* [GenServer] Add `c:GenServer.format_status/1` callback
* [Kernel] Add `Kernel.get_in/1` with safe nil-handling for access and structs
* [Kernel] Add `Kernel.is_non_struct_map/1` guard
* [Kernel] Add `Kernel.to_timeout/1`
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
* [Kernel] Support integers in uppercase sigils
* [Keyword] Add `Keyword.intersect/2-3` to mirror the `Map` API
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
* [Process] Add `Process.set_label/1`
* [String] Add `String.byte_slice/3` to slice a string to a maximum number of bytes while keeping it UTF-8 encoded
* [System] Support `use_stdio: false` in `System.cmd/3` and `System.shell/2`
* [Time] Add `Time.shift/2` to shift times with duration and calendar-specific semantics
#### ExUnit
* [ExUnit] Propagate the test process itself as a caller in `start_supervised`
* [ExUnit] Include max cases in ExUnit reports
#### IEx
* [IEx.Helpers] Warns if `recompile` was called and the current working directory changed
* [IEx.Helpers] Add `c/0` as an alias to `continue/0`
* [IEx.Pry] Add `IEx.Pry.annotate_quoted/3` to annotate a quoted expression with pry breakpoints
#### Logger
* [Logger] Format `:gen_statem` reports using Elixir data structures
* [Logger] Include process label in logger events
#### Mix
* [mix deps] Add `:depth` option to `Mix.SCM.Git`, thus supporting shallow clones of Git dependencies
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
* [mix profile.tprof] Add a new profiler, available on Erlang/OTP 27+, which can measure count, time, and heap usage
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
### 2. Bug fixes ### 2. Bug fixes
#### Elixir #### Elixir
* [Calendar] Fix `Calendar.strftime/3` formatting of negative years with `%y` * [bin/elixir.bat] Improve handling of quotes and exclamation marks in flags
* [Calendar] Fix rounding for `:day`, `:hour`, and `:minute` units in `DateTime.diff/3`, `NaiveDateTime.diff/3`, and `Time.diff/3` * [Code] Address a bug where AST nodes for `(a -> b)` were not wrapped as part of the literal encoder
* [Calendar.ISO] Fix `Calendar.ISO.valid_time?/4` to reject non-integer microsecond precision * [Kernel] Resolve inconsistencies of how `..` and `...` are handled at the AST level
* [Calendar.ISO] Reject negative zero UTC offsets in basic formats * [Kernel] Fix parsing precedence of ambiguous operators followed by containers
* [Code.Formatter] Fix rendering calls where `do` is followed by non-block keyword arguments * [Kernel] Do not expand code in `quote bind_quoted: ...` twice
* [Code.Fragment] Fix cursor completion when operator keywords such as `in`, `when`, `and`, `or`, and `not` follow another operator * [Kernel] Respect `:line` property when `:file` is given as option to `quote`
* [Date] Preserve the `:format` option in `Date.to_iso8601/2` with custom calendars * [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
* [Date.Range] Fix slicing date ranges with stepped ranges * [Kernel] Avoid double tracing events when capturing a function
* [Duration] Reject duplicate seconds in `Duration.from_iso8601/1` * [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Enum] Fix `Enum.min/2,3` and `Enum.max/2,3` with custom sorters on ranges * [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [IO.ANSI.Docs] Recognize additional punctuation delimiters when rendering Markdown * [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
* [Kernel] Expand `defguard` macros separately in guard and body contexts, preserving `and`/`or` error semantics outside guards
* [Kernel] Fix inferred stacktrace types to allow arbitrary keyword metadata
* [Kernel] Fix inferred types for functions with non-returning clauses
* [Kernel] Fix map field type inference in the presence of empty map types
* [Kernel] Fix tuple fetch and deletion type operations across equivalent tuple types
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
* [Kernel] Improve the error message for non-atom struct keys
* [Kernel] Raise when `|` is used in guards
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
* [Keyword] Delete duplicate keys when `Keyword.get_and_update/3` and `Keyword.get_and_update!/3` return `:pop`
* [Macro] Properly escape C1 control characters and Unicode noncharacters
* [NaiveDateTime] Fix `NaiveDateTime.diff/3` over-counting incomplete units
* [Range] Fix `Range.disjoint?/2` for ranges beyond floating-point precision
* [Range] Fix `Range.disjoint?/2` for single-element ranges with a negative step
* [String] Fix `String.reverse/1` grapheme ordering around invalid UTF-8 bytes
* [String] Return `1.0` from `String.bag_distance/2` for two empty strings
* [Time] Validate microseconds in `Time.from_seconds_after_midnight/3`
#### ExUnit
* [ExUnit.Assertions] Fix `refute_in_delta/4` at the delta boundary and with negative deltas
* [ExUnit.CaptureIO] Stop `StringIO` processes when capturing a named device fails
#### IEx #### IEx
* [IEx.Autocomplete] Fix completion crashes on maps with non-atom keys * [IEx.Helpers] Update the history size whenever history is pruned
* [IEx.Evaluator] Recognize `**` and `not in` as continuation operators
* [IEx.Helpers] Fix `r/1` when multiple modules are defined in the same file
* [IEx.Helpers] Fix heap and stack memory calculations in `process_info/1`
#### Mix #### Mix
* [Mix] Prevent synchronization lock files from being overwritten with empty contents * [mix deps] Fix error message for diverged SCM definition in sibling
* [Mix.Release] Accept chardata paths in `Mix.Release.make_boot_script/4`
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
* [mix deps] Recompile path and fetchable dependencies when one of the dependencies they were compiled with is removed
* [mix deps] Mark fetchable dependencies for compilation when their build exists but their SCM manifest is missing
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
* [mix format] Pass `:sigils` to plugins invoked for sigils, allowing nested sigils to be formatted
* [mix new] Avoid trailing whitespace in generated files
### 3. Hard deprecations ### 3. Soft deprecations (no warnings emitted)
#### Elixir #### Elixir
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead * [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
### 4. Soft deprecations #### Mix
* [mix profile.cprof] Deprecated in favor of the new `mix profile.tprof`
* [mix profile.eprof] Deprecated in favor of the new `mix profile.tprof`
### 4. Hard deprecations
#### Elixir #### Elixir
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1` * [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1` * [Kernel] Single-quote charlists are deprecated, use `~c` instead
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1` * [Kernel] Deprecate escaping closing delimiter in uppercase sigils
* [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead
* [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead
## v1.20 #### ExUnit
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md). * [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons
## v1.16
The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md).
+5 -11
View File
@@ -1,12 +1,6 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Code of Conduct # Code of Conduct
Contact: <elixir-lang-conduct@googlegroups.com> Contact: elixir-lang-conduct@googlegroups.com
## Why have a Code of Conduct? ## Why have a Code of Conduct?
@@ -51,15 +45,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project: Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
* The [official GitHub projects][1] and code reviews. * The [official GitHub projects][1] and code reviews.
* The official elixir-lang mailing lists. * The official elixir-lang mailing lists.
* The **[#elixir][2]** IRC channel on [Libera.Chat][3]. * The **[#elixir][2]** IRC channel on [Libera.Chat][3].
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information. Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group. **The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
-244
View File
@@ -1,244 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Contributing to Elixir
We invite contributions to Elixir. To contribute, there are a few
things you need to know about the code. First, Elixir code is divided
by each application inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
Some test files need their `test_helper.exs` to be explicitly required
before, such as:
```sh
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile all (including Erlang modules):
```sh
make compile
```
After your changes are done, run `make format` to guarantee
all files are properly formatted, then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
you can rebuild the language from scratch with:
```sh
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
## Sending a pull request
Contributions are done [via pull request](https://help.github.com/articles/using-pull-requests/)
and must include tests and other relevant proof of work:
* **Bug Fixes:** If you are fixing a bug, include a test that *fails* before
your change and *passes* afterward. This makes it easier to confirm that the
fix addresses the underlying issue and helps prevent regressions in the future.
* **New Features or Major Changes:** If you are adding a new feature or making
major changes to existing functionality, please add assocaited tests. Aim to
have the best code coverage possible.
* **Performance improvements:** For performance improvements, please include the
benchmark script, with inputs and results, in the pull request description.
We recommend using [benchee](https://github.com/bencheeorg/benchee).
Here are some pull requests we have received in the past you can use as reference:
* [Implement Enum.member?](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid?](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit](https://github.com/elixir-lang/elixir/pull/1059)
## Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
If changes are necessary, the team will leave appropriate comments requesting
changes to the code. Unfortunately, we cannot guarantee a pull request will
be merged, even when modifications are requested, as the Elixir team will
re-evaluate the contribution as it changes.
When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is cloned and compiled alongside Elixir.
After cloning and compiling Elixir, run:
```sh
elixir_dir=$(pwd)
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
# Now we will go back to Elixir's root directory,
cd "${elixir_dir}"
# and generate HTML and EPUB documents:
make docs
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
## Licensing and Compliance Requirements
Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
guidelines on licensing and compliance. Below is a summary of the key points
affecting **all external contributors**:
* Accepted Licenses: Any code contributed must be licensed under the
`Apache-2.0` license.
* SPDX License Headers: With the exception of approved test fixture files,
all new or modified files in a pull request must include correct SPDX
headers. If you are creating a new file under the `Apache-2.0` license, for
instance, please use:
```elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
```
* No Executable Binaries: Contributions must **not** include any executable
binary files. If you require an exception (for example, certain test artifacts),
please see the policy on how to request approval and document exceptions.
* Preserving Copyright and License Info: If you copy code from elsewhere,
ensure that **all original copyright and license notices remain intact**. If
they are missing or incomplete, you must add them.
* Failure to Comply: Pull requests that do not meet these licensing and
compliance standards will be rejected or require modifications before merging.
* Developer Certificate of Origin: All contributions are subject to the
Developer Certificate of Origin.
```text
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the
best of my knowledge, is covered under an appropriate open
source license and I have the right under that license to
submit that work with modifications, whether created in whole
or in part by me, under the same open source license (unless
I am permitted to submit under a different license), as
Indicated in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including
all personal information I submit with it, including my
sign-off) is maintained indefinitely and may be redistributed
consistent with this project or the open source license(s)
involved.
```
See <https://developercertificate.org/> for a copy of the Developer Certificate
of Origin license.
## Using AI and coding agents
While we allow the use of AI on contributions and discussions, please be mindful
when doing so. Generally speaking, Elixir maintainers already have access to AI
(like many other developers). Therefore, if we need the feedback or help of a
coding agent, we can request so ourselves. For this reason, we often find
the point of view of the human behind the agent more valuable.
That said, here are examples of how one might (or might not) use AI and coding
agents in Elixir spaces:
* When it comes to discussions, using AI to help express yourself is welcome,
but avoid directly copy and pasting AI generated content. If there is a language
barrier, use AI to translate, review, and improve your text, but do not use AI
to respond on your behalf.
* Do not use coding agents to tackle existing issues unless they have the
"Contributions Welcome" label.
* If you request a feature on the mailing list and it is accepted, you may
use coding agents to implement it, as long as it follows the AI Contributions
guidelines below.
* When automating AI usage on the Elixir codebase for performance improvements,
security fixes, or correctness changes to the compiler or type system, pair it
with a separate set of agents whose job is to argue against and try to invalidate
any proposed change. And treat their approval as advisory: a human must still
validate it before opening issues or pull requests.
If any code is written by AI, then you must follow the guidelines below.
### AI contributions
AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer
Certificate of Origin (DCO). The human submitter is responsible for:
* Reviewing all AI-generated code
* Ensuring compliance with licensing requirements
* Adding their own Signed-off-by tag to certify the DCO
* Taking full responsibility for the contribution
* Disclosing use of AI for comments and code contributions
When AI tools contribute to Elixir, proper attribution helps track the evolving role of
AI in the development process. Contributions should include an Assisted-by tag in the
following format:
Assisted-by: AGENT_NAME:MODEL_VERSION
-73
View File
@@ -1,73 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,98 +0,0 @@
ELIXIR TEAM TRADEMARKS POLICY
This document outlines the policy for allowed usage of the “Elixir” word and the
Elixir logo by other parties.
“Elixir” and the Elixir logo are registered trademarks of the Elixir Team. The
Elixir Team believes in a decentralized approach to growing the community and
the ecosystem, independent of the Elixir project and the Elixir Team.
Anyone can use the Elixir trademarks if that use of the trademark is nominative.
The trademarks must not be used to disparage the project and its community, nor
be used in any way to imply ownership, endorsement, or association with the
Elixir project and the Elixir Team.
You must not visually combine the Elixir logo with any other images, or change
the logo in any way other than ways required by printing restrictions. If you
want to create your own visual identity in relation to Elixir, you might use the
shape of an unrelated “water drop” as part of your design, as seen in many
community projects and initiatives. You must not combine or modify the Elixir
logo.
The Elixir logo is available in our repository in both vertical and horizontal
versions.
Nominative use
The “nominative use” (or “nominative fair use”) is a legal doctrine that
authorizes everyone (even commercial companies) to use or refer to the trademark
of another if:
The product or service in question must be one not readily identifiable without
use of the trademark.
Only so much of the mark or marks may be used as is reasonably necessary to
identify the product or service.
The organization using the mark must do nothing that would, in conjunction with
the mark, suggest sponsorship or endorsement by the trademark holder.
Our trademarks must be used to refer to the Elixir programming language.
Examples of permitted use
All examples listed next must strictly adhere to the terms outlined in the
previous sections:
Usage of the Elixir logo to say a technology is “powered by Elixir” under
nominative use. Linking back to the Elixir website, if possible, is appreciated.
Usage of the Elixir logo to display it as a supported technology in a service or
platform. For instance, you may say “we support Elixir” and use the Elixir logo,
but you may not refer to yourself as “the Elixir platform” nor imply any form of
endorsement or association with Elixir.
Usage of the Elixir logo in non-commercial community meetups, in presentations,
and in courses when referring to the language and its ecosystem under nominative
use.
Usage of the Elixir logo in non-commercial swag (stickers, t-shirts, mugs, etc)
to promote the Elixir programming language. The Elixir marks must be the only
marks featured in the product. You need permission to make swag that include
Elixir and other third party marks in them.
Inclusion of the Elixir logo in non-commercial icon sets. Use of the Elixir
icons must still adhere to Elixir’s trademark policies.
Usage of the “Elixir” word in book titles, meetups, conferences, and podcasts.
You must not use the word to imply uniqueness or endorsement from the Elixir
team. “The Elixir book” and “The Elixir podcast” are not permitted.
“Elixir in Action”, “Thinking Elixir”, and “Kraków Elixir User Group” are valid
examples already in use today.
Usage of the “Elixir” word in the names of freely distributed software and
hardware products is allowed when referring to use with or suitability for the
Elixir programming language, such as wxElixir, Elixirsense, etc. If the product
includes the Elixir programming language itself, then you must also respect its
license.
Examples of not permitted use
Here is a non-exhaustive list of non permitted uses of the marks:
Usage of the Elixir logo in book covers, conferences, and podcasts.
Usage of the Elixir logo as the mark of third party projects, even in combination
with other marks.
Naming any company or product after Elixir, such as “The Elixir Hosting”,
“The Elixir Consultants”, etc.
Examples that require permission
Here are some examples that may be granted permission upon request:
Selling merchandise (stickers, t-shirts, mugs, etc).
You can request permission by emailing trademarks@elixir-lang.org.
Important note
Nothing in this page shall be interpreted to allow any third party to claim any
association with the Elixir project and the Elixir Team, or to imply any
approval or support by the Elixir project and the Elixir Team for any third
party products, services, or events.
-58
View File
@@ -1,58 +0,0 @@
UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE
Unicode Data Files include all data files under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
http://www.unicode.org/cldr/data/ . Unicode Software includes any source
code published in the Unicode Standard or under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/, and
http://www.unicode.org/cldr/data/.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA
FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY
ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS
AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE
OR USE THE DATA FILES OR SOFTWARE.
COPYRIGHT AND PERMISSION NOTICE
Copyright © Unicode, Inc. All rights reserved. Distributed under
the Terms of Use in http://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Unicode data files and any associated documentation (the
"Data Files") or Unicode software and any associated documentation (the
"Software") to deal in the Data Files or Software without restriction,
including without limitation the rights to use, copy, modify, merge,
publish, distribute, and/or sell copies of the Data Files or Software,
and to permit persons to whom the Data Files or Software are furnished
to do so, provided that
(a) the above copyright notice(s) and this permission notice appear with
all copies of the Data Files or Software,
(b) both the above copyright notice(s) and this permission notice appear
in associated documentation, and
(c) there is clear notice in each modified Data File or in the Software
as well as in the documentation associated with the Data File(s) or
Software that the data or software has been modified.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR
ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or
other dealings in these Data Files or Software without prior written
authorization of the copyright holder.
Unicode and the Unicode logo are trademarks of Unicode, Inc., and may be
registered in some jurisdictions. All other trademarks and registered
trademarks mentioned herein are the property of their respective owners.
+42 -60
View File
@@ -1,16 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
PREFIX ?= /usr/local PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs" TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/ # CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
VERSION := $(strip $(shell cat VERSION)) VERSION := $(strip $(shell cat VERSION))
@@ -22,19 +17,19 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644 INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755 INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null )) GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1)) GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: cover install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips .PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.NOTPARALLEL: .NOTPARALLEL:
#==> Functions #==> Functions
define CHECK_ERLANG_RELEASE define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 27)])' -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 >= 25)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \ if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 27.0 is required to build Elixir"; \ echo "At least Erlang/OTP 25.0 is required to build Elixir"; \
exit 1; \ exit 1; \
fi fi
endef endef
@@ -53,10 +48,6 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
test_$(1): test_formatted $(1) test_$(1): test_formatted $(1)
@ echo "==> $(1) (ex_unit)" @ echo "==> $(1) (ex_unit)"
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)"; $(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
cover/ex_unit_$(1).coverdata:
$(Q) COVER="1" $(MAKE) test_$(1)
cover/combined.coverdata: cover/ex_unit_$(1).coverdata
endef endef
define WRITE_SOURCE_DATE_EPOCH define WRITE_SOURCE_DATE_EPOCH
@@ -74,21 +65,17 @@ endef
#==> Compilation tasks #==> Compilation tasks
APP := lib/elixir/ebin/elixir.app APP := lib/elixir/ebin/elixir.app
EEX := lib/eex/ebin/Elixir.EEx.beam
ELIXIR := lib/elixir/ebin/elixir.beam
PARSER := lib/elixir/src/elixir_parser.erl PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile default: compile
compile: erlang elixir compile: erlang $(APP) elixir
erlang: $(ELIXIR) erlang: $(PARSER)
$(ELIXIR): $(PARSER) lib/elixir/src/*
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi $(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)
$(Q) $(GENERATE_APP) $(VERSION)
$(PARSER): lib/elixir/src/elixir_parser.yrl $(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $< $(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -96,29 +83,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
# Since Mix depends on EEx and EEx depends on Mix, # Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file, # we first compile EEx without the .app file,
# then Mix, and then compile EEx fully # then Mix, and then compile EEx fully
elixir: stdlib $(EEX) mix ex_unit logger eex iex elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \ $(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \ echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \ $(ERL) -s elixir_compiler bootstrap -s erlang halt; \
"$(MAKE)" unicode; \
fi fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)"; @ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin; $(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) $(GENERATE_APP) $(VERSION) $(Q) "$(MAKE)" app
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP) app: $(APP)
$(Q) $(GENERATE_APP) $(VERSION) $(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
unicode: $(UNICODE) unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/* $(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)"; @ echo "==> unicode (compile)";
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin; $(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin; $(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC_MIN_SIG) lib/elixir/unicode/security.ex -o lib/elixir/ebin; $(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit)) $(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger)) $(eval $(call APP_TEMPLATE,logger,Logger))
@@ -157,7 +144,6 @@ check_reproducible: compile
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/ $(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/ $(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/ $(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
$(Q) rm -rf lib/*/ebin
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
$(Q) echo "Diffing..." $(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/ $(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
@@ -168,10 +154,16 @@ check_reproducible: compile
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/ $(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible" $(Q) echo "Builds are reproducible"
clean: clean_man clean:
rm -rf ebin rm -rf ebin
rm -rf lib/*/ebin rm -rf lib/*/ebin
rm -rf $(PARSER) rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/ rm -rf lib/*/_build/
rm -rf lib/*/tmp/ rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/ rm -rf lib/elixir/test/ebin/
@@ -181,15 +173,12 @@ clean: clean_man
rm -rf lib/mix/test/fixtures/git_sparse_repo/ rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/ rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump rm -f erl_crash.dump
rm -rf cover $(Q) "$(MAKE)" clean_man
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
#==> Documentation tasks #==> Documentation tasks
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}") SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4) DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)" DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
@@ -231,19 +220,19 @@ docs_logger: compile ../ex_doc/bin/ex_doc
$(call DOCS_CONFIG,logger) $(call DOCS_CONFIG,logger)
../ex_doc/bin/ex_doc: ../ex_doc/bin/ex_doc:
@ echo "ex_doc is not found in ../ex_doc as expected. See CONTRIBUTING.md for more information." @ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@ false @ false
#==> Zip tasks #==> Zip tasks
Docs.zip: docs Docs.zip: docs
rm -f Docs.zip rm -f Docs.zip
zip -9 -r Docs.zip CHANGELOG.md doc LICENSE README.md zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
@ echo "Docs file created $(CURDIR)/Docs.zip" @ echo "Docs file created $(CURDIR)/Docs.zip"
Precompiled.zip: build_man compile Precompiled.zip: build_man compile
rm -f Precompiled.zip rm -f Precompiled.zip
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man README.md VERSION zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE Makefile man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip" @ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
#==> Test tasks #==> Test tasks
@@ -294,15 +283,6 @@ test_stdlib: compile
cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \ cd lib/elixir && ../../bin/elixir --sname primary -r "test/elixir/test_helper.exs" -pr "test/elixir/**/$(TEST_FILES)"; \
fi fi
cover/ex_unit_elixir.coverdata:
$(Q) COVER="1" $(MAKE) test_stdlib
cover/combined.coverdata: cover/ex_unit_elixir.coverdata
cover/combined.coverdata:
bin/elixir ./lib/elixir/scripts/cover.exs
cover: cover/combined.coverdata
#==> Dialyzer tasks #==> Dialyzer tasks
DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs DIALYZER_OPTS = --no_check_plt --fullpath -Werror_handling -Wunmatched_returns -Wunderspecs
@@ -325,15 +305,17 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1 build_man: man/iex.1 man/elixir.1
define BUILD_MANPAGES man/iex.1:
man/$(APP).1: $(Q) cp man/iex.1.in man/iex.1
$(Q) cp man/$(APP).1.in man/$(APP).1 $(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1 $(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1 $(Q) rm -f man/iex.1.bak
$(Q) rm -f man/$(APP).1.bak
endef
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES))) man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
clean_man: clean_man:
rm -f man/elixir.1 rm -f man/elixir.1
+37
View File
@@ -0,0 +1,37 @@
LEGAL NOTICE INFORMATION
------------------------
All the files in this distribution are copyright to the terms below.
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
Copyright Ericsson AB 1996-2015
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
== All other files
Copyright 2012 Plataformatec
Copyright 2021 The Elixir Team
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-165
View File
@@ -1,165 +0,0 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
-->
# Open Source Policy
## 1. Introduction
This Open Source Policy outlines the licensing, contribution, and compliance
requirements for all code released under the Elixir project. By adhering to
these guidelines, we ensure that our community, maintainers, and contributors
uphold both legal and ethical standards while fostering a collaborative,
transparent environment.
This policy exists to support and protect the Elixir community. It aims to
balance openness, collaboration, and respect for all contributors’ rights,
ensuring that Elixir remains a trusted and innovative open source project.
## 2. Scope
This policy applies to the Elixir Programming language, located at
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
made, including documentation and any associated assets.
## 3. Licensing
All code released by the Elixir team is licensed under the
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
licenses are recognized as permissible in this project:
- The Unicode license, as documented at
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
- The Elixir Trademark Policy, as documented at
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
These licenses are considered acceptable for any files or code that form part of
an Elixir repository. If a contribution requires a different license, it must
either be rejected or prompt an update to this policy.
## 4. Contributing to the Elixir repository
Any code contributed to the Elixir repository must fall under one of the accepted
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
other license will be rejected unless this policy is formally revised to include
that license. All files except those specifically exempted (e.g., certain test
fixture files) must contain SPDX license and copyright headers
(`SPDX-License-Identifier` and `SPDX-FileCopyrightText`). If a file qualifies
for an exception, this must be configured in the ORT (Open Source Review Toolkit)
configuration and undergo review.
Contributions must not introduce executable binary files into the codebase.
## 5. Preservation of Copyright and License Information
Any third-party code incorporated into the Elixir repository must retain original
copyright and license headers. If no such headers exist in the source, they must
be added. This practice ensures that original authors receive proper credit and
that the licensing lineage is preserved.
## 6. Objectives
The Elixir project aims to promote a culture of responsible open source usage.
Specifically, our objectives include:
### 6.1 Clearly Define and Communicate Licensing & Compliance Policies
We will identify and document all third-party dependencies, ensure that license
information is communicated clearly, and maintain a project-wide license policy
or compliance handbook.
### 6.2 Implement Clear Processes for Reviewing Contributions
We will provide well-defined contribution guidelines. We implement the
Developer Certificate of Origin (DCO) for additional clarity regarding
contributor rights and obligations.
### 6.3 Track and Audit Third-Party Code Usage
All projects will implement a Software Bill of Materials (SBoM) strategy and
regularly verify license compliance for direct and transitive dependencies.
### 6.4 Monitor and Continuously Improve Open Source Compliance
We will conduct periodic internal audits, integrate compliance checks into
continuous integration (CI/CD) pipelines, and regularly review and refine these
objectives to align with best practices.
## 7. Roles and Responsibilities
### 7.1 Core Team Member
Core Team Members are responsible for being familiar with this policy and
ensuring it is consistently enforced. They must demonstrate sufficient
competencies to understand the policy requirements and must reject or request
changes to any pull requests that violate these standards.
### 7.2 Contributor
Contributors are expected to follow this policy when submitting code. If a
contributor submits a pull request that does not comply with the policy
(e.g., introduces a disallowed license), Core Team Members have the authority to
reject it or request changes. No special competencies are required for
contributors beyond awareness and adherence to the policy.
### 7.3 EEF CISO
The CISO designated by the Erlang Ecosystem Foundation (EEF) provides oversight
on queries and guidance regarding open source compliance or legal matters for
Elixir. The CISO is responsible for checking ongoing compliance with the policy,
escalating potential violations to the Core Team, and involving legal counsel if
necessary. This role does not require legal expertise but does involve
initiating legal or community discussions when needed.
## 8. Implications of Failing to Follow the Program Requirements
If a violation of this policy is identified, the Elixir Core Team will undertake
the following actions:
## 8.1 Review the Codebase for Additional Violations
We will investigate the codebase thoroughly to detect any similar instances of
non-compliance.
## 8.2 Review and Update the Process or Policy
In collaboration with the EEF CISO, the Elixir Core Team will assess the policy
and our internal workflows, making any necessary clarifications or amendments to
reduce the likelihood of recurrence.
## 8.3 Notify and Train Core Team Members
We will ensure that all active Core Team Members are informed about any policy
changes and understand how to apply them in everyday development.
## 8.4 Remove or Replace the Offending Code
If required, we will remove or replace the non-compliant code.
## 9. Contact
The project maintains a private mailing list at
[policy@elixir-lang.org](mailto:policy@elixir-lang.org) for handling licensing
and policy-related queries. Email is the preferred communication channel, and
the EEF CISO will be included on this list to provide assistance and ensure
timely responses. While solutions may take longer to implement, the project
commits to acknowledging all queries within five business days.
## 10. External Contributions of Core Team Members
When Core Team Members contribute to repositories outside Elixir, they do so in
a personal capacity or via their employer. They will not act as official
representatives of the Elixir team in those external contexts.
## 11. Policy Review and Amendments
This policy will be revisited annually to address new concerns, accommodate
changes in community standards, or adjust to emerging legal or technical
requirements. Proposed amendments must be reviewed by the Core Team and, if
necessary, by the EEF CISO. Any significant changes will be communicated to
contributors and made publicly available.
*Effective Date: 2025-02-20*
*Last Reviewed: 2025-11-20*
+147 -55
View File
@@ -1,20 +1,13 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
<h1> <h1>
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png"> <source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/public/images/logo/logo.png" width="200"> <img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture> </picture>
</h1> </h1>
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain) [![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10187/badge)](https://www.bestpractices.dev/projects/10187)
Elixir is a programming language designed for building scalable Elixir is a dynamic, functional language designed for building scalable
and maintainable applications. and maintainable applications.
For more about Elixir, installation and documentation, For more about Elixir, installation and documentation,
@@ -32,18 +25,19 @@ information, please read our [Security Policy][9].
All interactions in our official communication channels follow our All interactions in our official communication channels follow our
[Code of Conduct][1]. [Code of Conduct][1].
All contributions are required to conform to our [Open Source Policy][11].
## Bug reports ## Bug reports
For reporting bugs, [visit our issue tracker][2] and follow the steps For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities for reporting a new issue. **Please disclose security vulnerabilities
privately [in our Security page](https://github.com/elixir-lang/elixir/security)**. privately at <elixir-security@googlegroups.com>**.
All currently open bugs related to Elixir are listed in the issues tracker. ## Issues tracker management
The Elixir team uses the issues tracker to focus on *actionable items*,
including planned enhancements in the short and medium term. We also do All currently open bugs related to the Elixir repository are listed
our best to label entries for clarity and to ease collaboration. in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short and
medium term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as: Our *actionable item policy* has some important consequences, such as:
@@ -55,42 +49,38 @@ Our *actionable item policy* has some important consequences, such as:
elsewhere if appropriate). elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the * We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. If you believe we got something wrong, drop a issues tracker tidy. We may get things wrong from time to
comment and we can always reopen the issue. time and will gladly revisit issues, reopening when necessary.
By keeping the overall issues tracker tidy and organized, the community Keep the tone positive and be kind! For more information, see the
can easily peek at what is coming in new releases and also get involved [Code of Conduct][1].
by commenting on existing issues and submitting pull requests. Please
remember to keep the tone positive and be kind! For more information,
see the [Code of Conduct][1].
## Discussions, support, and help ### Proposing new features
For proposing new features, please start a discussion in the
[Elixir Core mailing list][3]. The [language development history and
its focus are described on our website](https://elixir-lang.org/development.html).
Keep in mind that it is your responsibility to argue and explain
why a feature is useful and how it will impact the codebase and
the community. A good proposal includes the problem description
and how the proposed solution compares with existing alternatives
in the Elixir ecosystem (as well as in other languages). To iron
out a proposal before submission, consider using and gathering
feedback from the community spaces [listed on the sidebar of the
Elixir website](https://elixir-lang.org/).
Once a proposal is accepted, it will be added to [the issue tracker][2].
Features and bug fixes that have already been merged and will be included
in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use the community For general discussions, support, and help, please use the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/), spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available such as forums, chat platforms, etc, where the wider community will be available
to help you. to help you.
## Proposing new features
We encourage you to first propose new features in the community spaces
listed above. These discussions help refine ideas and gather feedback before
submission. Our website also includes [a general outline of the language
history and its current development focus](https://elixir-lang.org/development.html).
Once you are ready, you can submit your proposal to the [Elixir Core
mailing list][3], either through the web interface or by subscribing to
it at <elixir-lang-core+subscribe@googlegroups.com>. Remember to include
a clear problem description, compare the proposed solution to existing
alternatives in the Elixir ecosystem (and in other languages if possible),
and consider the potential impact your changes will have on the codebase and
community.
Once a proposal is accepted, it will be added to [the issue tracker][2].
Features and bug fixes that have already been merged and will be included
in the next release are then "closed" and added to the [changelog][7]
before release.
## Compiling from source ## Compiling from source
For the many different ways to install Elixir, For the many different ways to install Elixir,
@@ -113,19 +103,122 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version, In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable). you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
When updating the repository, you may want to run `make clean` before Additionally, you may choose to run the test suite with `make clean test`.
recompiling. For deterministic builds, you should set the environment
variable `ERL_COMPILER_OPTIONS=deterministic`.
## Contributing ## Contributing
Contributions to Elixir are always welcome! Before you get started, please check We invite contributions to Elixir. To contribute, there are a few
out our [CONTRIBUTING.md](CONTRIBUTING.md) file. There you will find detailed things you need to know about the code. First, Elixir code is divided
guidelines on how to set up your environment, run the test suite, format your by each application inside the `lib` folder:
code, and submit pull requests.
Note you must disclose the use of coding agents and AI written code in your * `elixir` - Elixir's kernel and standard library
contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIBUTING.md).
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile (including Erlang modules):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
you can rebuild the language from scratch with:
```sh
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
With tests running and passing, you are ready to contribute to Elixir and
[send a pull request](https://help.github.com/articles/using-pull-requests/).
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
Committers may also push style changes directly to your branch. If you would
rather manage all changes yourself, you can disable the "Allow edits from maintainers"
feature when submitting your pull request.
The Elixir team may optionally assign someone to review a pull request.
If someone is assigned, they must explicitly approve the code before
another team member can merge it.
When the review finishes, your pull request will be squashed and merged
into the repository. If you have carefully organized your commits and
believe they should be merged without squashing, please mention it in
a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
```
Now go back to Elixir's root directory and run:
```sh
make docs # to generate HTML pages
make docs DOCS_FORMAT=epub # to generate EPUB documents
```
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
and `mix` under the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
## Development links ## Development links
@@ -148,7 +241,6 @@ contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIB
[8]: https://groups.google.com/group/elixir-lang-ann [8]: https://groups.google.com/group/elixir-lang-ann
[9]: SECURITY.md [9]: SECURITY.md
[10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date [10]: https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date
[11]: OPEN_SOURCE_POLICY.md
## License ## License
@@ -156,4 +248,4 @@ contributions. See the "Using AI and coding agents" in [CONTRIBUTING.md](CONTRIB
Elixir source code is released under Apache License 2.0. Elixir source code is released under Apache License 2.0.
Check [LICENSE](LICENSE) file for more information. Check [NOTICE](NOTICE) and [LICENSE](LICENSE) files for more information.
+5 -11
View File
@@ -1,14 +1,8 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Release process # Release process
## Shipping a new version ## Shipping a new version
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat 1. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date 2. Ensure /CHANGELOG.md is updated, versioned and add the current date
@@ -24,11 +18,11 @@
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com` 8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (usually before first rc) ## Creating a new vMAJOR.MINOR branch (before first rc)
### In the new branch ### In the new branch
1. Comment out `CANONICAL := main/` in /Makefile 1. Comment out `CANONICAL=` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations" 2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
@@ -36,7 +30,7 @@
### Back in main ### Back in main
1. Bump /VERSION file, bin/elixir, and bin/elixir.bat 1. Bump /VERSION file, bin/elixir and bin/elixir.bat
2. Start new /CHANGELOG.md 2. Start new /CHANGELOG.md
@@ -50,6 +44,6 @@
2. Update `otp_release` checks in `/Makefile` and `/lib/elixir/src/elixir.erl` 2. Update `otp_release` checks in `/Makefile` and `/lib/elixir/src/elixir.erl`
3. Update relevant CI workflows in `/.github/workflows/*.yml` - for release workflows, outdated/recently added Erlang/OTP versions must run conditionally 3. Update relevant CI workflows in `/.github/workflows/*.yml`
4. Remove `otp_release` version checks that are no longer needed 4. Remove `otp_release` version checks that are no longer needed
+5 -12
View File
@@ -1,9 +1,3 @@
<!--
SPDX-License-Identifier: Apache-2.0
SPDX-FileCopyrightText: 2021 The Elixir Team
SPDX-FileCopyrightText: 2012 Plataformatec
-->
# Security Policy # Security Policy
## Supported versions ## Supported versions
@@ -12,16 +6,15 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.21 | Development 1.17 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only 1.16 | Security patches only
1.15 | Security patches only
1.14 | Security patches only
1.13 | Security patches only
## Announcements ## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub. You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
+1 -1
View File
@@ -1 +1 @@
1.21.0-dev 1.17.3
+11 -8
View File
@@ -1,12 +1,7 @@
#!/bin/sh #!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e set -e
ELIXIR_VERSION=1.21.0-dev ELIXIR_VERSION=1.17.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2 cat <<USAGE >&2
@@ -23,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
-pz "PATH" Appends the given path to Erlang code path (*) -pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone) -v, --version Prints Erlang/OTP and Elixir versions (standalone)
--color, --no-color Enables or disables ANSI coloring
--erl "SWITCHES" Switches to be passed down to Erlang (*) --erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*) --eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting --logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting --logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution --no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone) --short-version Prints Elixir version (standalone)
--werl Uses Erlang's Windows shell GUI (Windows only)
Options given after the .exs file or -- are passed down to the executed code. Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl. Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
@@ -117,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1 C=1
MODE="iex" MODE="iex"
;; ;;
-v|--no-halt|--color|--no-color) -v|--no-halt)
C=1 C=1
;; ;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg) -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -187,6 +182,10 @@ while [ $I -le $LENGTH ]; do
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1 echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi fi
;; ;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
;;
*) *)
while [ $I -le $LENGTH ]; do while [ $I -le $LENGTH ]; do
I=$((I + 1)) I=$((I + 1))
@@ -221,6 +220,10 @@ SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
fi
# One MAY change ERTS_BIN= but you MUST NOT change # One MAY change ERTS_BIN= but you MUST NOT change
# ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases. # ERTS_BIN=$ERTS_BIN as it is handled by Elixir releases.
ERTS_BIN= ERTS_BIN=
+17 -10
View File
@@ -1,10 +1,6 @@
@echo off @echo off
:: SPDX-License-Identifier: Apache-2.0 set ELIXIR_VERSION=1.17.3
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.21.0-dev
if ""%1""=="""" if ""%2""=="""" goto documentation if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
@@ -28,13 +24,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*) echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone) echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo. echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*) echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*) echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone) echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo. echo.
echo Options given after the .exs file or -- are passed down to the executed code. echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl. echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
@@ -99,6 +95,7 @@ if "!par!"=="" (
shift shift
set par="!par:"=\"!" set par="!par:"=\"!"
rem ******* EXECUTION OPTIONS ********************** rem ******* EXECUTION OPTIONS **********************
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set useIEx=1 && goto startloop) if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop) if !par!=="+elixirc" (goto startloop)
rem ******* ELIXIR PARAMETERS ********************** rem ******* ELIXIR PARAMETERS **********************
@@ -112,8 +109,6 @@ if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop) if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop) if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop) if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop) if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop) if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop) if ""==!par:--dbg=! (shift && goto startloop)
@@ -133,6 +128,10 @@ if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on
:run :run
setlocal disabledelayedexpansion setlocal disabledelayedexpansion
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true %beforeExtra%
)
if not defined useIEx ( if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra% set beforeExtra=-s elixir start_cli %beforeExtra%
) )
@@ -140,9 +139,17 @@ if not defined useIEx (
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra% set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
if defined ELIXIR_CLI_DRY_RUN ( if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* if defined useWerl (
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
) else ( ) else (
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %* if defined useWerl (
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
) )
exit /B %ERRORLEVEL% exit /B %ERRORLEVEL%
:end :end
-5
View File
@@ -1,9 +1,4 @@
#!/bin/sh #!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e set -e
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
-5
View File
@@ -1,9 +1,4 @@
@echo off @echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
setlocal setlocal
set argc=0 set argc=0
for %%A in (%*) do ( for %%A in (%*) do (
-5
View File
@@ -1,9 +1,4 @@
#!/bin/sh #!/bin/sh
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
set -e set -e
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
+4 -6
View File
@@ -1,9 +1,4 @@
@echo off @echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
setlocal setlocal
if /I ""%1""==""--help"" goto documentation if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation if /I ""%1""==""-h"" goto documentation
@@ -21,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists. echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded. echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo. echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help". echo It accepts all other options listed by "elixir --help".
goto end goto end
:run :run
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %* if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end :end
endlocal endlocal
-5
View File
@@ -1,7 +1,2 @@
#!/usr/bin/env elixir #!/usr/bin/env elixir
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Mix.CLI.main() Mix.CLI.main()
-5
View File
@@ -1,7 +1,2 @@
@echo off @echo off
:: SPDX-License-Identifier: Apache-2.0
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
call "%~dp0\elixir.bat" "%~dp0\mix" %* call "%~dp0\elixir.bat" "%~dp0\mix" %*
Executable → Regular
+1 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
# Store path to mix.bat as a FileInfo object # Store path to mix.bat as a FileInfo object
$mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat')) $mixBatPath = (Get-ChildItem (((Get-ChildItem $MyInvocation.MyCommand.Path).Directory.FullName) + '\mix.bat'))
$newArgs = @() $newArgs = @()
@@ -24,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
} }
# Corrected arguments are ready to pass to batch file # Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs & $mixBatPath $newArgs
+6 -28
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.SyntaxError do defmodule EEx.SyntaxError do
defexception [:file, :line, :column, :snippet, message: "syntax error"] defexception [:file, :line, :column, :snippet, message: "syntax error"]
@@ -118,19 +114,6 @@ defmodule EEx do
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata} | {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
| {:eof, metadata} | {:eof, metadata}
@type tokenize_opt ::
{:file, binary()}
| {:line, line}
| {:column, column}
| {:indentation, non_neg_integer}
| {:trim, boolean()}
@type compile_opt ::
tokenize_opt
| {:engine, module()}
| {:parser_options, Code.parser_opts()}
| {atom(), term()}
@doc """ @doc """
Generates a function definition from the given string. Generates a function definition from the given string.
@@ -141,7 +124,6 @@ defmodule EEx do
template. template.
The supported `options` are described [in the module docs](#module-options). The supported `options` are described [in the module docs](#module-options).
Additional options are passed to the underlying engine.
## Examples ## Examples
@@ -234,11 +216,9 @@ defmodule EEx do
"3" "3"
""" """
@spec compile_string(String.t(), [compile_opt]) :: Macro.t() @spec compile_string(String.t(), keyword) :: Macro.t()
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim]) case tokenize(source, options) do
case tokenize(source, tokenize_opts) do
{:ok, tokens} -> {:ok, tokens} ->
EEx.Compiler.compile(tokens, source, options) EEx.Compiler.compile(tokens, source, options)
@@ -275,7 +255,7 @@ defmodule EEx do
#=> "3" #=> "3"
""" """
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t() @spec compile_file(Path.t(), keyword) :: Macro.t()
def compile_file(filename, options \\ []) when is_list(options) do def compile_file(filename, options \\ []) when is_list(options) do
filename = IO.chardata_to_string(filename) filename = IO.chardata_to_string(filename)
options = Keyword.merge([file: filename, line: 1], options) options = Keyword.merge([file: filename, line: 1], options)
@@ -293,7 +273,7 @@ defmodule EEx do
"foo baz" "foo baz"
""" """
@spec eval_string(String.t(), Code.binding(), [compile_opt]) :: term() @spec eval_string(String.t(), keyword, keyword) :: String.t()
def eval_string(source, bindings \\ [], options \\ []) def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do when is_binary(source) and is_list(bindings) and is_list(options) do
compiled = compile_string(source, options) compiled = compile_string(source, options)
@@ -315,7 +295,7 @@ defmodule EEx do
#=> "foo baz" #=> "foo baz"
""" """
@spec eval_file(Path.t(), Code.binding(), [compile_opt]) :: String.t() @spec eval_file(Path.t(), keyword, keyword) :: String.t()
def eval_file(filename, bindings \\ [], options \\ []) def eval_file(filename, bindings \\ [], options \\ [])
when is_list(bindings) and is_list(options) do when is_list(bindings) and is_list(options) do
filename = IO.chardata_to_string(filename) filename = IO.chardata_to_string(filename)
@@ -344,7 +324,6 @@ defmodule EEx do
It returns `{:ok, [token]}` where a token is one of: It returns `{:ok, [token]}` where a token is one of:
* `{:comment, content, %{column: column, line: line}}`
* `{:text, content, %{column: column, line: line}}` * `{:text, content, %{column: column, line: line}}`
* `{:expr, marker, content, %{column: column, line: line}}` * `{:expr, marker, content, %{column: column, line: line}}`
* `{:start_expr, marker, content, %{column: column, line: line}}` * `{:start_expr, marker, content, %{column: column, line: line}}`
@@ -356,7 +335,7 @@ defmodule EEx do
Note new tokens may be added in the future. Note new tokens may be added in the future.
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec tokenize([char()] | String.t(), [tokenize_opt]) :: @spec tokenize([char()] | String.t(), opts :: keyword) ::
{:ok, [token()]} | {:error, String.t(), metadata()} {:ok, [token()]} | {:error, String.t(), metadata()}
def tokenize(contents, opts \\ []) do def tokenize(contents, opts \\ []) do
EEx.Compiler.tokenize(contents, opts) EEx.Compiler.tokenize(contents, opts)
@@ -365,7 +344,6 @@ defmodule EEx do
### Helpers ### Helpers
defp do_eval(compiled, bindings, options) do defp do_eval(compiled, bindings, options) do
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
{result, _} = Code.eval_quoted(compiled, bindings, options) {result, _} = Code.eval_quoted(compiled, bindings, options)
result result
end end
+36 -95
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.Compiler do defmodule EEx.Compiler do
@moduledoc false @moduledoc false
@@ -52,14 +48,8 @@ defmodule EEx.Compiler do
end end
end end
# TODO: Remove me on Elixir v2.0 # TODO: Deprecate this on Elixir v1.18
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
line: line,
column: column,
file: state.file
)
case expr(t, line, column + 3, state, []) do case expr(t, line, column + 3, state, []) do
{:error, message} -> {:error, message} ->
{:error, message, %{line: line, column: column}} {:error, message, %{line: line, column: column}}
@@ -79,7 +69,7 @@ defmodule EEx.Compiler do
{:error, message <> code_snippet(state.source, state.indentation, meta), meta} {:error, message <> code_snippet(state.source, state.indentation, meta), meta}
{:ok, expr, new_line, new_column, rest} -> {:ok, expr, new_line, new_column, rest} ->
{key, expr, extra_meta} = {key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} -> {:ok, _line, _column, _warnings, rev_tokens, []} ->
# We ignore warnings because the code will be tokenized # We ignore warnings because the code will be tokenized
@@ -87,7 +77,7 @@ defmodule EEx.Compiler do
token_key(rev_tokens, expr) token_key(rev_tokens, expr)
{:error, _, _, _, _} -> {:error, _, _, _, _} ->
{:expr, expr, %{}} {:expr, expr}
end end
marker = marker =
@@ -96,14 +86,14 @@ defmodule EEx.Compiler do
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <> "unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
"please remove \"#{marker}\"" "please remove \"#{marker}\""
IO.warn(message, file: state.file, line: line, column: column) :elixir_errors.erl_warn({line, column}, state.file, message)
~c"" ~c""
else else
marker marker
end end
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)} token = {key, marker, expr, %{line: line, column: column}}
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1)) trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
end end
end end
@@ -127,27 +117,6 @@ defmodule EEx.Compiler do
tokenize(rest, line, column, state, [{line, column}], fun.(acc)) tokenize(rest, line, column, state, [{line, column}], fun.(acc))
end end
# Merge middle expressions separated only by whitespace so the whitespace is
# part of the Elixir expression, not a separate EEx body.
defp merge_token(
{:middle_expr, ~c"", chars, meta},
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
) do
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
else
[
{:middle_expr, ~c"", chars, meta},
{:text, text, text_meta},
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
]
end
end
defp merge_token(token, acc) do
[token | acc]
end
# Retrieve marker for <% # Retrieve marker for <%
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
@@ -198,37 +167,35 @@ defmodule EEx.Compiler do
defp token_key(rev_tokens, expr) do defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
{[{:end, _} | _], [{:do, _} | _]} -> {[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:do, _} | _]} -> {_, [{:do, _} | _]} ->
{:start_expr, maybe_append_space(expr), %{}} {:start_expr, maybe_append_space(expr)}
{_, [{:block_identifier, _, identifier} | _]} -> {_, [{:block_identifier, _, _} | _]} ->
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}} {:middle_expr, maybe_append_space(expr)}
{[{:end, _} | _], [{:stab_op, _, _} | _]} -> {[{:end, _} | _], [{:stab_op, _, _} | _]} ->
{:middle_expr, expr, %{}} {:middle_expr, expr}
{_, [{:stab_op, _, _} | rev_tokens]} -> {_, [{:stab_op, _, _} | reverse_tokens]} ->
if fn_before_end?(rev_tokens) do fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
{:start_expr, expr, %{}} end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
if end_index > fn_index do
{:start_expr, expr}
else else
{:middle_expr, expr, %{}} {:middle_expr, expr}
end end
{tokens, _} -> {tokens, _} ->
case Enum.drop_while(tokens, &closing_bracket?/1) do case Enum.drop_while(tokens, &closing_bracket?/1) do
[{:end, _} | _] -> {:end_expr, expr, %{}} [{:end, _} | _] -> {:end_expr, expr}
_ -> {:expr, expr, %{}} _ -> {:expr, expr}
end end
end end
end end
defp fn_before_end?([{:fn, _} | _]), do: true
defp fn_before_end?([{:end, _} | _]), do: false
defp fn_before_end?([_ | rev_tokens]), do: fn_before_end?(rev_tokens)
defp fn_before_end?([]), do: false
defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest) defp drop_eol([{:eol, _} | rest]), do: drop_eol(rest)
defp drop_eol(rest), do: rest defp drop_eol(rest), do: rest
@@ -326,20 +293,12 @@ defmodule EEx.Compiler do
file: file, file: file,
source: source, source: source,
line: line, line: line,
quoted: %{}, quoted: [],
parser_options: [indentation: indentation] ++ parser_options, parser_options: parser_options,
indentation: indentation indentation: indentation
} }
init = state.engine.init(opts) init = state.engine.init(opts)
if function_exported?(state.engine, :handle_text, 2) and
not function_exported?(state.engine, :handle_text, 3) do
IO.warn(
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
)
end
generate_buffer(tokens, init, [], state) generate_buffer(tokens, init, [], state)
end end
@@ -357,7 +316,8 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column] meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars)) state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else else
# TODO: Remove this on Elixir v2.0. The deprecation is on init. # TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars)) state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end end
@@ -370,7 +330,7 @@ defmodule EEx.Compiler do
state.parser_options state.parser_options
expr = Code.string_to_quoted!(chars, options) expr = Code.string_to_quoted!(chars, options)
buffer = handle_expr(buffer, mark, expr, meta, state) buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state) generate_buffer(rest, buffer, scope, state)
end end
@@ -389,17 +349,17 @@ defmodule EEx.Compiler do
rest, rest,
state.engine.handle_begin(buffer), state.engine.handle_begin(buffer),
[{contents, start_line, start_column} | scope], [{contents, start_line, start_column} | scope],
%{state | quoted: %{}, line: line} %{state | quoted: [], line: line}
) )
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message = message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\"" "the contents of this expression won't be output unless the EEx block starts with \"<%=\""
IO.warn(message, file: state.file, line: meta.line, column: meta.column) :elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end end
buffer = handle_expr(buffer, mark, contents, meta, state) buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state) generate_buffer(rest, buffer, scope, state)
end end
@@ -433,7 +393,7 @@ defmodule EEx.Compiler do
) do ) do
{wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state) {wrapped, state} = wrap_expr(current, meta.line, buffer, chars, state)
options = [file: state.file, line: line, column: column] ++ state.parser_options options = [file: state.file, line: line, column: column] ++ state.parser_options
tuples = Code.string_to_quoted!(:lists.flatten(wrapped), options) tuples = Code.string_to_quoted!(wrapped, options)
buffer = insert_quoted(tuples, state.quoted) buffer = insert_quoted(tuples, state.quoted)
{buffer, rest} {buffer, rest}
end end
@@ -449,7 +409,7 @@ defmodule EEx.Compiler do
defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do defp generate_buffer([{:eof, _meta}], _buffer, [{content, line, column} | _scope], state) do
message = "expected a closing '<% end %>' for block expression in EEx" message = "expected a closing '<% end %>' for block expression in EEx"
expr_meta = non_whitespace_meta(:lists.flatten(content), line, column, state) expr_meta = non_whitespace_meta(content, line, column, state)
syntax_error!(message, expr_meta, state) syntax_error!(message, expr_meta, state)
end end
@@ -466,10 +426,10 @@ defmodule EEx.Compiler do
defp wrap_expr(current, line, buffer, chars, state) do defp wrap_expr(current, line, buffer, chars, state) do
new_lines = List.duplicate(?\n, line - state.line) new_lines = List.duplicate(?\n, line - state.line)
key = map_size(state.quoted) key = length(state.quoted)
placeholder = [~c"__EEX__(", Integer.to_charlist(key), ~c");"] placeholder = ~c"__EEX__(" ++ Integer.to_charlist(key) ++ ~c");"
count = [current, placeholder, new_lines, chars] count = current ++ placeholder ++ new_lines ++ chars
new_state = %{state | quoted: Map.put(state.quoted, key, state.engine.handle_end(buffer))} new_state = %{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}
{count, new_state} {count, new_state}
end end
@@ -499,16 +459,11 @@ defmodule EEx.Compiler do
Enum.all?(chars, &(&1 in @all_spaces)) Enum.all?(chars, &(&1 in @all_spaces))
end end
defp clause_block_identifier?(%{block_identifier: identifier}) do
identifier in [:else, :rescue, :catch]
end
defp clause_block_identifier?(_meta), do: false
# Changes placeholder to real expression # Changes placeholder to real expression
defp insert_quoted({:__EEX__, _, [key]}, quoted) do defp insert_quoted({:__EEX__, _, [key]}, quoted) do
Map.fetch!(quoted, key) {^key, value} = List.keyfind(quoted, key, 0)
value
end end
defp insert_quoted({left, line, right}, quoted) do defp insert_quoted({left, line, right}, quoted) do
@@ -541,20 +496,6 @@ defmodule EEx.Compiler do
column: meta.column column: meta.column
end end
defp handle_expr(buffer, mark, expr, meta, state) do
state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
rescue
e in EEx.SyntaxError ->
reraise %{
e
| file: e.file || state.file,
line: e.line || meta.line,
column: e.column || meta.column,
snippet: e.snippet || code_snippet(state.source, state.indentation, meta)
},
__STACKTRACE__
end
defp code_snippet(source, indentation, meta) do defp code_snippet(source, indentation, meta) do
line_start = max(meta.line - 3, 1) line_start = max(meta.line - 3, 1)
line_end = meta.line line_end = meta.line
+1 -9
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.Engine do defmodule EEx.Engine do
@moduledoc ~S""" @moduledoc ~S"""
Basic EEx engine that ships with Elixir. Basic EEx engine that ships with Elixir.
@@ -17,10 +13,6 @@ defmodule EEx.Engine do
@doc """ @doc """
Called at the beginning of every template. Called at the beginning of every template.
It receives the options during compilation, including the
ones managed by EEx, such as `:line` and `:file`, as well
as custom engine options.
It must return the initial state. It must return the initial state.
""" """
@callback init(opts :: keyword) :: state @callback init(opts :: keyword) :: state
@@ -195,7 +187,7 @@ defmodule EEx.Engine do
def handle_expr(state, "=", ast) do def handle_expr(state, "=", ast) do
check_state!(state) check_state!(state)
%{binary: binary, dynamic: dynamic, vars_count: vars_count} = state %{binary: binary, dynamic: dynamic, vars_count: vars_count} = state
var = Macro.var(String.to_unsafe_atom("arg#{vars_count}"), __MODULE__) var = Macro.var(:"arg#{vars_count}", __MODULE__)
ast = ast =
quote do quote do
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.SmartEngine do defmodule EEx.SmartEngine do
@moduledoc """ @moduledoc """
The default engine used by EEx. The default engine used by EEx.
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule EEx.MixProject do defmodule EEx.MixProject do
use Mix.Project use Mix.Project
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__) Code.require_file("../test_helper.exs", __DIR__)
defmodule EEx.SmartEngineTest do defmodule EEx.SmartEngineTest do
+56 -18
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("../test_helper.exs", __DIR__) Code.require_file("../test_helper.exs", __DIR__)
defmodule EEx.TokenizerTest do defmodule EEx.TokenizerTest do
@@ -130,21 +126,51 @@ defmodule EEx.TokenizerTest do
end end
test "EEx comments" do test "EEx comments" do
ExUnit.CaptureIO.capture_io(:stderr, fn -> exprs = [
exprs = [ {:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"foo ", %{column: 1, line: 1}}, {:eof, %{column: 16, line: 1}}
{:eof, %{column: 16, line: 1}} ]
]
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs} assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
exprs = [ exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}}, {:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}} {:eof, %{column: 8, line: 2}}
] ]
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs} assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end) end
test "EEx comments with do-end" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 31, line: 1}},
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
{:text, ~c"bar", %{column: 46, line: 1}},
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
{:ok, exprs}
end end
test "EEx multi-line comments" do test "EEx multi-line comments" do
@@ -270,7 +296,7 @@ defmodule EEx.TokenizerTest do
{:text, ~c"foo ", %{column: 1, line: 1}}, {:text, ~c"foo ", %{column: 1, line: 1}},
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}}, {:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
{:text, ~c"bar", %{column: 21, line: 1}}, {:text, ~c"bar", %{column: 21, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}}, {:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
{:text, ~c"baz", %{column: 34, line: 1}}, {:text, ~c"baz", %{column: 34, line: 1}},
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}}, {:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
{:eof, %{column: 46, line: 1}} {:eof, %{column: 46, line: 1}}
@@ -286,7 +312,7 @@ defmodule EEx.TokenizerTest do
exprs = [ exprs = [
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}}, {:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}}, {:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}}, {:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}}, {:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}}, {:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
{:eof, %{column: 3, line: 7}} {:eof, %{column: 3, line: 7}}
@@ -295,6 +321,15 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs} assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
end end
test "trim mode with comment" do
exprs = [
{:text, ~c"\n123", %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do test "trim mode with multi-line comment" do
exprs = [ exprs = [
{:comment, ~c" comment ", %{column: 3, line: 1}}, {:comment, ~c" comment ", %{column: 3, line: 1}},
@@ -349,6 +384,9 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(~c"foo <% :bar", @opts) == assert EEx.tokenize(~c"foo <% :bar", @opts) ==
{:error, message, %{column: 5, line: 1}} {:error, message, %{column: 5, line: 1}}
assert EEx.tokenize(~c"<%# true ", @opts) ==
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
message = """ message = """
expected closing '--%>' for EEx expression expected closing '--%>' for EEx expression
| |
+12 -109
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
Code.require_file("test_helper.exs", __DIR__) Code.require_file("test_helper.exs", __DIR__)
require EEx require EEx
@@ -202,6 +198,15 @@ defmodule EExTest do
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>") assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
end end
test "embedded code with comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
assert_eval(
"foo\n\nbar\n",
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
)
end
test "embedded code with multi-line comments in do end" do test "embedded code with multi-line comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>") assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
@@ -262,37 +267,6 @@ defmodule EExTest do
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>") assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
end end
test "with expression with else clause split across tags" do
template = """
<%= with {:ok, x} <- @res do %>
<p><%= x %></p>
<% else %>
<% _ -> %>
<p>bad</p>
<% end %>
"""
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
engine: EEx.SmartEngine
)
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
engine: EEx.SmartEngine
)
end
test "empty clauses separated by whitespace" do
template = """
<%= case x do %>
<% :foo -> %>
<% :bar -> %>
<% end %>
"""
assert_eval("\n \n", template, x: :foo)
assert_eval("\n\n", template, x: :bar)
end
test "with end of token" do test "with end of token" do
assert_eval("foo bar %>", "foo bar %>") assert_eval("foo bar %>", "foo bar %>")
end end
@@ -533,59 +507,6 @@ defmodule EExTest do
end end
end end
test "from Elixir parser" do
line = __ENV__.line + 6
message =
assert_raise TokenMissingError, fn ->
EEx.compile_string(
"""
<li>
<strong>Some:</strong>
<%= true && @some[ %>
</li>
""",
file: __ENV__.file,
line: line,
indentation: 12
)
end
assert message |> Exception.message() |> strip_ansi() =~ """
│
#{line + 2} │ true && @some[\s
│ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter
"""
end
test "from Elixir parser with line breaks" do
line = __ENV__.line + 6
message =
assert_raise TokenMissingError, fn ->
EEx.compile_string(
"""
<li>
<strong>Some:</strong>
<%= true &&
@some[ %>
</li>
""",
file: __ENV__.file,
line: line,
indentation: 12
)
end
assert message |> Exception.message() |> strip_ansi() =~ """
│
#{line + 3} │ @some[\s
│ │ └ missing closing delimiter (expected "]")
│ └ unclosed delimiter
"""
end
test "honor line numbers" do test "honor line numbers" do
assert_raise EEx.SyntaxError, assert_raise EEx.SyntaxError,
"nofile:100:6: expected closing '%>' for EEx expression", "nofile:100:6: expected closing '%>' for EEx expression",
@@ -606,18 +527,6 @@ defmodule EExTest do
EEx.compile_string("foo <%= bar", file: "my_file.eex") EEx.compile_string("foo <%= bar", file: "my_file.eex")
end end
end end
test "unsupported marker error carries template location metadata" do
error =
assert_raise EEx.SyntaxError, fn ->
EEx.compile_string("<%/ true %>", file: "sample.eex", line: 7)
end
assert error.file == "sample.eex"
assert error.line == 7
assert error.column == 1
assert Exception.message(error) =~ "sample.eex:7:1:"
end
end end
describe "warnings" do describe "warnings" do
@@ -960,13 +869,13 @@ defmodule EExTest do
file = to_charlist(Path.relative_to_cwd(__ENV__.file)) file = to_charlist(Path.relative_to_cwd(__ENV__.file))
assert EExTest.Compiled.before_compile() == assert EExTest.Compiled.before_compile() ==
{11, {EExTest.Compiled, :before_compile, 0, [file: file, line: 11]}} {7, {EExTest.Compiled, :before_compile, 0, [file: file, line: 7]}}
assert EExTest.Compiled.after_compile() == assert EExTest.Compiled.after_compile() ==
{25, {EExTest.Compiled, :after_compile, 0, [file: file, line: 25]}} {21, {EExTest.Compiled, :after_compile, 0, [file: file, line: 21]}}
assert EExTest.Compiled.unknown() == assert EExTest.Compiled.unknown() ==
{30, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 30]}} {26, {EExTest.Compiled, :unknown, 0, [file: ~c"unknown", line: 26]}}
end end
end end
@@ -1044,12 +953,6 @@ defmodule EExTest do
end end
end end
@strip_ansi [IO.ANSI.green(), IO.ANSI.red(), IO.ANSI.reset()]
defp strip_ansi(doc) do
String.replace(doc, @strip_ansi, "")
end
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts) opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
result = EEx.eval_string(actual, binding, opts) result = EEx.eval_string(actual, binding, opts)
+5 -17
View File
@@ -1,20 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
{line_exclude, line_include} = {line_exclude, line_include} =
if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []} if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []}
Code.require_file("../../elixir/scripts/cover_record.exs", __DIR__) ExUnit.start(
CoverageRecorder.maybe_record("eex") trace: !!System.get_env("TRACE"),
include: line_include,
maybe_seed_opt = if seed = System.get_env("SEED"), do: [seed: String.to_integer(seed)], else: [] exclude: line_exclude
)
ex_unit_opts =
[
trace: !!System.get_env("TRACE"),
include: line_include,
exclude: line_exclude
] ++ maybe_seed_opt
ExUnit.start(ex_unit_opts)
+2 -6
View File
@@ -1,7 +1,3 @@
%% SPDX-License-Identifier: Apache-2.0
%% SPDX-FileCopyrightText: 2021 The Elixir Team
%% SPDX-FileCopyrightText: 2012 Plataformatec
{'src/*', [ {'src/*', [
warn_unused_vars, warn_unused_vars,
warn_export_all, warn_export_all,
@@ -13,8 +9,8 @@
warn_deprecated_function, warn_deprecated_function,
warn_obsolete_guard, warn_obsolete_guard,
warn_exported_vars, warn_exported_vars,
%% Enable this when we require Erlang/OTP 27+ %% warn_missing_spec,
%% warnings_as_errors, %% warn_untyped_record,
debug_info, debug_info,
{outdir, "ebin/"} {outdir, "ebin/"}
]}. ]}.
+50 -195
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Access do defmodule Access do
@moduledoc """ @moduledoc """
Key-based access to data structures. Key-based access to data structures.
@@ -10,8 +6,8 @@ defmodule Access do
keys of any type in a data structure via the `data[key]` syntax. keys of any type in a data structure via the `data[key]` syntax.
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out `Access` supports keyword lists (`Keyword`) and maps (`Map`) out
of the box. Keyword lists support only atom keys, while keys for maps of the box. Keywords supports only atoms keys, keys for maps can
can be of any type. Both return `nil` if the key does not exist: be of any type. Both return `nil` if the key does not exist:
iex> keywords = [a: 1, b: 2] iex> keywords = [a: 1, b: 2]
iex> keywords[:a] iex> keywords[:a]
@@ -33,7 +29,7 @@ defmodule Access do
iex> keywords[:c][:unknown] iex> keywords[:c][:unknown]
nil nil
This works because accessing anything on a `nil` value returns This works because accessing anything on a `nil` value, returns
`nil` itself: `nil` itself:
iex> nil[:a] iex> nil[:a]
@@ -226,8 +222,6 @@ defmodule Access do
end end
end end
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
@doc """ @doc """
Fetches the value for the given key in a container (a map, keyword Fetches the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour). list, or struct that implements the `Access` behaviour).
@@ -356,7 +350,7 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \ Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \ instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must access a list element by index, \ as a whole. If you really must access a list element by index, \
you can use Enum.at/2 or the functions in the List module\ you can Enum.at/1 or the functions in the List module\
""" """
end end
@@ -424,8 +418,8 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \ Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \ instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must modify a list element by index, \ as a whole. If you really must mostify a list element by index, \
you can use Access.at/1 or the functions in the List module\ you can Access.at/1 or the functions in the List module\
""" """
end end
@@ -487,7 +481,7 @@ defmodule Access do
## Accessors ## Accessors
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -516,56 +510,30 @@ defmodule Access do
iex> pop_in(map, [Access.key(:user), Access.key(:name)]) iex> pop_in(map, [Access.key(:user), Access.key(:name)])
{"john", %{user: %{}}} {"john", %{user: %{}}}
iex> keyword = [user: [name: "john"]] An error is raised if the accessed structure is not a map or a struct:
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
"john"
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
{"john", [user: []]}
An error is raised if the accessed structure is not a map, struct, or keyword list: iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got: []
iex> get_in(123, [Access.key(:foo)])
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
""" """
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
def key(key, default \\ nil) do def key(key, default \\ nil) do
fn fn
:get, %{} = data, next -> :get, data, next ->
next.(Map.get(data, key, default)) next.(Map.get(data, key, default))
:get_and_update, %{} = data, next -> :get_and_update, data, next ->
value = Map.get(data, key, default) value = Map.get(data, key, default)
case next.(value) do case next.(value) do
{get, update} -> {get, Map.put(data, key, update)} {get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.get(data, key, default))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.get(data, key, default)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next ->
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
end end
end end
@doc """ @doc """
Returns a function that accesses the given key in a map/struct/keyword list. Returns a function that accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`, The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends. `Kernel.get_and_update_in/3`, and friends.
@@ -574,19 +542,6 @@ defmodule Access do
## Examples ## Examples
iex> keyword = [user: [name: "john"]]
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
"john"
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [user: [name: "JOHN"]]}
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
{"john", [user: []]}
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in:
...
iex> map = %{user: %{name: "john"}} iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key!(:user), Access.key!(:name)]) iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
"john" "john"
@@ -597,8 +552,7 @@ defmodule Access do
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)]) iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
{"john", %{user: %{}}} {"john", %{user: %{}}}
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)]) iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
** (KeyError) key :unknown not found in: ** (KeyError) key :unknown not found in: %{name: \"john\"}
...
The examples above could be partially written as: The examples above could be partially written as:
@@ -615,15 +569,13 @@ defmodule Access do
`Access.key!/1` is useful when the key is not known in advance `Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically. and must be accessed dynamically.
An error is raised if the accessed structure is not a map/struct/keyword list: An error is raised if the accessed structure is not a map/struct:
iex> get_in(123, [Access.key!(:foo)]) iex> get_in([], [Access.key!(:foo)])
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123 ** (RuntimeError) Access.key!/1 expected a map/struct, got: []
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
""" """
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term) @spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
def key!(key) do def key!(key) do
fn fn
:get, %{} = data, next -> :get, %{} = data, next ->
@@ -637,19 +589,8 @@ defmodule Access do
:pop -> {value, Map.delete(data, key)} :pop -> {value, Map.delete(data, key)}
end end
:get, data, next when is_probably_keyword(data) ->
next.(Keyword.fetch!(data, key))
:get_and_update, data, next when is_probably_keyword(data) ->
value = Keyword.fetch!(data, key)
case next.(value) do
{get, update} -> {get, Keyword.put(data, key, update)}
:pop -> {value, Keyword.delete(data, key)}
end
_op, data, _next -> _op, data, _next ->
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}" raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
end end
end end
@@ -866,7 +807,7 @@ defmodule Access do
iex> get_in([:a, :b, :c], [Access.at!(2)]) iex> get_in([:a, :b, :c], [Access.at!(2)])
:c :c
iex> get_in([:a, :b, :c], [Access.at!(3)]) iex> get_in([:a, :b, :c], [Access.at!(3)])
** (Enum.OutOfBoundsError) out of bounds error at position 3 when traversing enumerable [:a, :b, :c] ** (Enum.OutOfBoundsError) out of bounds error
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@@ -878,14 +819,12 @@ defmodule Access do
defp at!(:get, data, index, next) when is_list(data) do defp at!(:get, data, index, next) when is_list(data) do
case Enum.fetch(data, index) do case Enum.fetch(data, index) do
{:ok, value} -> next.(value) {:ok, value} -> next.(value)
:error -> raise Enum.OutOfBoundsError, index: index, enumerable: data :error -> raise Enum.OutOfBoundsError
end end
end end
defp at!(:get_and_update, data, index, next) when is_list(data) do defp at!(:get_and_update, data, index, next) when is_list(data) do
get_and_update_at(data, index, next, [], fn -> get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
raise Enum.OutOfBoundsError, index: index, enumerable: data
end)
end end
defp at!(_op, data, _index, _next) do defp at!(_op, data, _index, _next) do
@@ -917,7 +856,7 @@ defmodule Access do
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name]) iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]} {["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
When no match is found, an empty list is returned and the update function is never called: When no match is found, an empty list is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
@@ -927,6 +866,11 @@ defmodule Access do
...> end) ...> end)
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]} {[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.filter(5)])
** (FunctionClauseError) no function clause matching in Access.filter/1
An error is raised if the accessed structure is not a list: An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)]) iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
@@ -934,13 +878,13 @@ defmodule Access do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec filter((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: list) @spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
def filter(func) when is_function(func) do def filter(func) when is_function(func) do
fn op, data, next -> filter(op, data, func, next) end fn op, data, next -> filter(op, data, func, next) end
end end
defp filter(:get, data, func, next) when is_list(data) do defp filter(:get, data, func, next) when is_list(data) do
for elem <- data, func.(elem), do: next.(elem) data |> Enum.filter(func) |> Enum.map(next)
end end
defp filter(:get_and_update, data, func, next) when is_list(data) do defp filter(:get_and_update, data, func, next) when is_list(data) do
@@ -1035,12 +979,12 @@ defmodule Access do
end end
defp slice(:get_and_update, data, range, next) when is_list(data) do defp slice(:get_and_update, data, range, next) when is_list(data) do
%Range{first: first, last: last, step: step} = normalize_range(range, data) range = normalize_range(range, data)
if first > last do if range.first > range.last do
{[], data} {[], data}
else else
get_and_update_slice(data, first, last, step, next, [], [], 0) get_and_update_slice(data, range, next, [], [], 0)
end end
end end
@@ -1048,93 +992,6 @@ defmodule Access do
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}" raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
end end
@doc """
Returns a function that accesses all values in a map or a keyword list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users, [Access.values(), :age]) |> Enum.sort()
[23, 27]
iex> update_in(users, [Access.values(), :age], fn age -> age + 1 end)
%{"john" => %{age: 28}, "meg" => %{age: 24}}
iex> put_in(users, [Access.values(), :planet], "Earth")
%{"john" => %{age: 27, planet: "Earth"}, "meg" => %{age: 23, planet: "Earth"}}
Values in keyword lists can be accessed as well:
iex> users = [john: %{age: 27}, meg: %{age: 23}]
iex> get_and_update_in(users, [Access.values(), :age], fn age -> {age, age + 1} end)
{[27, 23], [john: %{age: 28}, meg: %{age: 24}]}
By returning `:pop` from an accessor function, you can remove the accessed key and value
from the map or keyword list:
iex> require Integer
iex> numbers = [one: 1, two: 2, three: 3, four: 4]
iex> get_and_update_in(numbers, [Access.values()], fn num ->
...> if Integer.is_even(num), do: :pop, else: {num, to_string(num)}
...> end)
{[1, 2, 3, 4], [one: "1", three: "3"]}
An error is raised if the accessed structure is not a map nor a keyword list:
iex> get_in([1, 2, 3], [Access.values()])
** (RuntimeError) Access.values/0 expected a map or a keyword list, got: [1, 2, 3]
"""
@doc since: "1.19.0"
@spec values() :: Access.access_fun(data :: map() | keyword(), current_value :: list())
def values do
&values/3
end
defp values(:get, data = %{}, next) do
Enum.map(data, fn {_key, value} -> next.(value) end)
end
defp values(:get_and_update, data = %{}, next) do
{reverse_gets, updated_data} =
Enum.reduce(data, {[], %{}}, fn {key, value}, {gets, data_acc} ->
case next.(value) do
{get, update} -> {[get | gets], Map.put(data_acc, key, update)}
:pop -> {[value | gets], data_acc}
end
end)
{Enum.reverse(reverse_gets), updated_data}
end
defp values(op, data = [], next) do
values_keyword(op, data, next)
end
defp values(op, data = [{key, _value} | _tail], next) when is_atom(key) do
values_keyword(op, data, next)
end
defp values(_op, data, _next) do
raise "Access.values/0 expected a map or a keyword list, got: #{inspect(data)}"
end
defp values_keyword(:get, data, next) do
Enum.map(data, fn {key, value} when is_atom(key) -> next.(value) end)
end
defp values_keyword(:get_and_update, data, next) do
{reverse_gets, reverse_updated_data} =
Enum.reduce(data, {[], []}, fn {key, value}, {gets, data_acc} when is_atom(key) ->
case next.(value) do
{get, update} -> {[get | gets], [{key, update} | data_acc]}
:pop -> {[value | gets], data_acc}
end
end)
{Enum.reverse(reverse_gets), Enum.reverse(reverse_updated_data)}
end
defp normalize_range(%Range{first: first, last: last, step: step}, list) defp normalize_range(%Range{first: first, last: last, step: step}, list)
when first < 0 or last < 0 do when first < 0 or last < 0 do
count = length(list) count = length(list)
@@ -1145,23 +1002,16 @@ defmodule Access do
defp normalize_range(range, _list), do: range defp normalize_range(range, _list), do: range
defp get_and_update_slice(rest, _first, last, _step, _next, updates, gets, index) defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
when index > last do if index in range do
{:lists.reverse(gets), :lists.reverse(updates, rest)}
end
defp get_and_update_slice([head | rest], first, last, step, next, updates, gets, index) do
if index >= first and rem(index - first, step) == 0 do
case next.(head) do case next.(head) do
:pop -> :pop ->
get_and_update_slice(rest, first, last, step, next, updates, [head | gets], index + 1) get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
{get, update} -> {get, update} ->
get_and_update_slice( get_and_update_slice(
rest, rest,
first, range,
last,
step,
next, next,
[update | updates], [update | updates],
[get | gets], [get | gets],
@@ -1169,11 +1019,11 @@ defmodule Access do
) )
end end
else else
get_and_update_slice(rest, first, last, step, next, [head | updates], gets, index + 1) get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
end end
end end
defp get_and_update_slice([], _first, _last, _step, _next, updates, gets, _index) do defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)} {:lists.reverse(gets), :lists.reverse(updates)}
end end
@@ -1188,9 +1038,9 @@ defmodule Access do
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name]) iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
"francine" "francine"
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev -> iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
...> {prev, String.upcase(prev)} ...> {prev, String.upcase(prev)}
...> end) ...> end)
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]} {"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
`find/1` can also be used to pop the first found element out of a list or `find/1` can also be used to pop the first found element out of a list or
@@ -1200,7 +1050,7 @@ defmodule Access do
iex> pop_in(list, [Access.find(&(&1.salary <= 40))]) iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]} {%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called: When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}] iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name]) iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
@@ -1210,9 +1060,14 @@ defmodule Access do
...> end) ...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]} {nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list: An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)]) iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
** (RuntimeError) Access.find/1 expected a list, got: %{} ** (RuntimeError) Access.find/1 expected a list, got: %{}
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
+8 -12
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Agent do defmodule Agent do
@moduledoc """ @moduledoc """
Agents are a simple abstraction around state. Agents are a simple abstraction around state.
@@ -211,7 +207,7 @@ defmodule Agent do
@doc false @doc false
defmacro __using__(opts) do defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do unless Module.has_attribute?(__MODULE__, :doc) do
@doc """ @doc """
Returns a specification to start this module under a supervisor. Returns a specification to start this module under a supervisor.
@@ -288,7 +284,7 @@ defmodule Agent do
instead of an anonymous function; `fun` in `module` will be called with the instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state. given arguments `args` to initialize the state.
""" """
@spec start_link(module, atom, [term], GenServer.options()) :: on_start @spec start_link(module, atom, [any], GenServer.options()) :: on_start
def start_link(module, fun, args, options \\ []) do def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options) GenServer.start_link(Agent.Server, {module, fun, args}, options)
end end
@@ -315,7 +311,7 @@ defmodule Agent do
See `start_link/4` for more information. See `start_link/4` for more information.
""" """
@spec start(module, atom, [term], GenServer.options()) :: on_start @spec start(module, atom, [any], GenServer.options()) :: on_start
def start(module, fun, args, options \\ []) do def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options) GenServer.start(Agent.Server, {module, fun, args}, options)
end end
@@ -327,7 +323,7 @@ defmodule Agent do
passing the agent state. The result of the function invocation is passing the agent state. The result of the function invocation is
returned from this function. returned from this function.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -352,7 +348,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first instead of an anonymous function. The state is added as first
argument to the given list of arguments. argument to the given list of arguments.
""" """
@spec get(agent, module, atom, [term], timeout) :: term @spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do def get(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get, {module, fun, args}}, timeout) GenServer.call(agent, {:get, {module, fun, args}}, timeout)
end end
@@ -366,7 +362,7 @@ defmodule Agent do
elements, the first being the value to return (that is, the "get" value) elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent. and the second one being the new state of the agent.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
@@ -393,7 +389,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first instead of an anonymous function. The state is added as first
argument to the given list of arguments. argument to the given list of arguments.
""" """
@spec get_and_update(agent, module, atom, [term], timeout) :: term @spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout) GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end end
@@ -407,7 +403,7 @@ defmodule Agent do
This function always returns `:ok`. This function always returns `:ok`.
`timeout` is a non-negative integer which specifies how many `timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller is received within the specified time, the function call fails and the caller
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Agent.Server do defmodule Agent.Server do
@moduledoc false @moduledoc false
+42 -39
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Application do defmodule Application do
@moduledoc """ @moduledoc """
A module for working with applications and defining application callbacks. A module for working with applications and defining application callbacks.
@@ -58,12 +54,12 @@ defmodule Application do
You can also change the application environment dynamically by using functions You can also change the application environment dynamically by using functions
such as `put_env/3` and `delete_env/2`. such as `put_env/3` and `delete_env/2`.
> #### Application environment in libraries {: .info} > #### Environment in libraries {: .tip}
> >
> If you are writing a library to be used by other developers, > The config files `config/config.exs` and `config/runtime.exs`
> it is generally recommended to avoid the application environment, as the > are rarely used by libraries. Libraries typically define their environment
> application environment is effectively a global storage. For more information, > in the `application/0` function of their `mix.exs`. Configuration files
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries). > are rather used by applications to configure their libraries.
> #### Reading the environment of other applications {: .warning} > #### Reading the environment of other applications {: .warning}
> >
@@ -247,7 +243,7 @@ defmodule Application do
invoked if it hasn't been done yet. Then, it checks if the dependencies listed invoked if it hasn't been done yet. Then, it checks if the dependencies listed
in the `applications` key of the resource file are already started. Having at in the `applications` key of the resource file are already started. Having at
least one dependency not started is an error condition. Functions like least one dependency not started is an error condition. Functions like
`ensure_all_started/1` take care of starting an application and all of its `ensure_all_started/1` takes care of starting an application and all of its
dependencies for you. dependencies for you.
If the application does not have a callback module configured, starting is If the application does not have a callback module configured, starting is
@@ -507,7 +503,7 @@ defmodule Application do
of all loaded applications. Returns `nil` if of all loaded applications. Returns `nil` if
the module is not listed in any application spec. the module is not listed in any application spec.
""" """
@spec get_application(module) :: app | nil @spec get_application(atom) :: atom | nil
def get_application(module) when is_atom(module) do def get_application(module) when is_atom(module) do
case :application.get_application(module) do case :application.get_application(module) do
{:ok, app} -> app {:ok, app} -> app
@@ -677,16 +673,23 @@ defmodule Application do
> You must use this function to read only your own application > You must use this function to read only your own application
> environment. Do not read the environment of other applications. > environment. Do not read the environment of other applications.
> #### Application environment in libraries {: .info}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read our [library guidelines](library-guidelines.md).
## Examples ## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications. `get_env/3` is commonly used to read the configuration of your OTP applications.
Since Mix configurations are commonly used to configure applications (including Since Mix configurations are commonly used to configure applications, we will use
your dependencies), we will use this as a point of illustration. this as a point of illustration.
Consider a new application `:my_app`. `:my_app` contains a database engine which Consider a new application `:my_app`. `:my_app` contains a database engine which
supports a pool of databases. The database engine needs to know the configuration for supports a pool of databases. The database engine needs to know the configuration for
each of those databases, and that configuration is supplied by key-value pairs in each of those databases, and that configuration is supplied by key-value pairs in
environment of `:my_app`. For example, your `config/runtime.exs` file might have: environment of `:my_app`.
config :my_app, Databases.RepoOne, config :my_app, Databases.RepoOne,
# A database configuration # A database configuration
@@ -696,7 +699,7 @@ defmodule Application do
config :my_app, Databases.RepoTwo, config :my_app, Databases.RepoTwo,
# Another database configuration (for the same OTP app) # Another database configuration (for the same OTP app)
ip: "localhost", ip: "localhost",
port: 20_717 port: 20717
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo] config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
@@ -714,11 +717,6 @@ defmodule Application do
config = Application.get_env(:my_app, Databases.RepoOne) config = Application.get_env(:my_app, Databases.RepoOne)
config[:ip] config[:ip]
The sample `config/runtime.exs` above could be used both for `:my_app` to
configure itself but also to allow any application that depends on `:my_app`
to configure how it works. However, one should keep in mind the caveats described
in the `Application` module documentation: the application environment is global
state which should be avoided if possible.
""" """
@spec get_env(app, key, value) :: value @spec get_env(app, key, value) :: value
def get_env(app, key, default \\ nil) when is_atom(app) do def get_env(app, key, default \\ nil) when is_atom(app) do
@@ -798,12 +796,6 @@ defmodule Application do
@doc """ @doc """
Puts the `value` in `key` for the given `app`. Puts the `value` in `key` for the given `app`.
> #### Compile environment {: .warning}
>
> Do not use this function to change environment variables read
> via `Application.compile_env/2`. The compile environment must
> be exclusively set before compilation, in your config files.
## Options ## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds) * `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
@@ -819,7 +811,7 @@ defmodule Application do
stick after the application is loaded and also on application reload. stick after the application is loaded and also on application reload.
""" """
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok @spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do def put_env(app, key, value, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key) maybe_warn_on_app_env_key(app, key)
:application.set_env(app, key, value, opts) :application.set_env(app, key, value, opts)
end end
@@ -861,7 +853,7 @@ defmodule Application do
It receives the same options as `put_env/4`. Returns `:ok`. It receives the same options as `put_env/4`. Returns `:ok`.
""" """
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok @spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do def delete_env(app, key, opts \\ []) when is_atom(app) do
maybe_warn_on_app_env_key(app, key) maybe_warn_on_app_env_key(app, key)
:application.unset_env(app, key, opts) :application.unset_env(app, key, opts)
end end
@@ -908,16 +900,17 @@ defmodule Application do
@doc """ @doc """
Ensures the given `app` or `apps` and their child applications are started. Ensures the given `app` or `apps` and their child applications are started.
The second argument is either the `t:restart_type/0` (for consistency with The second argument is either the `t:restart_type/1` (for consistency with
`start/2`) or a keyword list. `start/2`) or a keyword list.
## Options ## Options
* `:type` - if the application should be started `:temporary` (default), * `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/0` for more information. `:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially * `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`). (`:serial`, default) or concurrently (`:concurrent`). This option requires
Erlang/OTP 26+.
""" """
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) :: @spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
@@ -926,11 +919,11 @@ defmodule Application do
{:ok, [app]} | {:error, term} {:ok, [app]} | {:error, term}
def ensure_all_started(app_or_apps, type_or_opts \\ []) def ensure_all_started(app_or_apps, type_or_opts \\ [])
def ensure_all_started(app_or_apps, type) when is_atom(type) do def ensure_all_started(app, type) when is_atom(type) do
ensure_all_started(app_or_apps, type: type) ensure_all_started(app, type: type)
end end
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do def ensure_all_started(app, opts) when is_atom(app) do
ensure_all_started([app], opts) ensure_all_started([app], opts)
end end
@@ -938,7 +931,18 @@ defmodule Application do
def ensure_all_started(apps, opts) when is_list(apps) and is_list(opts) do def ensure_all_started(apps, opts) when is_list(apps) and is_list(opts) do
opts = Keyword.validate!(opts, type: :temporary, mode: :serial) opts = Keyword.validate!(opts, type: :temporary, mode: :serial)
:application.ensure_all_started(apps, opts[:type], opts[:mode])
if function_exported?(:application, :ensure_all_started, 3) do
:application.ensure_all_started(apps, opts[:type], opts[:mode])
else
# TODO: Remove this clause when we require Erlang/OTP 26+
Enum.reduce_while(apps, {:ok, []}, fn app, {:ok, acc} ->
case :application.ensure_all_started(app, opts[:type]) do
{:ok, apps} -> {:cont, {:ok, apps ++ acc}}
{:error, e} -> {:halt, {:error, e}}
end
end)
end
end end
@doc """ @doc """
@@ -996,7 +1000,7 @@ defmodule Application do
end end
@doc """ @doc """
Gets the directory for `app`. Gets the directory for app.
This information is returned based on the code path. Here is an This information is returned based on the code path. Here is an
example: example:
@@ -1061,8 +1065,7 @@ defmodule Application do
Returns a list with information about the applications which are currently running. Returns a list with information about the applications which are currently running.
""" """
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}] @spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
def started_applications(timeout \\ 5000) def started_applications(timeout \\ 5000) do
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
:application.which_applications(timeout) :application.which_applications(timeout)
end end
@@ -1077,7 +1080,7 @@ defmodule Application do
@doc """ @doc """
Formats the error reason returned by `start/2`, Formats the error reason returned by `start/2`,
`ensure_started/2`, `stop/1`, `load/1` and `unload/1`, `ensure_started/2`, `stop/1`, `load/1` and `unload/1`,
and returns a string. returns a string.
""" """
@spec format_error(any) :: String.t() @spec format_error(any) :: String.t()
def format_error(reason) do def format_error(reason) do
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Atom do defmodule Atom do
@moduledoc """ @moduledoc """
Atoms are constants whose values are their own name. Atoms are constants whose values are their own name.
+155 -812
View File
File diff suppressed because it is too large Load Diff
+1 -6
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Behaviour do defmodule Behaviour do
@moduledoc """ @moduledoc """
Mechanism for handling behaviours. Mechanism for handling behaviours.
@@ -13,8 +9,7 @@ defmodule Behaviour do
attributes. attributes.
Instead of `MyModule.__behaviour__(:callbacks)`, Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1` `MyModule.behaviour_info(:callbacks)` can be used.
is documented in `Module`.
""" """
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead" @moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Bitwise do defmodule Bitwise do
@moduledoc """ @moduledoc """
A set of functions that perform calculations on bits. A set of functions that perform calculations on bits.
+49 -157
View File
@@ -1,10 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do defmodule Calendar do
@strftime_max_width 1024
@moduledoc """ @moduledoc """
This module defines the responsibilities for working with This module defines the responsibilities for working with
calendars, dates, times and datetimes in Elixir. calendars, dates, times and datetimes in Elixir.
@@ -60,20 +54,9 @@ defmodule Calendar do
@typedoc """ @typedoc """
Microseconds with stored precision. Microseconds with stored precision.
`value` always represents the total value in microseconds. The precision represents the number of digits that must be used when
The `precision` represents the number of digits that must be used when
representing the microseconds to external format. If the precision is `0`, representing the microseconds to external format. If the precision is `0`,
it means microseconds must be skipped. If the precision is `6`, it means it means microseconds must be skipped.
that `value` represents exactly the number of microseconds to be used.
## Examples
* `{0, 0}` means no microseconds.
* `{1, 6}` means 1µs.
* `{1000, 6}` means 1000µs (which is 1ms but measured at the microsecond precision).
* `{1000, 3}` means 1ms (which is measured at the millisecond precision).
""" """
@type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer} @type microsecond :: {value :: non_neg_integer, precision :: non_neg_integer}
@@ -106,7 +89,6 @@ defmodule Calendar do
@typedoc "Any map or struct that contains the time fields." @typedoc "Any map or struct that contains the time fields."
@type time :: %{ @type time :: %{
optional(any) => any, optional(any) => any,
calendar: calendar,
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
@@ -165,22 +147,6 @@ defmodule Calendar do
""" """
@type time_zone_database :: module() @type time_zone_database :: module()
@typedoc """
Options for formatting dates and times with `strftime/3`.
"""
@type strftime_opts :: [
preferred_datetime: String.t(),
preferred_date: String.t(),
preferred_time: String.t(),
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_month_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
abbreviated_day_of_week_names:
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
]
@doc """ @doc """
Returns how many days there are in the given month of the given year. Returns how many days there are in the given month of the given year.
""" """
@@ -206,15 +172,6 @@ defmodule Calendar do
`starting_on` represents the starting day of the week. All `starting_on` represents the starting day of the week. All
calendars must support at least the `:default` value. They may calendars must support at least the `:default` value. They may
also support other values representing their days of the week. also support other values representing their days of the week.
The value of `day_of_week` is an ordinal number meaning that a
value of `1` is defined to mean "first day of the week". It is
specifically not defined to mean `1` is `Monday`.
It is a requirement that `first_day_of_week` is less than `last_day_of_week`
and that `day_of_week` must be within that range. Therefore it can be said
that `day_of_week in first_day_of_week..last_day_of_week//1` must be
`true` for all values of `day_of_week`.
""" """
@callback day_of_week(year, month, day, starting_on :: :default | atom) :: @callback day_of_week(year, month, day, starting_on :: :default | atom) ::
{day_of_week(), first_day_of_week :: non_neg_integer(), {day_of_week(), first_day_of_week :: non_neg_integer(),
@@ -296,7 +253,7 @@ defmodule Calendar do
@callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond} @callback time_from_day_fraction(day_fraction) :: {hour, minute, second, microsecond}
@doc """ @doc """
Defines the rollover moment for the calendar. Define the rollover moment for the calendar.
This is the moment, in your calendar, when the current day ends This is the moment, in your calendar, when the current day ends
and the next day starts. and the next day starts.
@@ -382,13 +339,13 @@ defmodule Calendar do
@callback iso_days_to_end_of_day(iso_days) :: iso_days @callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """ @doc """
Shifts date by the given duration according to its calendar. Shifts date by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day} @callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """ @doc """
Shifts naive datetime by the given duration according to its calendar. Shifts naive datetime by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_naive_datetime( @callback shift_naive_datetime(
@@ -403,7 +360,7 @@ defmodule Calendar do
) :: {year, month, day, hour, minute, second, microsecond} ) :: {year, month, day, hour, minute, second, microsecond}
@doc """ @doc """
Shifts time by the given duration according to its calendar. Shifts time by given duration according to its calendar.
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) :: @callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
@@ -495,30 +452,25 @@ defmodule Calendar do
it can't contain the `%X` format and defaults to `"%H:%M:%S"` it can't contain the `%X` format and defaults to `"%H:%M:%S"`
if the option is not received if the option is not received
* `:am_pm_names` - a function that receives either `:am` or `:pm` * `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
(and also the datetime if the function is arity/2) and returns
the name of the period of the day, if the option is not received it defaults 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 to a function that returns `"am"` and `"pm"`, respectively
* `:month_names` - a function that receives a number (and also the * `:month_names` - a function that receives a number and returns the name of
datetime if the function is arity/2) and returns the name of
the corresponding month, if the option is not received it defaults to a the corresponding month, if the option is not received it defaults to a
function that returns the month names in English function that returns the month names in English
* `:abbreviated_month_names` - a function that receives a number (and also * `:abbreviated_month_names` - a function that receives a number and returns the
the datetime if the function is arity/2) and returns the
abbreviated name of the corresponding month, if the option is not received it 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 defaults to a function that returns the abbreviated month names in English
* `:day_of_week_names` - a function that receives a number and (and also the * `:day_of_week_names` - a function that receives a number and returns the name of
datetime if the function is arity/2) returns the name of
the corresponding day of week, if the option is not received it defaults to a 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 function that returns the day of week names in English
* `:abbreviated_day_of_week_names` - a function that receives a number (and also * `:abbreviated_day_of_week_names` - a function that receives a number and returns
the datetime if the function is arity/2) and returns the abbreviated name of the abbreviated name of the corresponding day of week, if the option is not received
the corresponding day of week, if the option is not received it defaults to a it defaults to a function that returns the abbreviated day of week names in English
function that returns the abbreviated day of week names in English
## Formatting syntax ## Formatting syntax
@@ -532,7 +484,6 @@ defmodule Calendar do
* `%`: indicates the start of a formatted section * `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below) * `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section * `<width>`: a number indicating the minimum size of the formatted section
(maximum #{@strftime_max_width})
* `<format>`: the format itself (see below) * `<format>`: the format itself (see below)
### Accepted padding options ### Accepted padding options
@@ -553,7 +504,7 @@ defmodule Calendar do
B | Full month name | January B | Full month name | January
c | Preferred date+time representation | 2018-10-17 12:34:56 c | Preferred date+time representation | 2018-10-17 12:34:56
d | Day of the month | 01, 31 d | Day of the month | 01, 31
f | Microseconds (uses its precision for width and padding) | 000000, 999999, 0123 f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
H | Hour using a 24-hour clock | 00, 23 H | Hour using a 24-hour clock | 00, 23
I | Hour using a 12-hour clock | 01, 12 I | Hour using a 12-hour clock | 01, 12
j | Day of the year | 001, 366 j | Day of the year | 001, 366
@@ -563,11 +514,11 @@ defmodule Calendar do
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 q | Quarter | 1, 2, 3, 4
s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877 s | Number of seconds since the Epoch, 1970-01-01 00:00:00+0000 (UTC) | 1565888877
S | Second | 00, 59 S | Second | 00, 59, 60
u | Day of the week | 1 (Monday), 7 (Sunday) u | Day of the week | 1 (Monday), 7 (Sunday)
x | Preferred date (without time) representation | 2018-10-17 x | Preferred date (without time) representation | 2018-10-17
X | Preferred time (without date) representation | 12:34:56 X | Preferred time (without date) representation | 12:34:56
y | Year as 2-digits | -01, 01, 86, 18 y | Year as 2-digits | 01, 01, 86, 18
Y | Year | -0001, 0001, 1986 Y | Year | -0001, 0001, 1986
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530 z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
Z | Time zone abbreviation (empty string if naive) | CET, BRST Z | Time zone abbreviation (empty string if naive) | CET, BRST
@@ -575,12 +526,6 @@ defmodule Calendar do
Any other character will be interpreted as an invalid format and raise an error. Any other character will be interpreted as an invalid format and raise an error.
### `%f` Microseconds
`%f` does not support width and padding modifiers. It will be formatted by truncating
the microseconds to the precision of the `microseconds` field of the struct, with a
minimum precision of 1.
## Examples ## Examples
Without user options: Without user options:
@@ -624,20 +569,9 @@ defmodule Calendar do
...>) ...>)
"серпень" "серпень"
Microsecond formatting:
iex> Calendar.strftime(~U[2019-08-26 13:52:06Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.0"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.048"
iex> Calendar.strftime(~U[2019-08-26 13:52:06.048531Z], "%y-%m-%d %H:%M:%S.%f")
"19-08-26 13:52:06.048531"
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec strftime(map(), String.t(), strftime_opts()) :: String.t() @spec strftime(map(), String.t(), keyword()) :: String.t()
def strftime(date_or_time_or_datetime, string_format, user_options \\ []) 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 when is_map(date_or_time_or_datetime) and is_binary(string_format) do
parse( parse(
@@ -671,13 +605,9 @@ defmodule Calendar do
end end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
width = (width || 0) * 10 + (digit - ?0) new_width = (width || 0) * 10 + (digit - ?0)
if width > @strftime_max_width do parse_modifiers(rest, new_width, pad, parser_data)
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
end
parse_modifiers(rest, width, pad, parser_data)
end end
# set default padding if none was specified # set default padding if none was specified
@@ -694,12 +624,12 @@ defmodule Calendar do
format_modifiers(rest, width, pad, datetime, format_options, acc) format_modifiers(rest, width, pad, datetime, format_options, acc)
end end
defp am_pm(hour, format_options, datetime) when hour > 11 do defp am_pm(hour, format_options) when hour > 11 do
apply_format(:pm, format_options.am_pm_names, datetime) format_options.am_pm_names.(:pm)
end end
defp am_pm(hour, format_options, datetime) when hour <= 11 do defp am_pm(hour, format_options) when hour <= 11 do
apply_format(:am, format_options.am_pm_names, datetime) format_options.am_pm_names.(:am)
end end
defp default_pad(format) when format in ~c"aAbBpPZ", do: ?\s defp default_pad(format) when format in ~c"aAbBpPZ", do: ?\s
@@ -712,7 +642,7 @@ defmodule Calendar do
# Literally just % # Literally just %
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
parse(rest, datetime, format_options, [pad_leading_ascii("%", width, pad) | acc]) parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
end end
# Abbreviated name of day # Abbreviated name of day
@@ -720,7 +650,7 @@ defmodule Calendar do
result = result =
datetime datetime
|> Date.day_of_week() |> Date.day_of_week()
|> apply_format(format_options.abbreviated_day_of_week_names, datetime) |> format_options.abbreviated_day_of_week_names.()
|> pad_leading(width, pad) |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
@@ -731,7 +661,7 @@ defmodule Calendar do
result = result =
datetime datetime
|> Date.day_of_week() |> Date.day_of_week()
|> apply_format(format_options.day_of_week_names, datetime) |> format_options.day_of_week_names.()
|> pad_leading(width, pad) |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
@@ -741,7 +671,7 @@ defmodule Calendar do
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
result = result =
datetime.month datetime.month
|> apply_format(format_options.abbreviated_month_names, datetime) |> format_options.abbreviated_month_names.()
|> pad_leading(width, pad) |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
@@ -749,10 +679,7 @@ defmodule Calendar do
# Full month name # Full month name
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
datetime.month
|> apply_format(format_options.month_names, datetime)
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -781,7 +708,7 @@ defmodule Calendar do
# Day of the month # Day of the month
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.day |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -800,45 +727,37 @@ defmodule Calendar do
# Hour using a 24-hour clock # Hour using a 24-hour clock
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.hour |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Hour using a 12-hour clock # Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
(rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the year # Day of the year
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Month # Month
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.month |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Minute # Minute
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.minute |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# "AM" or "PM" (noon is "PM", midnight as "AM") # "AM" or "PM" (noon is "PM", midnight as "AM")
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
datetime.hour
|> am_pm(format_options, datetime)
|> String.upcase()
|> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -847,7 +766,7 @@ defmodule Calendar do
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
result = result =
datetime.hour datetime.hour
|> am_pm(format_options, datetime) |> am_pm(format_options)
|> String.downcase() |> String.downcase()
|> pad_leading(width, pad) |> pad_leading(width, pad)
@@ -856,23 +775,19 @@ defmodule Calendar do
# Quarter # Quarter
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Second # Second
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
result = datetime.second |> Integer.to_string() |> pad_leading_ascii(width, pad) result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Day of the week # Day of the week
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading_ascii(width, pad)
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -922,25 +837,20 @@ defmodule Calendar do
# Year as 2-digits # Year as 2-digits
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
result = result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
if datetime.year < 0 do
[?- | -datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)]
else
datetime.year |> rem(100) |> Integer.to_string() |> pad_leading_ascii(width, pad)
end
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
# Year # Year
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
result = {sign, year} =
if datetime.year < 0 do if datetime.year < 0 do
[?- | -datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad)] {?-, -datetime.year}
else else
datetime.year |> Integer.to_string() |> pad_leading_ascii(width, pad) {[], datetime.year}
end end
result = [sign | year |> Integer.to_string() |> pad_leading(width, pad)]
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -987,7 +897,7 @@ defmodule Calendar do
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60)) Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
sign = if utc_offset + std_offset >= 0, do: "+", else: "-" sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
result = "#{sign}#{pad_leading_ascii(offset_number, width, pad)}" result = "#{sign}#{pad_leading(offset_number, width, pad)}"
parse(rest, datetime, format_options, [result | acc]) parse(rest, datetime, format_options, [result | acc])
end end
@@ -1006,19 +916,13 @@ defmodule Calendar do
raise ArgumentError, "invalid strftime format: %#{next}" raise ArgumentError, "invalid strftime format: %#{next}"
end end
defp pad_preferred(result, width, pad) do defp pad_preferred(result, width, pad) when length(result) < width do
result pad_preferred([pad | result], width, pad)
|> IO.iodata_to_binary()
|> pad_leading(width, pad)
end end
defp pad_preferred(result, _width, _pad), do: result
defp pad_leading(string, count, padding) do defp pad_leading(string, count, padding) do
to_pad = count - String.length(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end
# Similar to `pad_leading/3`, but only for strings that always ASCII-only
defp pad_leading_ascii(string, count, padding) do
to_pad = count - byte_size(string) to_pad = count - byte_size(string)
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
end end
@@ -1028,18 +932,6 @@ defmodule Calendar do
defp do_pad_leading(count, padding, acc), defp do_pad_leading(count, padding, acc),
do: do_pad_leading(count - 1, padding, [padding | acc]) do: do_pad_leading(count - 1, padding, [padding | acc])
defp apply_format(term, formatter, _datetime) when is_function(formatter, 1) do
formatter.(term)
end
defp apply_format(term, formatter, datetime) when is_function(formatter, 2) do
formatter.(term, datetime)
end
defp apply_format(_term, formatter, _datetime) do
raise ArgumentError, "formatter functions must be of arity 1 or 2, got: #{inspect(formatter)}"
end
defp options(user_options) do defp options(user_options) do
default_options = %{ default_options = %{
preferred_date: "%Y-%m-%d", preferred_date: "%Y-%m-%d",
+39 -55
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Date do defmodule Date do
@moduledoc """ @moduledoc """
A Date struct and functions. A Date struct and functions.
@@ -35,12 +31,11 @@ defmodule Date do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `Date` struct fields. For proper comparison between and based on the `Date` struct fields. For proper comparison between
dates, use the `compare/2`, `after?/2` and `before?/2` functions. dates, use the `compare/2` function. The existence of the `compare/2`
The existence of the `compare/2` function in this module also allows function in this module also allows using `Enum.min/2` and `Enum.max/2`
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and functions to get the minimum and maximum date of an `Enum`. For example:
maximum date of an `Enum`. For example:
iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date) iex> Enum.min([~D[2017-03-31], ~D[2017-04-01]], Date)
~D[2017-03-31] ~D[2017-03-31]
## Using epochs ## Using epochs
@@ -53,7 +48,7 @@ defmodule Date do
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01]) iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716 14716
iex> Date.add(~D[1970-01-01], 14_716) iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17] ~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2) iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
@@ -81,7 +76,7 @@ defmodule Date do
Ranges of dates can be increasing (`first <= last`) and are Ranges of dates can be increasing (`first <= last`) and are
always inclusive. For a decreasing range, use `range/3` with always inclusive. For a decreasing range, use `range/3` with
a step of -1 as third argument. a step of -1 as first argument.
## Examples ## Examples
@@ -160,7 +155,7 @@ defmodule Date do
) do ) do
raise ArgumentError, raise ArgumentError,
"both dates must have matching calendar and the step must be a " <> "both dates must have matching calendar and the step must be a " <>
"non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{inspect(step)}" "non-zero integer, got: #{inspect(first)}, #{inspect(last)}, #{step}"
end end
defp range(first, first_days, last, last_days, calendar, step) do defp range(first, first_days, last, last_days, calendar, step) do
@@ -193,8 +188,9 @@ defmodule Date do
end end
def utc_today(calendar) do def utc_today(calendar) do
%{year: year, month: month, day: day} = DateTime.utc_now(calendar) calendar
%Date{year: year, month: month, day: day, calendar: calendar} |> DateTime.utc_now()
|> DateTime.to_date()
end end
@doc """ @doc """
@@ -321,7 +317,7 @@ defmodule Date do
@doc """ @doc """
Converts the given date to a string according to its calendar. Converts the given date to a string according to its calendar.
## Examples ### Examples
iex> Date.to_string(~D[2000-02-28]) iex> Date.to_string(~D[2000-02-28])
"2000-02-28" "2000-02-28"
@@ -399,7 +395,7 @@ defmodule Date do
or other calendars in which the days also start at midnight. or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`. Attempting to convert dates from other calendars will raise an `ArgumentError`.
## Examples ### Examples
iex> Date.to_iso8601(~D[2000-02-28]) iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28" "2000-02-28"
@@ -422,7 +418,7 @@ defmodule Date do
def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do def to_iso8601(%{calendar: _} = date, format) when format in [:basic, :extended] do
date date
|> convert!(Calendar.ISO) |> convert!(Calendar.ISO)
|> to_iso8601(format) |> to_iso8601()
end end
@doc """ @doc """
@@ -556,18 +552,14 @@ defmodule Date do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt @spec compare(Calendar.date(), Calendar.date()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = date1, %{calendar: calendar} = date2) do
%{year: year1, month: month1, day: day1, calendar: calendar}, %{year: year1, month: month1, day: day1} = date1
%{year: year2, month: month2, day: day2, calendar: calendar} %{year: year2, month: month2, day: day2} = date2
) do
cond do case {{year1, month1, day1}, {year2, month2, day2}} do
year1 > year2 -> :gt {first, second} when first > second -> :gt
year1 < year2 -> :lt {first, second} when first < second -> :lt
month1 > month2 -> :gt _ -> :eq
month1 < month2 -> :lt
day1 > day2 -> :gt
day1 < day2 -> :lt
true -> :eq
end end
end end
@@ -637,7 +629,7 @@ defmodule Date do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene) iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
@@ -671,7 +663,7 @@ defmodule Date do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene) iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
@@ -695,15 +687,10 @@ defmodule Date do
@doc """ @doc """
Adds the number of days to the given `date`. Adds the number of days to the given `date`.
> #### Prefer `shift/2` {: .info} The days are counted as Gregorian days. The date is returned in the same
> calendar as it was given in.
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
>
> `add/2` always considers a day to be measured according to the
> `Calendar.ISO`.
The days are counted as Gregorian days, independent of the underlying To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
calendar. The date is returned in the same calendar as it was given in.
## Examples ## Examples
@@ -865,7 +852,7 @@ defmodule Date do
end end
@doc """ @doc """
Calculates the ordinal day of the week of a given `date`. Calculates the day of the week of a given `date`.
Returns the day of the week as an integer. For the ISO 8601 Returns the day of the week as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 7, where calendar (the default), it is an integer from 1 to 7, where
@@ -874,19 +861,10 @@ defmodule Date do
An optional `starting_on` value may be supplied, which An optional `starting_on` value may be supplied, which
configures the weekday the week starts on. The default value configures the weekday the week starts on. The default value
for it is `:default`, which translates to `:monday` for the for it is `:default`, which translates to `:monday` for the
built-in ISO 8601 calendar. Any other weekday may be used for built-in ISO calendar. Any other weekday may be given to.
`starting_on`, in such cases, that weekday will be considered the first
day of the week, and therefore it will be assigned the ordinal number 1.
The other calendars, the value returned is an ordinal day of week.
For example, `1` may mean "first day of the week" and `7` is
defined to mean "seventh day of the week". Custom calendars may
also accept their own variations of the `starting_on` parameter
with their own meaning.
## Examples ## Examples
# 2016-10-31 is a Monday and by default Monday is the first day of the week
iex> Date.day_of_week(~D[2016-10-31]) iex> Date.day_of_week(~D[2016-10-31])
1 1
iex> Date.day_of_week(~D[2016-11-01]) iex> Date.day_of_week(~D[2016-11-01])
@@ -896,7 +874,6 @@ defmodule Date do
iex> Date.day_of_week(~D[-0015-10-30]) iex> Date.day_of_week(~D[-0015-10-30])
3 3
# 2016-10-31 is a Monday but, as we start the week on Sunday, now it returns 2
iex> Date.day_of_week(~D[2016-10-31], :sunday) iex> Date.day_of_week(~D[2016-10-31], :sunday)
2 2
iex> Date.day_of_week(~D[2016-11-01], :sunday) iex> Date.day_of_week(~D[2016-11-01], :sunday)
@@ -1051,7 +1028,7 @@ defmodule Date do
@doc """ @doc """
Calculates the quarter of the year of a given `date`. Calculates the quarter of the year of a given `date`.
Returns the quarter of the year as an integer. For the ISO 8601 Returns the day of the year as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 4. calendar (the default), it is an integer from 1 to 4.
## Examples ## Examples
@@ -1096,7 +1073,13 @@ defmodule Date do
def year_of_era(date) def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.year_of_era(year, month, day) # TODO: Remove me on 1.17
# The behaviour implementation already warns on missing callback.
if function_exported?(calendar, :year_of_era, 3) do
calendar.year_of_era(year, month, day)
else
calendar.year_of_era(year)
end
end end
@doc """ @doc """
@@ -1176,11 +1159,12 @@ defmodule Date do
defimpl Inspect do defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar != Calendar.ISO or year in -9999..9999 do when year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]" "~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end end
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar == Calendar.ISO do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})" "Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end end
+48 -36
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Date.Range do defmodule Date.Range do
@moduledoc """ @moduledoc """
Returns an inclusive range between dates. Returns an inclusive range between dates.
@@ -37,19 +33,21 @@ defmodule Date.Range do
first_in_iso_days: first_days, first_in_iso_days: first_days,
last_in_iso_days: last_days, last_in_iso_days: last_days,
step: step step: step
}, } = range,
%Date{calendar: calendar} = date %Date{calendar: calendar} = date
) do ) do
{days, _} = Date.to_iso_days(date) {days, _} = Date.to_iso_days(date)
in_range? = cond do
if step > 0 do empty?(range) ->
first_days <= days and days <= last_days and rem(days - first_days, step) == 0 {:ok, false}
else
last_days <= days and days <= first_days and rem(days - first_days, step) == 0
end
{:ok, in_range?} 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}
end
end end
def member?(%Date.Range{step: _}, _) do def member?(%Date.Range{step: _}, _) do
@@ -57,20 +55,11 @@ defmodule Date.Range do
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
member? = def member?(
quote generated: true do %{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
member?( date_range,
%{ date
__struct__: Date.Range, ) do
first_in_iso_days: var!(first_days),
last_in_iso_days: var!(last_days)
} =
var!(date_range),
var!(date)
)
end
def unquote(member?) do
step = if first_days <= last_days, do: 1, else: -1 step = if first_days <= last_days, do: 1, else: -1
member?(Map.put(date_range, :step, step), date) member?(Map.put(date_range, :step, step), date)
end end
@@ -86,7 +75,7 @@ defmodule Date.Range do
step: step step: step
} = range } = range
) do ) do
{:ok, size(range), &slice(first + &1 * step, step * &3, &2, calendar)} {:ok, size(range), &slice(first + &1 * step, step + &3 - 1, &2, calendar)}
end end
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
@@ -102,7 +91,7 @@ defmodule Date.Range do
[date_from_iso_days(current, calendar)] [date_from_iso_days(current, calendar)]
end end
defp slice(current, step, remaining, calendar) when remaining > 1 do defp slice(current, step, remaining, calendar) do
[ [
date_from_iso_days(current, calendar) date_from_iso_days(current, calendar)
| slice(current + step, step, remaining - 1, calendar) | slice(current + step, step, remaining - 1, calendar)
@@ -178,12 +167,8 @@ defmodule Date.Range do
when step < 0 and first_days < last_days, when step < 0 and first_days < last_days,
do: 0 do: 0
defp size(%Date.Range{ defp size(%Date.Range{first_in_iso_days: first_days, last_in_iso_days: last_days, step: step}),
first_in_iso_days: first_days, do: abs(div(last_days - first_days, step)) + 1
last_in_iso_days: last_days,
step: step
}),
do: div(last_days - first_days, step) + 1
# TODO: Remove me on v2.0 # TODO: Remove me on v2.0
defp size( defp size(
@@ -193,16 +178,43 @@ defmodule Date.Range do
step = if first_days <= last_days, do: 1, else: -1 step = if first_days <= last_days, do: 1, else: -1
size(Map.put(date_range, :step, step)) size(Map.put(date_range, :step, step))
end end
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{step: _}), do: false
# TODO: Remove me on v2.0
defp empty?(
%{__struct__: Date.Range, first_in_iso_days: first_days, last_in_iso_days: last_days} =
date_range
) do
step = if first_days <= last_days, do: 1, else: -1
empty?(Map.put(date_range, :step, step))
end
end end
defimpl Inspect do defimpl Inspect do
import Kernel, except: [inspect: 2] import Kernel, except: [inspect: 2]
def inspect(%Date.Range{first: first, last: last, step: 1}, %Inspect.Opts{}) do def inspect(%Date.Range{first: first, last: last, step: 1}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")" "Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ")"
end end
def inspect(%Date.Range{first: first, last: last, step: step}, %Inspect.Opts{}) do def inspect(%Date.Range{first: first, last: last, step: step}, _) do
"Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})" "Date.range(" <> inspect(first) <> ", " <> inspect(last) <> ", #{step})"
end end
+113 -180
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule DateTime do defmodule DateTime do
@moduledoc """ @moduledoc """
A datetime implementation with a time zone. A datetime implementation with a time zone.
@@ -17,8 +13,8 @@ defmodule DateTime do
Remember, comparisons in Elixir using `==/2`, `>/2`, `</2` and friends Remember, comparisons in Elixir using `==/2`, `>/2`, `</2` and friends
are structural and based on the DateTime struct fields. For proper are structural and based on the DateTime struct fields. For proper
comparison between datetimes, use the `compare/2`, `after?/2` and `before?/2` functions. comparison between datetimes, use the `compare/2` function. The
The existence of the `compare/2` function in this module also allows existence of the `compare/2` function in this module also allows
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and using `Enum.min/2` and `Enum.max/2` functions to get the minimum and
maximum datetime of an `Enum`. For example: maximum datetime of an `Enum`. For example:
@@ -32,12 +28,7 @@ defmodule DateTime do
## Time zone database ## Time zone database
Many functions in this module require a time zone database. Many functions in this module require a time zone database.
A time zone database is a record of the UTC offsets that its locales have By default, it uses the default time zone database returned by
used at various times in the past, are using, and are expected to use in the
future.
Because those plans can change, it needs to be periodically updated.
By default, `DateTime` uses the default time zone database returned by
`Calendar.get_time_zone_database/0`, which defaults to `Calendar.get_time_zone_database/0`, which defaults to
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC" `Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
datetimes and returns `{:error, :utc_only_time_zone_database}` datetimes and returns `{:error, :utc_only_time_zone_database}`
@@ -52,7 +43,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) - * [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices recommended for embedded devices
To use one of them, first make sure it is added as a dependency in `mix.exs`. To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration: It can then be configured either via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase config :elixir, :time_zone_database, Tz.TimeZoneDatabase
@@ -180,7 +171,7 @@ defmodule DateTime do
since v1.15.0. since v1.15.0.
The default unit if none gets passed is `:native`, The default unit if none gets passed is `:native`,
which results in a default resolution of microseconds. which results on a default resolution of microseconds.
## Examples ## Examples
@@ -383,12 +374,13 @@ defmodule DateTime do
@doc """ @doc """
Converts the given Unix time to `DateTime`. Converts the given Unix time to `DateTime`.
The integer can be given in different unit, according to `System.convert_time_unit/3`, The integer can be given in different unit
and it will be converted to microseconds internally, which is the maximum precision according to `System.convert_time_unit/3` and it will
supported by `DateTime`. In other words, any precision higher than microseconds will be converted to microseconds internally. Up to
lead to truncation. 253402300799 seconds is supported.
Unix times are always in UTC. Therefore the DateTime will be returned in UTC. Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
## Examples ## Examples
@@ -720,9 +712,6 @@ defmodule DateTime do
Other time zone databases can be passed as argument or set globally. Other time zone databases can be passed as argument or set globally.
See the "Time zone database" section in the module docs. See the "Time zone database" section in the module docs.
Shifting to the `"Etc/UTC"` time zone always succeeds without
consulting the `time_zone_database`.
## Examples ## Examples
iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase) iex> {:ok, pacific_datetime} = DateTime.shift_zone(~U[2018-07-16 10:00:00Z], "America/Los_Angeles", FakeTimeZoneDatabase)
@@ -756,28 +745,6 @@ defmodule DateTime do
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
end end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, "Etc/UTC", _time_zone_db) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days_utc)
datetime = %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"
}
{:ok, datetime}
end
defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do defp shift_zone_for_iso_days_utc(iso_days_utc, calendar, precision, time_zone, time_zone_db) do
case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do case time_zone_db.time_zone_period_from_utc_iso_days(iso_days_utc, time_zone) do
{:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} -> {:ok, %{std_offset: std_offset, utc_offset: utc_offset, zone_abbr: zone_abbr}} ->
@@ -909,10 +876,8 @@ defmodule DateTime do
The `datetime` is expected to be using the ISO calendar The `datetime` is expected to be using the ISO calendar
with a year greater than or equal to 0. with a year greater than or equal to 0.
It will return the integer with the given unit, according It will return the integer with the given unit,
to `System.convert_time_unit/3`. If the given unit is different according to `System.convert_time_unit/3`.
than microseconds, the returned value will be either truncated
or padded accordingly.
## Examples ## Examples
@@ -1060,18 +1025,15 @@ defmodule DateTime do
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended" 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. 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. You can also optionally specify an offset for the formatted string.
If none is given, the one in the given `datetime` is used.
Only supports converting datetimes which are in the ISO calendar.
If another calendar is given, it is automatically converted to ISO.
It raises if not possible.
WARNING: the ISO 8601 datetime format does not contain the time zone nor WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such its abbreviation, which means information is lost when converting to such
format. format.
## Examples ### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET", iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0}, ...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1121,21 +1083,8 @@ defmodule DateTime do
@spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t() @spec to_iso8601(Calendar.datetime(), :basic | :extended, nil | integer()) :: String.t()
def to_iso8601(datetime, format \\ :extended, offset \\ nil) def to_iso8601(datetime, format \\ :extended, offset \\ nil)
def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset) def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, nil)
when format in [:extended, :basic] do when format in [:extended, :basic] do
datetime
|> to_iso8601_iodata(format, offset)
|> IO.iodata_to_binary()
end
def to_iso8601(%{calendar: _} = datetime, format, offset)
when format in [:extended, :basic] do
datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format, offset)
end
defp to_iso8601_iodata(datetime, format, nil) do
%{ %{
year: year, year: year,
month: month, month: month,
@@ -1149,51 +1098,35 @@ defmodule DateTime do
std_offset: std_offset std_offset: std_offset
} = datetime } = datetime
[ datetime_to_string(year, month, day, hour, minute, second, microsecond, format) <>
datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format), Calendar.ISO.offset_to_string(utc_offset, std_offset, time_zone, format)
Calendar.ISO.offset_to_iodata(utc_offset, std_offset, time_zone, format)
]
end end
defp to_iso8601_iodata( def to_iso8601(
%{microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime, %{calendar: Calendar.ISO, microsecond: {_, precision}, time_zone: "Etc/UTC"} = datetime,
format, format,
0 0
) do )
when format in [:extended, :basic] do
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, 0) {year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, 0)
[ datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
datetime_to_iodata( "Z"
year,
month,
day,
hour,
minute,
second,
{microsecond, precision},
format
),
?Z
]
end end
defp to_iso8601_iodata(datetime, format, offset) do def to_iso8601(%{calendar: Calendar.ISO} = datetime, format, offset)
when format in [:extended, :basic] do
{_, precision} = datetime.microsecond {_, precision} = datetime.microsecond
{year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, offset) {year, month, day, hour, minute, second, {microsecond, _}} = shift_by_offset(datetime, offset)
[ datetime_to_string(year, month, day, hour, minute, second, {microsecond, precision}, format) <>
datetime_to_iodata( Calendar.ISO.offset_to_string(offset, 0, nil, format)
year, end
month,
day, def to_iso8601(%{calendar: _} = datetime, format, offset) when format in [:extended, :basic] do
hour, datetime
minute, |> convert!(Calendar.ISO)
second, |> to_iso8601(format, offset)
{microsecond, precision},
format
),
Calendar.ISO.offset_to_iodata(offset, 0, nil, format)
]
end end
defp shift_by_offset(%{calendar: calendar} = datetime, offset) do defp shift_by_offset(%{calendar: calendar} = datetime, offset) do
@@ -1202,16 +1135,14 @@ defmodule DateTime do
datetime datetime
|> to_iso_days() |> to_iso_days()
# Subtract total original offset in order to get UTC and add the new offset # Subtract total original offset in order to get UTC and add the new offset
|> Calendar.ISO.add_time_unit_to_iso_days(offset - total_offset, :second) |> Calendar.ISO.add_day_fraction_to_iso_days(offset - total_offset, 86400)
|> calendar.naive_datetime_from_iso_days() |> calendar.naive_datetime_from_iso_days()
end end
defp datetime_to_iodata(year, month, day, hour, minute, second, microsecond, format) do defp datetime_to_string(year, month, day, hour, minute, second, microsecond, format) do
[ Calendar.ISO.date_to_string(year, month, day, format) <>
Calendar.ISO.date_to_iodata(year, month, day, format), "T" <>
?T, Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
]
end end
@doc """ @doc """
@@ -1321,9 +1252,9 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a number of Gregorian seconds to a `DateTime` struct. Converts a number of gregorian seconds to a `DateTime` struct.
The returned `DateTime` will have `UTC` timezone, if you want another timezone, please use The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
`DateTime.shift_zone/3`. `DateTime.shift_zone/3`.
## Examples ## Examples
@@ -1366,7 +1297,7 @@ defmodule DateTime do
end end
@doc """ @doc """
Converts a `DateTime` struct to a number of Gregorian seconds and microseconds. Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
## Examples ## Examples
@@ -1410,12 +1341,7 @@ defmodule DateTime do
@doc """ @doc """
Converts the given `datetime` to a string according to its calendar. Converts the given `datetime` to a string according to its calendar.
Unfortunately, there is no standard that specifies rendering of a ### Examples
datetime with its complete time zone information, so Elixir uses a
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET", iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0}, ...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
@@ -1601,15 +1527,15 @@ defmodule DateTime do
def diff(datetime1, datetime2, unit \\ :second) def diff(datetime1, datetime2, unit \\ :second)
def diff(datetime1, datetime2, :day) do def diff(datetime1, datetime2, :day) do
diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000) diff(datetime1, datetime2, :second) |> div(86400)
end end
def diff(datetime1, datetime2, :hour) do def diff(datetime1, datetime2, :hour) do
diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000) diff(datetime1, datetime2, :second) |> div(3600)
end end
def diff(datetime1, datetime2, :minute) do def diff(datetime1, datetime2, :minute) do
diff(datetime1, datetime2, :microsecond) |> div(60_000_000) diff(datetime1, datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -1624,60 +1550,42 @@ defmodule DateTime do
end end
naive_diff = naive_diff =
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) - (datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) (datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
offset_diff = utc_offset2 + std_offset2 - (utc_offset1 + std_offset1) offset_diff = utc_offset2 + std_offset2 - (utc_offset1 + std_offset1)
naive_diff + System.convert_time_unit(offset_diff, :second, unit)
System.convert_time_unit(naive_diff, :microsecond, unit) +
System.convert_time_unit(offset_diff, :second, unit)
end end
@doc """ @doc """
Adds a specified amount of time to a `DateTime`. Adds a specified amount of time to a `DateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`, Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
If the datetime is in the `"Etc/UTC"` time zone, this function This function always considers the unit to be computed according
always succeeds without consulting the `time_zone_database`. to the `Calendar.ISO`.
This function relies on a contiguous representation of time, This function relies on a contiguous representation of time,
ignoring timezone changes. For example, if you add one day when there ignoring the wall time and timezone changes. For example, if you add
are summer time/daylight saving time changes, it will also change the one day when there are summer time/daylight saving time changes,
time forward or backward by one hour, so the elapsed time is precisely it will also change the time forward or backward by one hour,
24 hours. Similarly, adding just a few seconds to a datetime just before so the elapsed time is precisely 24 hours. Similarly, adding just
"spring forward" can cause wall time to increase by more than an hour. a few seconds to a datetime just before "spring forward" can cause
wall time to increase by more than an hour.
While this means this function is precise in terms of elapsed time, While this means this function is precise in terms of elapsed time,
its result may be confusing in certain use cases. For example, if a its result may be misleading in certain use cases. For example, if a
user requests a meeting to happen every day at 15:00 and you use this user requests a meeting to happen every day at 15:00 and you use this
function to compute all future meetings by adding day after day, this function to compute all future meetings by adding day after day, this
function may change the meeting time to 14:00 or 16:00 if there are function may change the meeting time to 14:00 or 16:00 if there are
changes to the current timezone. changes to the current timezone. Computing of recurring datetimes is
not currently supported in Elixir's standard library but it is available
by third-party libraries.
In case you don't want these changes to happen automatically or you ### Examples
want to surface time zone conflicts to the user, you can add to
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## Examples
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase) iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase) iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
@@ -1705,6 +1613,8 @@ defmodule DateTime do
iex> result.microsecond iex> result.microsecond
{21000, 3} {21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
""" """
@doc since: "1.8.0" @doc since: "1.8.0"
@spec add( @spec add(
@@ -1772,16 +1682,13 @@ defmodule DateTime do
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
If the datetime is in the `"Etc/UTC"` time zone, this function
always succeeds without consulting the `time_zone_database`.
This operation is equivalent to shifting the datetime wall clock This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see (in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it on their watch), then applying the time zone offset to convert it
to UTC, and finally computing the new timezone in case of shifts. to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime. This ensures `shift/3` always returns a valid datetime.
Consequently, time zones that observe "Daylight Saving Time" On the other hand, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours or other changes, across summer/winter time will add/remove hours
from the resulting datetime: from the resulting datetime:
@@ -1793,22 +1700,12 @@ defmodule DateTime do
DateTime.shift(dt, hour: 2) DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles> #=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
Although the first example shows a difference of 2 hours when
comparing the wall clocks of the given datetime with the returned one,
due to the "spring forward" time jump, the actual elapsed time is
still exactly of 1 hour.
In case you don't want these changes to happen automatically or you In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`: the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone) dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
The above will surface time jumps and ambiguous datetimes, allowing you
to deal with them accordingly.
## ISO calendar considerations
When using the default ISO calendar, durations are collapsed and When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds: applied in the order of months, then seconds and microseconds:
@@ -1843,6 +1740,44 @@ defmodule DateTime do
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t @spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{ %{
year: year, year: year,
@@ -1936,7 +1871,7 @@ defmodule DateTime do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT", iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -1965,7 +1900,7 @@ defmodule DateTime do
if Calendar.compatible_calendars?(dt_calendar, calendar) do if Calendar.compatible_calendars?(dt_calendar, calendar) do
result_datetime = result_datetime =
datetime datetime
|> to_iso_days() |> to_iso_days
|> from_iso_days(datetime, calendar, precision) |> from_iso_days(datetime, calendar, precision)
{:ok, result_datetime} {:ok, result_datetime}
@@ -1983,7 +1918,7 @@ defmodule DateTime do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT", iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
@@ -2053,12 +1988,11 @@ defmodule DateTime do
end end
defp apply_tz_offset(iso_days, offset) do defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_time_unit_to_iso_days(iso_days, -offset, :second) Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
end end
defp from_map(%{} = datetime_map) do defp from_map(%{} = datetime_map) do
%DateTime{ %DateTime{
calendar: datetime_map.calendar,
year: datetime_map.year, year: datetime_map.year,
month: datetime_map.month, month: datetime_map.month,
day: datetime_map.day, day: datetime_map.day,
@@ -2145,8 +2079,7 @@ defmodule DateTime do
) )
case datetime do case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year} %{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
when calendar != Calendar.ISO or year in -9999..9999 ->
"~U[" <> formatted <> suffix(calendar) <> "]" "~U[" <> formatted <> suffix(calendar) <> "]"
_ -> _ ->
+53 -217
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Duration do defmodule Duration do
@moduledoc """ @moduledoc """
Struct and functions for handling durations. Struct and functions for handling durations.
@@ -72,7 +69,7 @@ defmodule Duration do
However, once again, it is important to remember that shifting a duration is not However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what arithmetic, so you may want to use the functions in this module depending on what
you want to achieve. Compare the results of both examples below: you to achieve. Compare the results of both examples below:
# Adding one month after the other # Adding one month after the other
iex> date = ~D[2016-01-31] iex> date = ~D[2016-01-31]
@@ -91,30 +88,6 @@ defmodule Duration do
The second example consistently points to the last day of the month, The second example consistently points to the last day of the month,
as it performs operations on the duration, rather than shifting date as it performs operations on the duration, rather than shifting date
after date. after date.
## Comparing durations
In order to accurately compare durations, you need to either compare
only certain fields or use a reference time instant. This is because
some fields are relative to others. For example, you may say that
1 month is the same as 30 days, but if you add both of these durations
to `~D[2015-02-01]`, you would get different results, as that month
has only 28 days.
Therefore, if you wish to compare durations, one option is to use
`Date.shift/2` (or `DateTime.shift/2` or similar), and then compare
the dates:
iex> date = ~D[2015-02-01]
iex> Date.compare(Date.shift(date, month: 1), Date.shift(date, day: 30))
:lt
Or alternatively convert the durations to a fixed unit by using `to_timeout/1`,
which supports durations only up to weeks, raising if it has the month or year
fields set.
iex> to_timeout(hour: 24) == to_timeout(day: 1)
true
""" """
@moduledoc since: "1.17.0" @moduledoc since: "1.17.0"
@@ -129,16 +102,6 @@ defmodule Duration do
second: 0, second: 0,
microsecond: {0, 0} microsecond: {0, 0}
@typedoc """
The microsecond component of a duration.
Unlike `t:Calendar.microsecond/0`, the value may be negative, as
durations may represent negative amounts of time. The precision is
an integer from 0 to 6 holding the number of significant digits,
as in the calendar types.
"""
@type microsecond :: {value :: integer, precision :: 0..6}
@typedoc """ @typedoc """
The duration struct type. The duration struct type.
""" """
@@ -150,7 +113,7 @@ defmodule Duration do
hour: integer, hour: integer,
minute: integer, minute: integer,
second: integer, second: integer,
microsecond: microsecond() microsecond: {integer, 0..6}
} }
@typedoc """ @typedoc """
@@ -164,29 +127,13 @@ defmodule Duration do
| {:hour, integer} | {:hour, integer}
| {:minute, integer} | {:minute, integer}
| {:second, integer} | {:second, integer}
| {:microsecond, microsecond()} | {:microsecond, {integer, 0..6}}
@typedoc """ @typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs. The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
""" """
@type duration :: t | [unit_pair] @type duration :: t | [unit_pair]
@typedoc """
Options for `Duration.to_string/2`.
"""
@type to_string_opts :: [
units: [
year: String.t(),
month: String.t(),
week: String.t(),
day: String.t(),
hour: String.t(),
minute: String.t(),
second: String.t()
],
separator: String.t()
]
@microseconds_per_second 1_000_000 @microseconds_per_second 1_000_000
@doc """ @doc """
@@ -242,7 +189,7 @@ defmodule Duration do
@doc """ @doc """
Adds units of given durations `d1` and `d2`. Adds units of given durations `d1` and `d2`.
Respects the highest microsecond precision of the two. Respects the the highest microsecond precision of the two.
## Examples ## Examples
@@ -253,26 +200,26 @@ defmodule Duration do
""" """
@spec add(t, t) :: t @spec add(t, t) :: t
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def add(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 + y2, year: d1.year + d2.year,
month: mo1 + mo2, month: d1.month + d2.month,
week: w1 + w2, week: d1.week + d2.week,
day: day1 + day2, day: d1.day + d2.day,
hour: h1 + h2, hour: d1.hour + d2.hour,
minute: mi1 + mi2, minute: d1.minute + d2.minute,
second: s1 + s2, second: d1.second + d2.second,
microsecond: {ms1 + ms2, max(p1, p2)} microsecond: {m1 + m2, max(p1, p2)}
} }
end end
@doc """ @doc """
Subtracts units of given durations `d1` and `d2`. Subtracts units of given durations `d1` and `d2`.
Respects the highest microsecond precision of the two. Respects the the highest microsecond precision of the two.
## Examples ## Examples
@@ -283,19 +230,19 @@ defmodule Duration do
""" """
@spec subtract(t, t) :: t @spec subtract(t, t) :: t
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do def subtract(%Duration{} = d1, %Duration{} = d2) do
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1 {m1, p1} = d1.microsecond
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2 {m2, p2} = d2.microsecond
%Duration{ %Duration{
year: y1 - y2, year: d1.year - d2.year,
month: mo1 - mo2, month: d1.month - d2.month,
week: w1 - w2, week: d1.week - d2.week,
day: day1 - day2, day: d1.day - d2.day,
hour: h1 - h2, hour: d1.hour - d2.hour,
minute: mi1 - mi2, minute: d1.minute - d2.minute,
second: s1 - s2, second: d1.second - d2.second,
microsecond: {ms1 - ms2, max(p1, p2)} microsecond: {m1 - m2, max(p1, p2)}
} }
end end
@@ -312,16 +259,14 @@ defmodule Duration do
""" """
@spec multiply(t, integer) :: t @spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: y * integer, year: duration.year * integer,
month: mo * integer, month: duration.month * integer,
week: w * integer, week: duration.week * integer,
day: d * integer, day: duration.day * integer,
hour: h * integer, hour: duration.hour * integer,
minute: mi * integer, minute: duration.minute * integer,
second: s * integer, second: duration.second * integer,
microsecond: {ms * integer, p} microsecond: {ms * integer, p}
} }
end end
@@ -339,16 +284,14 @@ defmodule Duration do
""" """
@spec negate(t) :: t @spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do def negate(%Duration{microsecond: {ms, p}} = duration) do
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
%Duration{ %Duration{
year: -y, year: -duration.year,
month: -mo, month: -duration.month,
week: -w, week: -duration.week,
day: -d, day: -duration.day,
hour: -h, hour: -duration.hour,
minute: -mi, minute: -duration.minute,
second: -s, second: -duration.second,
microsecond: {-ms, p} microsecond: {-ms, p}
} }
end end
@@ -412,119 +355,11 @@ defmodule Duration do
end end
end end
@doc """
Converts the given `duration` to a human readable representation.
## Options
* `:units` - the units to be used alongside each duration component.
The default units follow the ISO 80000-3 standard:
[
year: "a",
month: "mo",
week: "wk",
day: "d",
hour: "h",
minute: "min",
second: "s"
]
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
## Examples
iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"
By default, this function uses ISO 80000-3 units, which uses "a" for years.
But you can customize all units via the units option:
iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"
You may also choose the separator:
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"
A duration without components is rendered as "0s":
iex> Duration.to_string(Duration.new!([]))
"0s"
Microseconds are rendered as part of seconds with the appropriate precision:
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"
"""
@doc since: "1.18.0"
@spec to_string(t, to_string_opts) :: String.t()
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
case to_string_year(duration, [], units) do
[] ->
"0" <> Keyword.get(units, :second, "s")
[part] ->
IO.iodata_to_binary(part)
parts ->
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
end
end
defp to_string_part(0, _units, _key, _default, acc),
do: acc
defp to_string_part(x, units, key, default, acc),
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
defp to_string_year(%{year: year} = duration, acc, units) do
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
end
defp to_string_month(%{month: month} = duration, acc, units) do
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
end
defp to_string_week(%{week: week} = duration, acc, units) do
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
end
defp to_string_day(%{day: day} = duration, acc, units) do
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
end
defp to_string_hour(%{hour: hour} = duration, acc, units) do
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
end
defp to_string_minute(%{minute: minute} = duration, acc, units) do
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
end
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
acc
end
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
end
@doc """ @doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string. Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`. Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to
appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct.
## Examples ## Examples
@@ -572,15 +407,15 @@ defmodule Duration do
[] []
end end
defp second_component(%{second: 0, microsecond: {_, 0}}) do
~c"0S"
end
defp second_component(%{second: second, microsecond: {_, 0}}) do
[Integer.to_string(second), ?S]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do defp second_component(%{second: second, microsecond: {ms, p}}) do
[second_component(second, ms, p), ?S]
end
defp second_component(second, _ms, 0) do
Integer.to_string(second)
end
defp second_component(second, ms, p) do
total_ms = second * @microseconds_per_second + ms total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs() second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs() ms = total_ms |> rem(@microseconds_per_second) |> abs()
@@ -590,7 +425,8 @@ defmodule Duration do
sign, sign,
Integer.to_string(second), Integer.to_string(second),
?., ?.,
Calendar.ISO.microseconds_to_iodata(ms, p) ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p),
?S
] ]
end end
File diff suppressed because it is too large Load Diff
+89 -135
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule NaiveDateTime do defmodule NaiveDateTime do
@moduledoc """ @moduledoc """
A NaiveDateTime struct (without a time zone) and functions. A NaiveDateTime struct (without a time zone) and functions.
@@ -40,10 +36,10 @@ defmodule NaiveDateTime do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `NaiveDateTime` struct fields. For proper comparison and based on the `NaiveDateTime` struct fields. For proper comparison
between naive datetimes, use the `compare/2`, `after?/2` and `before?/2` functions. between naive datetimes, use the `compare/2` function. The existence of the
The existence of the `compare/2` function in this module also allows `compare/2` function in this module also allows using `Enum.min/2` and
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and `Enum.max/2` functions to get the minimum and maximum naive datetime of an
maximum naive datetime of an `Enum`. For example: `Enum`. For example:
iex> Enum.min([~N[2020-01-01 23:00:07], ~N[2000-01-01 23:00:07]], NaiveDateTime) iex> Enum.min([~N[2020-01-01 23:00:07], ~N[2000-01-01 23:00:07]], NaiveDateTime)
~N[2000-01-01 23:00:07] ~N[2000-01-01 23:00:07]
@@ -114,12 +110,30 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t @spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO) def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO) utc_now(time_unit, Calendar.ISO)
end end
def utc_now(calendar) do def utc_now(calendar) do
utc_now(:native, calendar) calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
end end
@doc """ @doc """
@@ -144,27 +158,14 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t @spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar) def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} = DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
end end
@doc """ @doc """
Returns the "local time" for the machine the Elixir program is running on. Returns the "local time" for the machine the Elixir program is running on.
WARNING: This function can cause insidious bugs. It depends on the time zone WARNING: This function can cause insidious bugs. It depends on the time zone
configuration at run time. This can change and be set to a time zone that has configuration at run time. This can changed and be set to a time zone that has
daylight saving jumps (spring forward or fall back). daylight saving jumps (spring forward or fall back).
This function can be used to display what the time is right now for the time This function can be used to display what the time is right now for the time
@@ -251,7 +252,7 @@ defmodule NaiveDateTime do
Calendar.hour(), Calendar.hour(),
Calendar.minute(), Calendar.minute(),
Calendar.second(), Calendar.second(),
Calendar.microsecond() | non_neg_integer(), Calendar.microsecond() | non_neg_integer,
Calendar.calendar() Calendar.calendar()
) :: {:ok, t} | {:error, atom} ) :: {:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -316,7 +317,7 @@ defmodule NaiveDateTime do
Calendar.hour(), Calendar.hour(),
Calendar.minute(), Calendar.minute(),
Calendar.second(), Calendar.second(),
Calendar.microsecond() | non_neg_integer(), Calendar.microsecond() | non_neg_integer,
Calendar.calendar() Calendar.calendar()
) :: t ) :: t
def new!( def new!(
@@ -391,20 +392,13 @@ defmodule NaiveDateTime do
@doc """ @doc """
Adds a specified amount of time to a `NaiveDateTime`. Adds a specified amount of time to a `NaiveDateTime`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` provides a lower-level API which only supports fixed units
> such as `:hour` and `:second`, but not `:month` (as the exact length
> of a month depends on the current month). `add/3` always considers
> the unit to be computed according to the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`, Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
## Examples ## Examples
@@ -454,6 +448,8 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second) iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28] ~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t @spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -541,15 +537,15 @@ defmodule NaiveDateTime do
def diff(naive_datetime1, naive_datetime2, unit \\ :second) def diff(naive_datetime1, naive_datetime2, unit \\ :second)
def diff(naive_datetime1, naive_datetime2, :day) do def diff(naive_datetime1, naive_datetime2, :day) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(86400)
end end
def diff(naive_datetime1, naive_datetime2, :hour) do def diff(naive_datetime1, naive_datetime2, :hour) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(3600)
end end
def diff(naive_datetime1, naive_datetime2, :minute) do def diff(naive_datetime1, naive_datetime2, :minute) do
diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000) diff(naive_datetime1, naive_datetime2, :second) |> div(60)
end end
def diff( def diff(
@@ -570,11 +566,9 @@ defmodule NaiveDateTime do
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" "unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end end
diff_microsecond = units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) - units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
(naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) units1 - units2
System.convert_time_unit(diff_microsecond, :microsecond, unit)
end end
@doc """ @doc """
@@ -674,7 +668,7 @@ defmodule NaiveDateTime do
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec truncate(Calendar.naive_datetime(), :microsecond | :millisecond | :second) :: t() @spec truncate(t(), :microsecond | :millisecond | :second) :: t()
def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do def truncate(%NaiveDateTime{microsecond: microsecond} = naive_datetime, precision) do
%{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)} %{naive_datetime | microsecond: Calendar.truncate(microsecond, precision)}
end end
@@ -717,18 +711,16 @@ defmodule NaiveDateTime do
""" """
@spec to_date(Calendar.naive_datetime()) :: Date.t() @spec to_date(Calendar.naive_datetime()) :: Date.t()
def to_date( def to_date(%{
%{ year: year,
year: year, month: month,
month: month, day: day,
day: day, calendar: calendar,
calendar: calendar, hour: _,
hour: _, minute: _,
minute: _, second: _,
second: _, microsecond: _
microsecond: _ }) do
} = _naive_datetime
) do
%Date{year: year, month: month, day: day, calendar: calendar} %Date{year: year, month: month, day: day, calendar: calendar}
end end
@@ -745,18 +737,16 @@ defmodule NaiveDateTime do
""" """
@spec to_time(Calendar.naive_datetime()) :: Time.t() @spec to_time(Calendar.naive_datetime()) :: Time.t()
def to_time( def to_time(%{
%{ year: _,
year: _, month: _,
month: _, day: _,
day: _, calendar: calendar,
calendar: calendar, hour: hour,
hour: hour, minute: minute,
minute: minute, second: second,
second: second, microsecond: microsecond
microsecond: microsecond }) do
} = _naive_datetime
) do
%Time{ %Time{
hour: hour, hour: hour,
minute: minute, minute: minute,
@@ -769,10 +759,7 @@ defmodule NaiveDateTime do
@doc """ @doc """
Converts the given naive datetime to a string according to its calendar. Converts the given naive datetime to a string according to its calendar.
For readability, this function follows the RFC3339 suggestion of removing ### Examples
the "T" separator between the date and time components.
## Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13]) iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13" "2000-02-28 23:00:13"
@@ -919,7 +906,7 @@ defmodule NaiveDateTime do
Only supports converting naive datetimes which are in the ISO calendar, Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise. attempting to convert naive datetimes from other calendars will raise.
## Examples ### Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13]) iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13" "2000-02-28T23:00:13"
@@ -945,19 +932,6 @@ defmodule NaiveDateTime do
def to_iso8601(%{calendar: Calendar.ISO} = naive_datetime, format) def to_iso8601(%{calendar: Calendar.ISO} = naive_datetime, format)
when format in [:basic, :extended] do when format in [:basic, :extended] do
naive_datetime
|> to_iso8601_iodata(format)
|> IO.iodata_to_binary()
end
def to_iso8601(%{calendar: _} = naive_datetime, format)
when format in [:basic, :extended] do
naive_datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
defp to_iso8601_iodata(naive_datetime, format) do
%{ %{
year: year, year: year,
month: month, month: month,
@@ -968,11 +942,14 @@ defmodule NaiveDateTime do
microsecond: microsecond microsecond: microsecond
} = naive_datetime } = naive_datetime
[ Calendar.ISO.date_to_string(year, month, day, format) <>
Calendar.ISO.date_to_iodata(year, month, day, format), "T" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond, format)
?T, end
Calendar.ISO.time_to_iodata(hour, minute, second, microsecond, format)
] def to_iso8601(%{calendar: _} = naive_datetime, format) when format in [:basic, :extended] do
naive_datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end end
@doc """ @doc """
@@ -1016,8 +993,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}) iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]} {:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
{:ok, ~N[2000-01-01 13:30:15.005000]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3}) iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]} {:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}}) iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
@@ -1026,11 +1001,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date} {:error, :invalid_date}
""" """
@spec from_erl( @spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
{:ok, t} | {:error, atom} {:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1049,19 +1020,13 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}) iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15] ~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
~N[2000-01-01 13:30:15.005000]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3}) iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005] ~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}}) iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date ** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
""" """
@spec from_erl!( @spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do case from_erl(tuple, microsecond, calendar) do
{:ok, value} -> {:ok, value} ->
@@ -1152,18 +1117,16 @@ defmodule NaiveDateTime do
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()} @spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
def to_gregorian_seconds( def to_gregorian_seconds(%{
%{ calendar: calendar,
calendar: calendar, year: year,
year: year, month: month,
month: month, day: day,
day: day, hour: hour,
hour: hour, minute: minute,
minute: minute, second: second,
second: second, microsecond: {microsecond, precision}
microsecond: {microsecond, precision} }) do
} = _naive_datetime
) do
{days, day_fraction} = {days, day_fraction} =
calendar.naive_datetime_to_iso_days( calendar.naive_datetime_to_iso_days(
year, year,
@@ -1274,7 +1237,7 @@ defmodule NaiveDateTime do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene) iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1287,10 +1250,6 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars} {:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors. # Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert( def convert(
%{ %{
calendar: calendar, calendar: calendar,
@@ -1322,7 +1281,7 @@ defmodule NaiveDateTime do
if Calendar.compatible_calendars?(ndt_calendar, calendar) do if Calendar.compatible_calendars?(ndt_calendar, calendar) do
result_naive_datetime = result_naive_datetime =
naive_datetime naive_datetime
|> to_iso_days() |> to_iso_days
|> from_iso_days(calendar, precision) |> from_iso_days(calendar, precision)
{:ok, result_naive_datetime} {:ok, result_naive_datetime}
@@ -1340,7 +1299,7 @@ defmodule NaiveDateTime do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene) iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
@@ -1492,15 +1451,10 @@ defmodule NaiveDateTime do
calendar: calendar calendar: calendar
} = naive_datetime } = naive_datetime
if calendar != Calendar.ISO or year in -9999..9999 do formatted =
formatted = calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]" "~N[" <> formatted <> suffix(calendar) <> "]"
else
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
end
end end
defp suffix(Calendar.ISO), do: "" defp suffix(Calendar.ISO), do: ""
+46 -115
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Time do defmodule Time do
@moduledoc """ @moduledoc """
A Time struct and functions. A Time struct and functions.
@@ -35,10 +31,9 @@ defmodule Time do
Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural Comparisons in Elixir using `==/2`, `>/2`, `</2` and similar are structural
and based on the `Time` struct fields. For proper comparison between and based on the `Time` struct fields. For proper comparison between
times, use the `compare/2`, `after?/2` and `before?/2` functions. times, use the `compare/2` function. The existence of the `compare/2`
The existence of the `compare/2` function in this module also allows function in this module also allows using `Enum.min/2` and `Enum.max/2`
using `Enum.min/2` and `Enum.max/2` functions to get the minimum and functions to get the minimum and maximum time of an `Enum`. For example:
maximum time of an `Enum`. For example:
iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time) iex> Enum.min([~T[23:00:07.001], ~T[10:00:07.001]], Time)
~T[10:00:07.001] ~T[10:00:07.001]
@@ -60,52 +55,17 @@ defmodule Time do
@doc """ @doc """
Returns the current time in UTC. Returns the current time in UTC.
You can pass a time unit to automatically truncate the resulting time.
The default unit if none gets passed is `:native` which results in a default resolution of microseconds.
## Examples ## Examples
iex> time = Time.utc_now() iex> time = Time.utc_now()
iex> time.hour >= 0 iex> time.hour >= 0
true true
iex> time = Time.utc_now(:second)
iex> time.microsecond
{0, 0}
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t @spec utc_now(Calendar.calendar()) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO) do def utc_now(calendar \\ Calendar.ISO) do
case calendar_or_time_unit do {:ok, _, time, microsecond} = Calendar.ISO.from_unix(:os.system_time(), :native)
unit when unit in [:native, :microsecond, :millisecond, :second] ->
utc_now(unit, Calendar.ISO)
calendar ->
utc_now(:native, calendar)
end
end
@doc """
Returns the current time in UTC, supporting a precision and a specific calendar.
## Examples
iex> time = Time.utc_now(:microsecond, Calendar.ISO)
iex> time.hour >= 0
true
iex> time = Time.utc_now(:second, Calendar.ISO)
iex> time.microsecond
{0, 0}
"""
@doc since: "1.19.0"
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, _, time, microsecond} = Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
{hour, minute, second} = time {hour, minute, second} = time
iso_time = %Time{ iso_time = %Time{
@@ -146,9 +106,8 @@ defmodule Time do
iex> Time.new(23, 59, 59, 1_000_000) iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time} {:error, :invalid_time}
Invalid precision: # Invalid precision
Time.new(23, 59, 59, {999_999, 10})
iex> Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time} {:error, :invalid_time}
""" """
@@ -156,7 +115,7 @@ defmodule Time do
Calendar.hour(), Calendar.hour(),
Calendar.minute(), Calendar.minute(),
Calendar.second(), Calendar.second(),
Calendar.microsecond() | non_neg_integer(), Calendar.microsecond() | non_neg_integer,
Calendar.calendar() Calendar.calendar()
) :: {:ok, t} | {:error, atom} ) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -226,7 +185,7 @@ defmodule Time do
@doc """ @doc """
Converts the given `time` to a string. Converts the given `time` to a string.
## Examples ### Examples
iex> Time.to_string(~T[23:00:00]) iex> Time.to_string(~T[23:00:00])
"23:00:00" "23:00:00"
@@ -335,7 +294,7 @@ defmodule Time do
format, for human readability. It also supports the "basic" format through format, for human readability. It also supports the "basic" format through
passing the `:basic` option. passing the `:basic` option.
## Examples ### Examples
iex> Time.to_iso8601(~T[23:00:13]) iex> Time.to_iso8601(~T[23:00:13])
"23:00:13" "23:00:13"
@@ -396,21 +355,13 @@ defmodule Time do
## Examples ## Examples
iex> Time.from_erl({23, 30, 15})
{:ok, ~T[23:30:15]}
iex> Time.from_erl({23, 30, 15}, 5000)
{:ok, ~T[23:30:15.005000]}
iex> Time.from_erl({23, 30, 15}, {5000, 3}) iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]} {:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15}) iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time} {:error, :invalid_time}
""" """
@spec from_erl( @spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
{:ok, t} | {:error, atom} {:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -426,8 +377,6 @@ defmodule Time do
iex> Time.from_erl!({23, 30, 15}) iex> Time.from_erl!({23, 30, 15})
~T[23:30:15] ~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, 5000)
~T[23:30:15.005000]
iex> Time.from_erl!({23, 30, 15}, {5000, 3}) iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005] ~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15}) iex> Time.from_erl!({24, 30, 15})
@@ -467,12 +416,8 @@ defmodule Time do
Calendar.microsecond(), Calendar.microsecond(),
Calendar.calendar() Calendar.calendar()
) :: t ) :: t
def from_seconds_after_midnight( def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
seconds, when is_integer(seconds) do
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) and microsecond in 0..999_999 and precision in 0..6 do
seconds_in_day = Integer.mod(seconds, @seconds_per_day) seconds_in_day = Integer.mod(seconds, @seconds_per_day)
{hour, minute, second, {_, _}} = {hour, minute, second, {_, _}} =
@@ -483,7 +428,7 @@ defmodule Time do
hour: hour, hour: hour,
minute: minute, minute: minute,
second: second, second: second,
microsecond: {microsecond, precision} microsecond: microsecond
} }
end end
@@ -501,7 +446,7 @@ defmodule Time do
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec to_seconds_after_midnight(Calendar.time()) :: {non_neg_integer(), non_neg_integer()} @spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
iso_days = {0, to_day_fraction(time)} iso_days = {0, to_day_fraction(time)}
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond} {Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
@@ -510,18 +455,13 @@ defmodule Time do
@doc """ @doc """
Adds the `amount_to_add` of `unit`s to the given `time`. Adds the `amount_to_add` of `unit`s to the given `time`.
> #### Prefer `shift/2` {: .info}
>
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
>
> `add/3` always considers the unit to be computed according to
> the `Calendar.ISO`.
Accepts an `amount_to_add` in any `unit`. `unit` can be Accepts an `amount_to_add` in any `unit`. `unit` can be
`:hour`, `:minute`, `:second` or any subsecond precision from `:hour`, `:minute`, `:second` or any subsecond precision from
`t:System.time_unit/0` for convenience but ultimately they are `t:System.time_unit/0`. It defaults to `:second`. Negative values
all converted to microseconds. Negative values will move backwards will move backwards in time.
in time and the default precision is `:second`.
This function always consider the unit to be computed according
to the `Calendar.ISO`.
Note the result value represents the time of day, meaning that it is cyclic, Note the result value represents the time of day, meaning that it is cyclic,
for instance, it will never go over 24 hours for the ISO calendar. for instance, it will never go over 24 hours for the ISO calendar.
@@ -559,6 +499,8 @@ defmodule Time do
iex> result.microsecond iex> result.microsecond
{21000, 3} {21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t @spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -579,7 +521,7 @@ defmodule Time do
do: unit > 0, do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a else: unit in ~w(second millisecond microsecond nanosecond)a
if not valid? do unless valid? do
raise ArgumentError, raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" "unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end end
@@ -694,7 +636,7 @@ defmodule Time do
@doc """ @doc """
Compares two time structs. Compares two time structs.
Returns `:gt` if the first time is later than the second Returns `:gt` if first time is later than the second
and `:lt` for vice versa. If the two times are equal and `:lt` for vice versa. If the two times are equal
`:eq` is returned. `:eq` is returned.
@@ -720,32 +662,14 @@ defmodule Time do
""" """
@doc since: "1.4.0" @doc since: "1.4.0"
@spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt @spec compare(Calendar.time(), Calendar.time()) :: :lt | :eq | :gt
def compare( def compare(%{calendar: calendar} = time1, %{calendar: calendar} = time2) do
%{ %{hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}} = time1
hour: hour1, %{hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}} = time2
minute: minute1,
second: second1, case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
microsecond: {microsecond1, _}, {first, second} when first > second -> :gt
calendar: calendar {first, second} when first < second -> :lt
}, _ -> :eq
%{
hour: hour2,
minute: minute2,
second: second2,
microsecond: {microsecond2, _},
calendar: calendar
}
) do
cond do
hour1 > hour2 -> :gt
hour1 < hour2 -> :lt
minute1 > minute2 -> :gt
minute1 < minute2 -> :lt
second1 > second2 -> :gt
second1 < second2 -> :lt
microsecond1 > microsecond2 -> :gt
microsecond1 < microsecond2 -> :lt
true -> :eq
end end
end end
@@ -807,7 +731,7 @@ defmodule Time do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene) iex> Time.convert(~T[13:30:15], Calendar.Holocene)
@@ -863,7 +787,7 @@ defmodule Time do
## Examples ## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10 000 years to the current Gregorian Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year: year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene) iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
@@ -873,8 +797,15 @@ defmodule Time do
@doc since: "1.5.0" @doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t @spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do def convert!(time, calendar) do
{:ok, value} = convert(time, calendar) case convert(time, calendar) do
value {:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end end
@doc """ @doc """
@@ -924,11 +855,11 @@ defmodule Time do
def diff(time1, time2, unit \\ :second) def diff(time1, time2, unit \\ :second)
def diff(time1, time2, :hour) do def diff(time1, time2, :hour) do
diff(time1, time2, :microsecond) |> div(3_600_000_000) diff(time1, time2, :second) |> div(3600)
end end
def diff(time1, time2, :minute) do def diff(time1, time2, :minute) do
diff(time1, time2, :microsecond) |> div(60_000_000) diff(time1, time2, :second) |> div(60)
end end
def diff( def diff(
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar.TimeZoneDatabase do defmodule Calendar.TimeZoneDatabase do
@moduledoc """ @moduledoc """
This module defines a behaviour for providing time zone data. This module defines a behaviour for providing time zone data.
+141 -393
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code do defmodule Code do
@moduledoc ~S""" @moduledoc ~S"""
Utilities for managing code compilation, code evaluation, and code loading. Utilities for managing code compilation, code evaluation, and code loading.
@@ -50,7 +46,7 @@ defmodule Code do
You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and You can use `ensure_loaded/1` (as well as `ensure_loaded?/1` and
`ensure_loaded!/1`) to check if a module is loaded before using it and `ensure_loaded!/1`) to check if a module is loaded before using it and
act accordingly. act.
## `ensure_compiled/1` and `ensure_compiled!/1` ## `ensure_compiled/1` and `ensure_compiled!/1`
@@ -127,11 +123,6 @@ defmodule Code do
of the imported function/macro. A :remote_function/:remote_macro event of the imported function/macro. A :remote_function/:remote_macro event
may still be emitted for the imported module/name/arity. may still be emitted for the imported module/name/arity.
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
imported function or macro is processed inside a `quote/2`. `meta` is the
call AST metadata, `module` is the module the import is from, followed by
the `name` and a list of `arities` of the imported function/macro.
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased * `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
to `as`. `meta` is the alias AST metadata and `opts` are the alias options. to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
@@ -189,7 +180,7 @@ defmodule Code do
defmodule MyTracer do defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}") IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok :ok
end end
@@ -248,84 +239,19 @@ defmodule Code do
""" """
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()} @type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for code formatting functions.
"""
@type format_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:line_length, pos_integer()}
| {:locals_without_parens, keyword()}
| {:force_do_end_blocks, boolean()}
| {:migrate, boolean()}
| {:migrate_atom_interpolations, boolean()}
| {:migrate_bitstring_modifiers, boolean()}
| {:migrate_call_parens_on_pipe, boolean()}
| {:migrate_charlists_as_sigils, boolean()}
| {:migrate_unless, boolean()}
| {atom(), term()}
@typedoc """
Options for `quoted_to_algebra/2`.
"""
@type quoted_to_algebra_opt ::
{:line, pos_integer() | nil}
| {:escape, boolean()}
| {:locals_without_parens, keyword()}
| {:comments, [term()]}
| {:syntax_colors, [{Inspect.Opts.color_key(), IO.ANSI.ansidata()}]}
@typedoc """
Options for parsing functions that convert strings to quoted expressions.
"""
@type parser_opts :: [
file: binary(),
line: pos_integer(),
column: pos_integer(),
indentation: non_neg_integer(),
columns: boolean(),
unescape: boolean(),
existing_atoms_only: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
static_atoms_encoder: (binary(), Macro.metadata() -> {:ok, term()} | {:error, binary()}),
emit_warnings: boolean()
]
@typedoc """
Options for evaluation environment, accepted by `env_for_eval/1`.
"""
@type env_eval_opt ::
{:file, binary()}
| {:line, pos_integer()}
| {:module, module()}
@typedoc """
Options for evaluation functions like `eval_string/3`, `eval_quoted/3`
and `eval_quoted_with_env/4`.
"""
@type eval_opt ::
{:prune_binding, boolean()}
| {:dbg_callback, {module(), atom(), list()}}
@boolean_compiler_options [ @boolean_compiler_options [
:docs, :docs,
:debug_info, :debug_info,
:ignore_already_consolidated, :ignore_already_consolidated,
:ignore_module_conflict, :ignore_module_conflict,
:relative_paths :relative_paths,
:warnings_as_errors
] ]
@list_compiler_options [:tracers, :parser_options, :erlc_options] @list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@available_compiler_options @boolean_compiler_options ++ @available_compiler_options @boolean_compiler_options ++
@list_compiler_options ++ @list_compiler_options ++ [:on_undefined_variable]
[
:on_undefined_variable,
:infer_signatures,
:no_warn_undefined,
:module_definition
]
@doc """ @doc """
Lists all required files. Lists all required files.
@@ -412,10 +338,10 @@ defmodule Code do
* `:cache` - (since v1.15.0) when true, the code path is cached * `:cache` - (since v1.15.0) when true, the code path is cached
the first time it is traversed in order to reduce file system the first time it is traversed in order to reduce file system
operations. operations. It requires Erlang/OTP 26, otherwise it is a no-op.
""" """
@spec append_path(Path.t(), cache: boolean()) :: boolean() @spec append_path(Path.t(), cache: boolean()) :: true | false
def append_path(path, opts \\ []) do def append_path(path, opts \\ []) do
apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true apply(:code, :add_pathz, [to_charlist(Path.expand(path)) | cache(opts)]) == true
end end
@@ -443,7 +369,7 @@ defmodule Code do
* `:cache` - (since v1.15.0) when true, the code path is cached * `:cache` - (since v1.15.0) when true, the code path is cached
the first time it is traversed in order to reduce file system the first time it is traversed in order to reduce file system
operations. operations. It requires Erlang/OTP 26, otherwise it is a no-op.
""" """
@spec prepend_path(Path.t(), cache: boolean()) :: boolean() @spec prepend_path(Path.t(), cache: boolean()) :: boolean()
@@ -472,7 +398,7 @@ defmodule Code do
* `:cache` - when true, the code path is cached the first time * `:cache` - when true, the code path is cached the first time
it is traversed in order to reduce file system operations. it is traversed in order to reduce file system operations.
It requires Erlang/OTP 26, otherwise it is a no-op.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec prepend_paths([Path.t()], cache: boolean()) :: :ok @spec prepend_paths([Path.t()], cache: boolean()) :: :ok
@@ -501,7 +427,7 @@ defmodule Code do
* `:cache` - when true, the code path is cached the first time * `:cache` - when true, the code path is cached the first time
it is traversed in order to reduce file system operations. it is traversed in order to reduce file system operations.
It requires Erlang/OTP 26, otherwise it is a no-op.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec append_paths([Path.t()], cache: boolean()) :: :ok @spec append_paths([Path.t()], cache: boolean()) :: :ok
@@ -554,7 +480,8 @@ defmodule Code do
This is the list of directories the Erlang VM uses for finding This is the list of directories the Erlang VM uses for finding
module code. The list of files is managed per Erlang VM node. module code. The list of files is managed per Erlang VM node.
All paths are expanded with `Path.expand/1` before being deleted. The path is expanded with `Path.expand/1` before being deleted. If the
path does not exist, this function returns `false`.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec delete_paths([Path.t()]) :: :ok @spec delete_paths([Path.t()]) :: :ok
@@ -580,11 +507,14 @@ defmodule Code do
## Options ## Options
It accepts the same options as both `env_for_eval/1` and Options can be:
`eval_quoted_with_env/4`. Additionally, you may also pass an environment
as third argument, so the evaluation happens within that environment.
## Return * `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
Additionally, you may also pass an environment as second argument,
so the evaluation happens within that environment.
Returns a tuple of the form `{value, binding}`, where `value` is the value Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating returned from evaluating `string`. If an error occurs while evaluating
@@ -614,40 +544,32 @@ defmodule Code do
iex> Enum.sort(binding) iex> Enum.sort(binding)
[a: 3, b: 2] [a: 3, b: 2]
For convenience, you can pass `__ENV__/0` as the `opts_or_env` argument and For convenience, you can pass `__ENV__/0` as the `opts` argument and
all imports, requires and aliases defined in the current environment all imports, requires and aliases defined in the current environment
will be automatically carried over: will be automatically carried over:
iex> require Integer, warn: false iex> {result, binding} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
iex> result iex> result
3 3
iex> Enum.sort(binding) iex> Enum.sort(binding)
[a: 1, b: 2] [a: 1, b: 2]
""" """
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) :: @spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
{term, binding} def eval_string(string, binding \\ [], opts \\ [])
def eval_string(string, binding \\ [], opts_or_env \\ [])
def eval_string(string, binding, %Macro.Env{} = env) do def eval_string(string, binding, %Macro.Env{} = env) do
validated_eval_string(string, validate_binding(binding), env_for_eval(env), []) validated_eval_string(string, binding, env)
end end
def eval_string(string, binding, opts) when is_list(opts) do def eval_string(string, binding, opts) when is_list(opts) do
validated_eval_string(string, validate_binding(binding), env_for_eval(opts), opts) validated_eval_string(string, binding, opts)
end end
defp validate_binding(binding) when is_list(binding), do: binding defp validated_eval_string(string, binding, opts_or_env) do
%{line: line, file: file} = env = env_for_eval(opts_or_env)
defp validate_binding(binding) do
raise ArgumentError, "binding must be a list, got: #{inspect(binding)}"
end
defp validated_eval_string(string, binding, env, opts) do
%{line: line, file: file} = env
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, []) forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
{value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env, opts]) {value, binding, _env} = eval_verify(:eval_forms, [forms, binding, env])
{value, binding} {value, binding}
end end
@@ -688,8 +610,7 @@ defmodule Code do
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec with_diagnostics([log: boolean()], (-> result)) :: @spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
{result, [diagnostic(:warning | :error)]}
when result: term() when result: term()
def with_diagnostics(opts \\ [], fun) do def with_diagnostics(opts \\ [], fun) do
value = :erlang.get(:elixir_code_diagnostics) value = :erlang.get(:elixir_code_diagnostics)
@@ -722,7 +643,7 @@ defmodule Code do
Defaults to `true`. Defaults to `true`.
""" """
@doc since: "1.15.0" @doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok @spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true) read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?) :elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@@ -738,15 +659,13 @@ defmodule Code do
## Options ## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error * `:file` - the file which contains the string, used for error
reporting reporting
* `:line` - the line the string starts, used for error reporting * `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting * `:line_length` - the line length to aim for when formatting
the document. Defaults to `98`. This value indicates when an expression the document. Defaults to 98. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information to do so. See the "Line length" section below for more information
@@ -763,48 +682,28 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords. converted back to keywords.
Migration options (change the AST), see the "Migration formatting" section below: * `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_atom_interpolations` (since v1.21.0) - when `true`, rewrites
deprecated atom interpolations to explicit calls to `String.to_unsafe_atom/1`.
For example, `:"foo_#{bar}"` becomes `String.to_unsafe_atom("foo_#{bar}")`.
Interpolated keywords like `["foo_#{bar}": 1]` are **not** migrated.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
removes unnecessary parentheses in known bitstring removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>` [modifiers](`<<>>/1`), for example `<<foo::binary()>>`
becomes `<<foo::binary>>`, or adds parentheses for custom becomes `<<foo::binary>>`, or adds parentheses for custom
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`. modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
Defaults to the value of the `:migrate` option. This option changes the AST. Defaults to `true`. This option changes the AST.
* `:migrate_call_parens_on_pipe` (since v1.19.0) - when `true`, * `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
formats calls on the right-hand side of the pipe operator to always include
parentheses, for example `foo |> bar` becomes `foo |> bar()` and
`foo |> mod.fun` becomes `foo |> mod.fun()`.
Parentheses are always added for qualified calls like `foo |> Bar.bar` even
when this option is `false`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`. `'foo'` becomes `~c"foo"`.
Defaults to the value of the `:migrate` option. This option changes the AST. Defaults to `true`. This option changes the AST.
* `:migrate_unless` (since v1.18.0) - when `true`,
rewrites `unless` expressions using `if` with a negated condition, for example
`unless foo, do:` becomes `if !foo, do:`.
Defaults to the value of the `:migrate` option. This option changes the AST.
## Design principles ## Design principles
The formatter was designed under three principles. The formatter was designed under three principles.
First, the formatter never changes the semantics of the code by default. First, the formatter never changes the semantics of the code.
This means the input AST and the output AST are almost always equivalent. This means the input AST and the output AST are almost always equivalent.
The only cases where the formatter will change the AST is when the input AST
would cause *compiler warnings* and the output AST won't. The cases where
the formatter changes the AST can be disabled through formatting options
if desired.
The second principle is to provide as little configuration as possible. The second principle is to provide as little configuration as possible.
This eases the formatter adoption by removing contention points while This eases the formatter adoption by removing contention points while
@@ -1038,8 +937,9 @@ defmodule Code do
## Code comments ## Code comments
The formatter handles code comments and guarantees a space is always added The formatter also handles code comments in a way to guarantee a space
between the beginning of the comment (#) and the next character. is always added between the beginning of the comment (#) and the next
character.
The formatter also extracts all trailing comments to their previous line. The formatter also extracts all trailing comments to their previous line.
For example, the code below For example, the code below
@@ -1051,25 +951,9 @@ defmodule Code do
# world # world
hello hello
While the formatter attempts to preserve comments in most situations, Because code comments are handled apart from the code representation (AST),
that's not always possible, because code comments are handled apart from there are some situations where code comments are seen as ambiguous by the
the code representation (AST). While the formatter can preserve code code formatter. For example, the comment in the anonymous function below
comments between expressions and function arguments, the formatter
cannot currently preserve them around operators. For example, the following
code:
foo() ||
# also check for bar
bar()
will move the code comments to before the operator usage:
# also check for bar
foo() ||
bar()
In some situations, code comments can be seen as ambiguous by the formatter.
For example, the comment in the anonymous function below
fn fn
arg1 -> arg1 ->
@@ -1098,26 +982,11 @@ defmodule Code do
## Newlines ## Newlines
The formatter converts all newlines in code from `\r\n` to `\n`. The formatter converts all newlines in code from `\r\n` to `\n`.
## Migration formatting
As part of the Elixir release cycle, deprecations are being introduced,
emitting warnings which might require existing code to be changed.
In order to reduce the burden on developers when upgrading Elixir to the
next version, the formatter exposes some options, disabled by default,
in order to automate this process.
These options should address most of the typical use cases, but given they
introduce changes to the AST, there is a non-zero risk for meta-programming
heavy projects that relied on a specific AST, or projects that are
re-defining functions from the `Kernel`. In such cases, migrations cannot
be applied blindly and some extra changes might be needed in order to
address the deprecation warnings.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec format_string!(binary, [format_opt]) :: iodata @spec format_string!(binary, keyword) :: iodata
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
{line_length, opts} = Keyword.pop(opts, :line_length, 98) line_length = Keyword.get(opts, :line_length, 98)
to_quoted_opts = to_quoted_opts =
[ [
@@ -1140,7 +1009,7 @@ defmodule Code do
available options. available options.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec format_file!(binary, [format_opt]) :: iodata @spec format_file!(binary, keyword) :: iodata
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
string = File.read!(file) string = File.read!(file)
formatted = format_string!(string, [file: file, line: 1] ++ opts) formatted = format_string!(string, [file: file, line: 1] ++ opts)
@@ -1155,9 +1024,7 @@ defmodule Code do
Macro arguments are typically transformed by unquoting them into the Macro arguments are typically transformed by unquoting them into the
returned quoted expressions (instead of evaluated). returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of arguments and return types. See `eval_string/3` for a description of `binding` and `opts`.
It accepts the same options as both `env_for_eval/1` and
`eval_quoted_with_env/4`.
## Examples ## Examples
@@ -1179,20 +1046,11 @@ defmodule Code do
[a: 1, b: 2] [a: 1, b: 2]
""" """
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | [eval_opt | env_eval_opt]) :: @spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
{term, binding} def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) {value, binding, _env} =
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
def eval_quoted(quoted, binding, %Macro.Env{} = env) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(env), [])
end
def eval_quoted(quoted, binding, opts) when is_list(opts) do
eval_quoted(quoted, validate_binding(binding), env_for_eval(opts), opts)
end
defp eval_quoted(quoted, binding, env, opts) do
{value, binding, _env} = eval_verify(:eval_quoted, [quoted, binding, env, opts])
{value, binding} {value, binding}
end end
@@ -1217,12 +1075,8 @@ defmodule Code do
* `:file` - the file to be considered in the evaluation * `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts * `:line` - the line on which the script starts
* `:module` - the module to run the environment on
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec env_for_eval(Macro.Env.t() | [env_eval_opt]) :: Macro.Env.t()
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts) def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@doc """ @doc """
@@ -1242,13 +1096,9 @@ defmodule Code do
by the modules. You can submit to the `:on_module` tracer event by the modules. You can submit to the `:on_module` tracer event
and access the variables used by the module from its environment. and access the variables used by the module from its environment.
* `:dbg_callback` - (since v1.20.0) overrides the behaviour of `dbg/2`
used in the evaluated code. It must be a `{module, function, args}`
tuple, see `dbg/2` for more details.
""" """
@doc since: "1.14.0" @doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), [eval_opt]) :: @spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
{term, binding, Macro.Env.t()} {term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ []) def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do when is_list(binding) do
@@ -1267,14 +1117,10 @@ defmodule Code do
Defaults to `"nofile"`. Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed. * `:line` - the starting line of the string being parsed.
Defaults to `1`. Defaults to 1.
* `:column` - (since v1.11.0) the starting column of the string being parsed. * `:column` - (since v1.11.0) the starting column of the string being parsed.
Defaults to `1`. Defaults to 1.
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
This is useful when the code parsed is embedded within another document.
Defaults to `0`.
* `:columns` - when `true`, attach a `:column` key to the quoted * `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`. metadata. Defaults to `false`.
@@ -1298,9 +1144,9 @@ defmodule Code do
* `:literal_encoder` (since v1.10.0) - how to encode literals in the AST. * `:literal_encoder` (since v1.10.0) - how to encode literals in the AST.
It must be a function that receives two arguments, the literal and its It must be a function that receives two arguments, the literal and its
metadata, and it must return `{:ok, ast :: Macro.t}` or metadata, and it must return `{:ok, ast :: Macro.t}` or
`{:error, reason :: binary}`. If you return anything other than the literal `{:error, reason :: binary}`. If you return anything than the literal
itself as the `term`, then the AST is no longer valid. This option itself as the `term`, then the AST is no longer valid. This option
may still be useful for textual analysis of the source code. may still useful for textual analysis of the source code.
* `:static_atoms_encoder` - the static atom encoder function, see * `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this "The `:static_atoms_encoder` function" section below. Note this
@@ -1326,7 +1172,7 @@ defmodule Code do
and keyword lists. and keyword lists.
The encoder function will receive the atom name (as a binary) and a The encoder function will receive the atom name (as a binary) and a
keyword list with the current line and column. It must return keyword list with the current file, line and column. It must return
`{:ok, token :: term} | {:error, reason :: binary}`. `{:ok, token :: term} | {:error, reason :: binary}`.
The encoder function is supposed to create an atom from the given The encoder function is supposed to create an atom from the given
@@ -1350,22 +1196,21 @@ defmodule Code do
* atoms used to represent single-letter sigils like `:sigil_X` * atoms used to represent single-letter sigils like `:sigil_X`
(but multi-letter sigils like `:sigil_XYZ` are encoded). (but multi-letter sigils like `:sigil_XYZ` are encoded).
## Examples
iex> Code.string_to_quoted("1 + 3")
{:ok, {:+, [line: 1], [1, 3]}}
iex> Code.string_to_quoted("1 \ 3")
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
""" """
@spec string_to_quoted(List.Chars.t(), parser_opts) :: @spec string_to_quoted(List.Chars.t(), keyword) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}} {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def string_to_quoted(string, opts \\ []) when is_list(opts) do def string_to_quoted(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile") file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1) line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1) column = Keyword.get(opts, :column, 1)
:elixir.string_to_quoted(to_charlist(string), line, column, file, opts)
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
{:ok, tokens} ->
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, _error_msg} = error ->
error
end
end end
@doc """ @doc """
@@ -1379,7 +1224,7 @@ defmodule Code do
Check `string_to_quoted/2` for options information. Check `string_to_quoted/2` for options information.
""" """
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t() @spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
def string_to_quoted!(string, opts \\ []) when is_list(opts) do def string_to_quoted!(string, opts \\ []) when is_list(opts) do
file = Keyword.get(opts, :file, "nofile") file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1) line = Keyword.get(opts, :line, 1)
@@ -1394,15 +1239,12 @@ defmodule Code do
while preserving information like comments and literals position. while preserving information like comments and literals position.
Returns `{:ok, quoted_form, comments}` if it succeeds, Returns `{:ok, quoted_form, comments}` if it succeeds,
`{:error, {location, error, token}}` otherwise, where `location` `{:error, {line, error, token}}` otherwise.
is keyword metadata containing the line and column of the error.
Comments are maps with the following fields: Comments are maps with the following fields:
* `:line` - The line number of the source code * `:line` - The line number of the source code
* `:column` - The column number of the source code
* `:text` - The full text of the comment, including the leading `#` * `:text` - The full text of the comment, including the leading `#`
* `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment * `:previous_eol_count` - How many end of lines there are between the comment and the previous AST node or comment
@@ -1433,7 +1275,7 @@ defmodule Code do
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) :: @spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}} {:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
charlist = to_charlist(string) charlist = to_charlist(string)
@@ -1444,7 +1286,8 @@ defmodule Code do
Process.put(:code_formatter_comments, []) Process.put(:code_formatter_comments, [])
opts = [preserve_comments: &preserve_comments/5] ++ opts opts = [preserve_comments: &preserve_comments/5] ++ opts
with {:ok, forms} <- :elixir.string_to_quoted(charlist, line, column, file, opts) do with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, column, file, opts),
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, opts) do
comments = Enum.reverse(Process.get(:code_formatter_comments)) comments = Enum.reverse(Process.get(:code_formatter_comments))
{:ok, forms, comments} {:ok, forms, comments}
end end
@@ -1457,14 +1300,12 @@ defmodule Code do
Returns the AST and a list of comments if it succeeds, raises an exception Returns the AST and a list of comments if it succeeds, raises an exception
otherwise. The exception is a `TokenMissingError` in case a token is missing otherwise. The exception is a `TokenMissingError` in case a token is missing
(usually because the expression is incomplete), `MismatchedDelimiterError` (usually because the expression is incomplete), `SyntaxError` otherwise.
(in case of mismatched opening and closing delimiters) and `SyntaxError`
otherwise.
Check `string_to_quoted/2` for options information. Check `string_to_quoted/2` for options information.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())} @spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
def string_to_quoted_with_comments!(string, opts \\ []) do def string_to_quoted_with_comments!(string, opts \\ []) do
charlist = to_charlist(string) charlist = to_charlist(string)
@@ -1473,11 +1314,13 @@ defmodule Code do
{forms, comments} {forms, comments}
{:error, {location, error, token}} -> {:error, {location, error, token}} ->
file = Keyword.get(opts, :file, "nofile") :elixir_errors.parse_error(
line = Keyword.get(opts, :line, 1) location,
column = Keyword.get(opts, :column, 1) Keyword.get(opts, :file, "nofile"),
input = {charlist, line, column, Keyword.get(opts, :indentation, 0)} error,
:elixir_errors.parse_error(location, file, error, token, input) token,
{charlist, Keyword.get(opts, :line, 1), Keyword.get(opts, :column, 1)}
)
end end
end end
@@ -1487,7 +1330,7 @@ defmodule Code do
comment = %{ comment = %{
line: line, line: line,
column: column, column: column,
previous_eol_count: min(previous_eol_count(tokens), last_comment_distance(comments, line)), previous_eol_count: previous_eol_count(tokens),
next_eol_count: next_eol_count(rest, 0), next_eol_count: next_eol_count(rest, 0),
text: List.to_string(comment) text: List.to_string(comment)
} }
@@ -1501,9 +1344,6 @@ defmodule Code do
defp next_eol_count([?\r, ?\n | rest], count), do: next_eol_count(rest, count + 1) defp next_eol_count([?\r, ?\n | rest], count), do: next_eol_count(rest, count + 1)
defp next_eol_count(_, count), do: count defp next_eol_count(_, count), do: count
defp last_comment_distance([%{line: last_line} | _], line), do: line - last_line
defp last_comment_distance([], _line), do: :infinity
defp previous_eol_count([{token, {_, _, count}} | _]) defp previous_eol_count([{token, {_, _, count}} | _])
when token in [:eol, :",", :";"] and count > 0 do when token in [:eol, :",", :";"] and count > 0 do
count count
@@ -1549,9 +1389,6 @@ defmodule Code do
## Options ## Options
This function accepts all options supported by `format_string!/2` for controlling
code formatting, plus these additional options:
* `:comments` - the list of comments associated with the quoted expression. * `:comments` - the list of comments associated with the quoted expression.
Defaults to `[]`. It is recommended that both `:token_metadata` and Defaults to `[]`. It is recommended that both `:token_metadata` and
`:literal_encoder` options are given to `string_to_quoted_with_comments/2` `:literal_encoder` options are given to `string_to_quoted_with_comments/2`
@@ -1562,17 +1399,17 @@ defmodule Code do
`string_to_quoted/2`, setting this option to `false` will prevent it from `string_to_quoted/2`, setting this option to `false` will prevent it from
escaping the sequences twice. Defaults to `true`. escaping the sequences twice. Defaults to `true`.
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
The arity may be the atom `:*`, which implies all arities of
that name. The formatter already includes a list of functions
and this option augments this list.
* `:syntax_colors` - a keyword list of colors the output is colorized. * `:syntax_colors` - a keyword list of colors the output is colorized.
See `Inspect.Opts` for more information. See `Inspect.Opts` for more information.
See `format_string!/2` for the full list of formatting options including
`:file`, `:line`, `:locals_without_parens`, `:force_do_end_blocks`, and all
migration options like `:migrate_charlists_as_sigils`. Note `:line_length`
does not apply here.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) :: @spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
Inspect.Algebra.t()
def quoted_to_algebra(quoted, opts \\ []) do def quoted_to_algebra(quoted, opts \\ []) do
quoted quoted
|> Code.Normalizer.normalize(opts) |> Code.Normalizer.normalize(opts)
@@ -1650,19 +1487,13 @@ defmodule Code do
nil nil
:proceed -> :proceed ->
try do loaded =
loaded = Module.ParallelChecker.verify(fn ->
Module.ParallelChecker.verify(fn -> :elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end) end)
end)
:elixir_code_server.cast({:required, file}) :elixir_code_server.cast({:required, file})
loaded loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
end end
end end
@@ -1688,7 +1519,7 @@ defmodule Code do
@doc """ @doc """
Stores all given compilation options. Stores all given compilation options.
Changing the compilation options affects all processes Changing the compilation options affect all processes
running in a given Erlang VM node. To store individual running in a given Erlang VM node. To store individual
options and for a description of all options, see options and for a description of all options, see
`put_compiler_option/2`. `put_compiler_option/2`.
@@ -1697,8 +1528,8 @@ defmodule Code do
## Examples ## Examples
Code.compiler_options(infer_signatures: false) Code.compiler_options(warnings_as_errors: true)
#=> %{infer_signatures: [:elixir]} #=> %{warnings_as_errors: false}
""" """
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term} @spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1727,12 +1558,6 @@ defmodule Code do
:elixir_config.get(key) :elixir_config.get(key)
end end
# TODO: Remove me in Elixir v2.0
def get_compiler_option(:warnings_as_errors) do
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
:ok
end
@doc """ @doc """
Returns a list with all available compiler options. Returns a list with all available compiler options.
@@ -1752,32 +1577,25 @@ defmodule Code do
@doc """ @doc """
Stores a compilation option. Stores a compilation option.
Changing the compilation options affects all processes running in a Changing the compilation options affect all processes running in a
given Erlang VM node. given Erlang VM node.
Available options are: Available options are:
* `:debug_info` - when `true`, retains debug information in the compiled * `:docs` - when `true`, retains documentation in the compiled module.
module. This option can also be overridden per module using the `@compile` Defaults to `true`.
directive. Defaults to `true`.
This enables tooling to partially reconstruct the original source code, * `:debug_info` - when `true`, retains debug information in the compiled
for instance, to perform static analysis of code. Therefore, disabling module. Defaults to `true`.
This enables static analysis tools as it allows developers to
partially reconstruct the original source code. Therefore, disabling
`:debug_info` is not recommended as it removes the ability of the `:debug_info` is not recommended as it removes the ability of the
Elixir compiler and other tools to provide feedback. If you want to Elixir compiler and other tools to provide feedback. If you want to
remove the `:debug_info` while deploying, tools like `mix release` remove the `:debug_info` while deploying, tools like `mix release`
already do such by default. already do such by default.
Additionally, `mix test` disables it via the `:test_elixirc_options`
Other environments, such as `mix test`, automatically disable this project configuration option.
via the `:test_elixirc_options` project configuration, as there is This option can also be overridden per module using the `@compile` directive.
typically no need to store debug chunks for test files.
* `:docs` - when `true`, retains documentation in the compiled module.
Defaults to `true`.
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
debug metadata for BEAM debuggers. Defaults to `[]`.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn * `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added. when a protocol has already been consolidated and a new implementation is added.
@@ -1786,41 +1604,20 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has * `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`. already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - a list of applications whose modules * `:relative_paths` - when `true`, uses relative paths in quoted nodes,
should be used during type inference. When `false`, it disables module-local warnings, and errors generated by the compiler. Note disabling this option
signature inference used when type checking remote calls to the compiled won't affect runtime warnings and errors. Defaults to `true`.
module. Type checking will be executed regardless of the value of this option.
Mix projects will set this option to your dependencies list in dev/prod, and
it will disable this option during test (as there is typically no need to infer
signatures for test files). Outside of Mix projects, it defaults to `[:elixir]`.
* `:module_definition` (since v1.20.0) - stores if the module definition should * `:warnings_as_errors` - causes compilation to fail when warnings are
be `:compiled` (the default) or `:interpreted`. Note this does not affect the generated. Defaults to `false`.
`.beam` file written to disk, only how the contents inside `defmodule` are
executed. Using the `:interpreted` mode may offer better compilation times for
large projects, especially on machines with high core count, however, it comes
with some downsides:
* Errors during compilation may have less precise stacktraces
* Anonymous functions within `defmodule` can have only up to 20 arguments.
If this is an issue, you can use maps or tuples to group the data.
Note the functions themselves inside `defmodule`, such as the ones defined
inside `def` and friends, can still have up to 255 arguments
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}` * `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined at compilation time. Pass atom `:all` to skip warning for all undefined
functions. This can be useful when doing dynamic compilation. Defaults to `[]`. functions. This can be useful when doing dynamic compilation. Defaults to `[]`.
* `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`. * `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during
When `:raise` (the default), undefined variables will trigger a compilation compilation. See the module docs for more information. Defaults to `[]`.
error. You may set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
* `:parser_options` (since v1.10.0) - a keyword list of options to be given * `:parser_options` (since v1.10.0) - a keyword list of options to be given
to the parser when compiling files. It accepts the same options as to the parser when compiling files. It accepts the same options as
@@ -1831,12 +1628,14 @@ defmodule Code do
and `compile_file/2` but not `string_to_quoted/2` and friends, as the and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation. latter is used for other purposes beyond compilation.
* `:relative_paths` - when `true`, uses relative paths in quoted nodes, * `:on_undefined_variable` (since v1.15.0) - either `:raise` or `:warn`.
warnings, and errors generated by the compiler. Note disabling this option When `:raise` (the default), undefined variables will trigger a compilation
won't affect runtime warnings and errors. Defaults to `true`. error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
* `:tracers` (since v1.10.0) - a list of tracers (modules) to be used during of the same name (for example, `node` would be expanded as `node()`).
compilation. See the module docs for more information. Defaults to `[]`. This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
It always returns `:ok`. Raises an error for invalid options. It always returns `:ok`. Raises an error for invalid options.
@@ -1857,6 +1656,16 @@ defmodule Code do
:ok :ok
end end
def put_compiler_option(:no_warn_undefined, value) do
if value != :all and not is_list(value) do
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
"got: #{inspect(value)}"
end
:elixir_config.put(:no_warn_undefined, value)
:ok
end
def put_compiler_option(key, value) when key in @list_compiler_options do def put_compiler_option(key, value) when key in @list_compiler_options do
if not is_list(value) do if not is_list(value) do
raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}" raise "compiler option #{inspect(key)} should be a list, got: #{inspect(value)}"
@@ -1876,70 +1685,9 @@ defmodule Code do
:ok :ok
end end
def put_compiler_option(:module_definition, value) do # TODO: Make this option have no effect on Elixir v2.0
if value not in [:interpreted, :compiled] do # TODO: Warn if mode is :warn on Elixir v1.19
raise "compiler option :module_definition should be either :interpreted or :compiled, got: #{inspect(value)}"
end
:elixir_config.put(:module_definition, value)
:ok
end
def put_compiler_option(:infer_signatures, value) do
value =
cond do
value == false ->
false
value == true ->
[:elixir]
is_list(value) and Enum.all?(value, &is_atom/1) ->
value
true ->
raise "compiler option :infer_signatures should be a boolean or a list of applications, got: #{inspect(value)}"
end
:elixir_config.put(:infer_signatures, value)
:ok
end
def put_compiler_option(:no_warn_undefined, value) do
if value != :all and not is_list(value) do
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
"got: #{inspect(value)}"
end
:elixir_config.put(:no_warn_undefined, value)
:ok
end
# TODO: Remove me in Elixir v2.0
def put_compiler_option(:warnings_as_errors, _value) do
IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
"instead you must pass it as a --warnings-as-errors flag. " <>
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
"[compile: \"compile --warnings-as-errors\"]"
)
:ok
end
# TODO: Remove me in Elixir v2.0
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
if value == :warn do
IO.warn_once(
{__MODULE__, :on_undefined_variable},
fn ->
"setting :on_undefined_variable to :warn is deprecated. " <>
"The warning behaviour will be removed in future releases"
end,
3
)
end
:elixir_config.put(:on_undefined_variable, value) :elixir_config.put(:on_undefined_variable, value)
:ok :ok
end end
@@ -2150,7 +1898,7 @@ defmodule Code do
If the module being checked is currently in a compiler deadlock, If the module being checked is currently in a compiler deadlock,
this function returns `{:error, :unavailable}`. Unavailable doesn't this function returns `{:error, :unavailable}`. Unavailable doesn't
necessarily mean the module doesn't exist, just that it is not currently necessarily mean the module doesn't exist, just that it is not currently
available, but it may (or may not) become available in the future. available, but it (or may not) become available in the future.
Therefore, if you can only continue if the module is available, use Therefore, if you can only continue if the module is available, use
`ensure_compiled!/1` instead. In particular, do not do this: `ensure_compiled!/1` instead. In particular, do not do this:
@@ -2204,7 +1952,7 @@ defmodule Code do
case :code.ensure_loaded(module) do case :code.ensure_loaded(module) do
{:error, :nofile} = error -> {:error, :nofile} = error ->
if can_await_module_compilation?() do if can_await_module_compilation?() do
case Kernel.ErrorHandler.ensure_compiled(module, :module, mode, nil) do case Kernel.ErrorHandler.ensure_compiled(module, :module, mode) do
:found -> {:module, module} :found -> {:module, module}
:deadlock -> {:error, :unavailable} :deadlock -> {:error, :unavailable}
:not_found -> {:error, :nofile} :not_found -> {:error, :nofile}
@@ -2226,7 +1974,7 @@ defmodule Code do
## Examples ## Examples
iex> Code.loaded?(String) iex> Code.loaded?(Atom)
true true
iex> Code.loaded?(NotYetLoaded) iex> Code.loaded?(NotYetLoaded)
+104 -262
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Formatter do defmodule Code.Formatter do
@moduledoc false @moduledoc false
import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4] import Inspect.Algebra, except: [format: 2, surround: 3, surround: 4]
@@ -20,7 +16,7 @@ defmodule Code.Formatter do
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1) @ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
# Operators that are composed of multiple binary operators # Operators that are composed of multiple binary operators
@multi_binary_operators [:..//] @multi_binary_operators [:"..//"]
# Operators that do not have space between operands # Operators that do not have space between operands
@no_space_binary_operators [:.., :"//"] @no_space_binary_operators [:.., :"//"]
@@ -35,37 +31,30 @@ defmodule Code.Formatter do
@right_new_line_before_binary_operators [:|, :when] @right_new_line_before_binary_operators [:|, :when]
# Operators that are logical cannot be mixed without parens # Operators that are logical cannot be mixed without parens
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and] @required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
# Operators with next break fits # Operators with next break fits
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"] @next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
# Operators that always require parens even # Operators that always require parens on operands when they are the parent
# when they are their own parents as they are not semantically associative
@required_parens_even_when_parent [:--, :---]
# Operators that always require parens on operands
# when they are the parent of another operator with a difference precedence
# Most operators are listed, except comparison, arithmetic, and low precedence
@required_parens_on_binary_operands [ @required_parens_on_binary_operands [
:|>,
:<<<, :<<<,
:>>>, :>>>,
:|>,
:<~, :<~,
:~>, :~>,
:<<~, :<<~,
:~>>, :~>>,
:<~>, :<~>,
:"<|>", :"<|>",
:in,
:"^^^", :"^^^",
:"//",
:++,
:--,
:+++, :+++,
:---, :---,
:<>, :in,
:.. :++,
:--,
:..,
:<>
] ]
@locals_without_parens [ @locals_without_parens [
@@ -158,7 +147,6 @@ defmodule Code.Formatter do
@doc """ @doc """
Converts the quoted expression into an algebra document. Converts the quoted expression into an algebra document.
""" """
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
def to_algebra(quoted, opts \\ []) do def to_algebra(quoted, opts \\ []) do
comments = Keyword.get(opts, :comments, []) comments = Keyword.get(opts, :comments, [])
@@ -194,12 +182,8 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, []) locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil) file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, []) sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false) normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
migrate_atom_interpolations = Keyword.get(opts, :migrate_atom_interpolations, migrate) normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_call_parens_on_pipe = Keyword.get(opts, :migrate_call_parens_on_pipe, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
syntax_colors = Keyword.get(opts, :syntax_colors, []) syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils = sigils =
@@ -224,11 +208,8 @@ defmodule Code.Formatter do
comments: comments, comments: comments,
sigils: sigils, sigils: sigils,
file: file, file: file,
migrate_atom_interpolations: migrate_atom_interpolations, normalize_bitstring_modifiers: normalize_bitstring_modifiers,
migrate_bitstring_modifiers: migrate_bitstring_modifiers, normalize_charlists_as_sigils: normalize_charlists_as_sigils,
migrate_call_parens_on_pipe: migrate_call_parens_on_pipe,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors} inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
} }
end end
@@ -285,7 +266,7 @@ defmodule Code.Formatter do
end end
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state} {var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
end end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -336,20 +317,14 @@ defmodule Code.Formatter do
end end
defp quoted_to_algebra( defp quoted_to_algebra(
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries} = bitstring, :utf8]} = {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} = quoted,
quoted,
context, context,
state state
) do ) do
cond do if interpolated?(entries) do
not interpolated?(entries) -> interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
remote_to_algebra(quoted, context, state) else
remote_to_algebra(quoted, context, state)
state.migrate_atom_interpolations ->
quoted_to_algebra(quote(do: String.to_unsafe_atom(unquote(bitstring))), context, state)
true ->
interpolation_to_algebra(entries, @double_quote, state, ":\"", @double_quote)
end end
end end
@@ -422,7 +397,7 @@ defmodule Code.Formatter do
{@double_heredoc {@double_heredoc
|> concat(string) |> concat(string)
|> concat(@double_heredoc) |> concat(@double_heredoc)
|> color_doc(:string, state.inspect_opts) |> color(:string, state.inspect_opts)
|> force_unfit(), state} |> force_unfit(), state}
else else
string = escape_string(string, @double_quote) string = escape_string(string, @double_quote)
@@ -430,7 +405,7 @@ defmodule Code.Formatter do
{@double_quote {@double_quote
|> concat(string) |> concat(string)
|> concat(@double_quote) |> concat(@double_quote)
|> color_doc(:string, state.inspect_opts), state} |> color(:string, state.inspect_opts), state}
end end
end end
@@ -462,7 +437,7 @@ defmodule Code.Formatter do
end end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color_doc("nil", nil, state.inspect_opts), state} {color("nil", nil, state.inspect_opts), state}
end end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -479,7 +454,7 @@ defmodule Code.Formatter do
end end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1))) {Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color_doc(:atom, state.inspect_opts), state} |> color(:atom, state.inspect_opts), state}
end end
# &1 # &1
@@ -502,62 +477,6 @@ defmodule Code.Formatter do
binary_op_to_algebra(:in, "not in", meta, left, right, context, state) binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
end end
# disable migrate_call_parens_on_pipe within defmacro
defp quoted_to_algebra(
{atom, _, [{:|>, _, _}, _]} = ast,
context,
%{migrate_call_parens_on_pipe: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_call_parens_on_pipe: false})
end
defp quoted_to_algebra(
{atom, _, [{:unless, _, _}, _]} = ast,
context,
%{migrate_unless: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
end
# rewrite unless as if!
defp quoted_to_algebra(
{:unless, meta, [condition, block]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
end
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
quoted_to_algebra(
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
context,
state
)
end
# condition |> unless(...) => negated(condition) |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra(
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
context,
state
)
end
# .. # ..
defp quoted_to_algebra({:.., _meta, []}, context, state) do defp quoted_to_algebra({:.., _meta, []}, context, state) do
if context in [:no_parens_arg, :no_parens_one_arg] do if context in [:no_parens_arg, :no_parens_one_arg] do
@@ -573,7 +492,7 @@ defmodule Code.Formatter do
end end
# 1..2//3 # 1..2//3
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state) quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
end end
@@ -605,12 +524,20 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do if keyword_key?(left_arg) do
{left, state} = {left, state} =
case left_arg do case left_arg do
{:__block__, _, [atom]} when is_atom(atom) -> # TODO: Remove this clause in v1.18 when we no longer quote operator :..//
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2) {:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), state}
{formatted {:__block__, _, [atom]} when is_atom(atom) ->
|> string() iodata =
|> color_doc(:atom, state.inspect_opts), state} if Macro.classify_atom(atom) in [:identifier, :unquoted] do
[Atom.to_string(atom), ?:]
else
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
end
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} -> {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":") interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -626,22 +553,16 @@ defmodule Code.Formatter do
end end
doc = doc =
concat( with_next_break_fits(next_break_fits?(right_arg, state), right, fn right ->
group(left), concat(group(left), group(nest(glue(op, group(right)), 2, :break)))
with_next_break_fits(next_break_fits?(right_arg, state), right, fn right -> end)
nest(glue(op, right), 2, :break)
end)
)
{doc, state} {doc, state}
end end
# #PID's and #Ref's may appear on regular AST # #PID's and #Ref's may appear on regular AST
# Other foreign structures, such as maps and structs,
# may appear from Macro.to_string, so we stick a limit,
# although they won't be formatted accordingly.
defp quoted_to_algebra(unknown, _context, state) do defp quoted_to_algebra(unknown, _context, state) do
{inspect(unknown, printable_limit: :infinity), state} {inspect(unknown), state}
end end
## Blocks ## Blocks
@@ -711,7 +632,7 @@ defmodule Code.Formatter do
Atom.to_string(op) Atom.to_string(op)
end end
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state} {color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
@@ -814,13 +735,15 @@ defmodule Code.Formatter do
{right, state} = {right, state} =
binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0) binary_operand_to_algebra(right_arg, right_context, state, op, op_info, :right, 0)
{op_string, right} = doc =
cond do cond do
op in @no_space_binary_operators -> op in @no_space_binary_operators ->
{op_string, group(right)} op_doc = color(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators -> op in @no_newline_binary_operators ->
{" " <> op_string <> " ", group(right)} op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true -> true ->
eol? = eol?(meta, state) eol? = eol?(meta, state)
@@ -828,15 +751,14 @@ defmodule Code.Formatter do
next_break_fits? = next_break_fits? =
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol? op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
{" " <> op_string, with_next_break_fits(next_break_fits?, right, fn right ->
with_next_break_fits(next_break_fits?, right, fn right -> op_doc = color(" " <> op_string, :operator, state.inspect_opts)
right = nest(concat(break(), right), nesting, :break) right = nest(glue(op_doc, group(right)), nesting, :break)
if eol?, do: force_unfit(right), else: right right = if eol?, do: force_unfit(right), else: right
end)} concat(group(left), group(right))
end)
end end
op_doc = color_doc(op_string, :operator, state.inspect_opts)
doc = concat(concat(group(left), op_doc), group(right))
{doc, state} {doc, state}
end end
@@ -858,38 +780,6 @@ defmodule Code.Formatter do
{wrap_in_parens(doc), state} {wrap_in_parens(doc), state}
end end
# |> var
# |> var()
defp binary_operand_to_algebra(
{var, meta, var_context},
context,
%{migrate_call_parens_on_pipe: true} = state,
:|>,
_parent_info,
:right,
_nesting
)
when is_atom(var) and is_atom(var_context) do
operand = {var, meta, []}
quoted_to_algebra(operand, context, state)
end
# |> var.fun
# |> var.fun()
defp binary_operand_to_algebra(
{{:., _, [_, fun]} = call, meta, []},
context,
%{migrate_call_parens_on_pipe: true} = state,
:|>,
_parent_info,
:right,
_nesting
)
when is_atom(fun) do
meta = Keyword.put_new_lazy(meta, :closing, fn -> [line: meta[:line]] end)
quoted_to_algebra({call, meta, []}, context, state)
end
defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do defp binary_operand_to_algebra(operand, context, state, parent_op, parent_info, side, nesting) do
{parent_assoc, parent_prec} = parent_info {parent_assoc, parent_prec} = parent_info
@@ -899,13 +789,14 @@ defmodule Code.Formatter do
op_string = Atom.to_string(op) op_string = Atom.to_string(op)
cond do cond do
# If we have the same operator and it is in the correct side, # If the operator has the same precedence as the parent and is on
# we don't add parens unless it is explicitly required. # the correct side, we respect the nesting rule to avoid multiple
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent -> # nestings. This only applies for left associativity or same operator.
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting) binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the operator requires parens (most of them do) or we are mixing logical operators # If the parent requires parens or the precedence is inverted or
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis. # it is in the wrong side, then we *need* parenthesis.
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or (parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
(op in @required_parens_logical_binary_operands and (op in @required_parens_logical_binary_operands and
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
@@ -1062,7 +953,7 @@ defmodule Code.Formatter do
) )
when is_atom(fun) and is_integer(arity) do when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state) {target_doc, state} = remote_target_to_algebra(target, state)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2) fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state} {target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end end
@@ -1109,9 +1000,7 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state) {target_doc, state} = remote_target_to_algebra(target, state)
fun_doc = fun_doc =
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2) Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
|> string()
|> color_doc(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc) remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1179,7 +1068,7 @@ defmodule Code.Formatter do
fun fun
|> Atom.to_string() |> Atom.to_string()
|> string() |> string()
|> color_doc(:call, state.inspect_opts) |> color(:call, state.inspect_opts)
|> concat(call_doc) |> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1273,7 +1162,7 @@ defmodule Code.Formatter do
args_doc = args_doc =
if skip_parens? do if skip_parens? do
left_doc left_doc
|> concat(group(right_doc, :optimistic)) |> concat(next_break_fits(group(right_doc, :inherit), :enabled))
|> nest(:cursor, :break) |> nest(:cursor, :break)
else else
right_doc = right_doc =
@@ -1281,7 +1170,8 @@ defmodule Code.Formatter do
|> nest(2, :break) |> nest(2, :break)
|> concat(break("")) |> concat(break(""))
|> concat(")") |> concat(")")
|> group(:optimistic) |> group(:inherit)
|> next_break_fits(:enabled)
concat(nest(left_doc, 2, :break), right_doc) concat(nest(left_doc, 2, :break), right_doc)
end end
@@ -1324,11 +1214,13 @@ defmodule Code.Formatter do
|> concat(args_doc) |> concat(args_doc)
|> nest(2) |> nest(2)
|> concat(extra) |> concat(extra)
|> group()
skip_parens? -> skip_parens? ->
" " " "
|> concat(args_doc) |> concat(args_doc)
|> concat(extra) |> concat(extra)
|> group()
true -> true ->
"(" "("
@@ -1336,12 +1228,13 @@ defmodule Code.Formatter do
|> nest(2, :break) |> nest(2, :break)
|> concat(args_doc) |> concat(args_doc)
|> concat(extra) |> concat(extra)
|> group()
end end
if next_break_fits? do if next_break_fits? do
{group(doc, :pessimistic), state} {next_break_fits(doc, :disabled), state}
else else
{group(doc), state} {doc, state}
end end
end end
@@ -1465,7 +1358,7 @@ defmodule Code.Formatter do
metadata = [ metadata = [
file: state.file, file: state.file,
line: meta[:line], line: meta[:line],
sigil: String.to_unsafe_atom(name), sigil: String.to_atom(name),
modifiers: modifiers, modifiers: modifiers,
opening_delimiter: opening_delimiter opening_delimiter: opening_delimiter
] ]
@@ -1538,7 +1431,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state) {doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} = {spec, state} =
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::") bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
spec = wrap_in_parens_if_inspected_atom(spec) spec = wrap_in_parens_if_inspected_atom(spec)
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
@@ -1616,8 +1509,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} = {args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("[", :list, state.inspect_opts) left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts) right_bracket = color("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state} {surround(left_bracket, args_doc, right_bracket), state}
end end
@@ -1649,8 +1542,8 @@ defmodule Code.Formatter do
end end
defp do_map_to_algebra(name_doc, args_doc, state) do defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts) name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state} {surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
end end
defp tuple_to_algebra(meta, args, join, state) do defp tuple_to_algebra(meta, args, join, state) do
@@ -1660,8 +1553,8 @@ defmodule Code.Formatter do
{args_doc, join, state} = {args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun) args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("{", :tuple, state.inspect_opts) left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts) right_bracket = color("}", :tuple, state.inspect_opts)
if join == :flex_break do if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state} {left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1671,11 +1564,16 @@ defmodule Code.Formatter do
end end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts) Atom.to_string(atom) |> color(:boolean, inspect_opts)
end end
defp atom_to_algebra(nil, _, inspect_opts) do defp atom_to_algebra(nil, _, inspect_opts) do
Atom.to_string(nil) |> color_doc(nil, inspect_opts) Atom.to_string(nil) |> color(nil, inspect_opts)
end
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
defp atom_to_algebra(:"..//", _, inspect_opts) do
string(":\"..//\"") |> color(:atom, inspect_opts)
end end
defp atom_to_algebra(:\\, meta, inspect_opts) do defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1688,7 +1586,7 @@ defmodule Code.Formatter do
_ -> ":\\\\" _ -> ":\\\\"
end end
string(string) |> color_doc(:atom, inspect_opts) string(string) |> color(:atom, inspect_opts)
end end
defp atom_to_algebra(atom, _, inspect_opts) do defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1701,10 +1599,10 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"] [?:, ?", String.replace(string, "\"", "\\\""), ?"]
end end
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts) iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
end end
defp integer_to_algebra(text, inspect_opts) do defp integer_to_algebra(text, inspect_otps) do
case text do case text do
<<?0, ?x, rest::binary>> -> <<?0, ?x, rest::binary>> ->
"0x" <> String.upcase(rest) "0x" <> String.upcase(rest)
@@ -1718,15 +1616,15 @@ defmodule Code.Formatter do
decimal -> decimal ->
insert_underscores(decimal) insert_underscores(decimal)
end end
|> color_doc(:number, inspect_opts) |> color(:number, inspect_otps)
end end
defp float_to_algebra(text, inspect_opts) do defp float_to_algebra(text, inspect_otps) do
[int_part, decimal_part] = :binary.split(text, ".") [int_part, decimal_part] = :binary.split(text, ".")
decimal_part = String.downcase(decimal_part) decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_opts) color(string, :number, inspect_otps)
end end
defp insert_underscores("-" <> digits) do defp insert_underscores("-" <> digits) do
@@ -1807,17 +1705,10 @@ defmodule Code.Formatter do
doc = doc =
case args do case args do
[_ | _] -> [_ | _] -> concat_to_last_group(doc, ",")
concat_to_last_group(doc, ",") [] when last_arg_mode == :force_comma -> concat_to_last_group(doc, ",")
[] when last_arg_mode == :next_break_fits -> next_break_fits(doc, :enabled)
[] when last_arg_mode == :force_comma -> [] when last_arg_mode == :none -> doc
concat_to_last_group(doc, ",")
[] when last_arg_mode == :next_break_fits ->
doc |> ungroup_if_group() |> group(:optimistic)
[] when last_arg_mode == :none ->
doc
end end
{{doc, @empty, 1}, state} {{doc, @empty, 1}, state}
@@ -2335,14 +2226,11 @@ defmodule Code.Formatter do
defp with_next_break_fits(condition, doc, fun) do defp with_next_break_fits(condition, doc, fun) do
if condition do if condition do
doc doc
|> group(:optimistic) |> next_break_fits(:enabled)
|> fun.() |> fun.()
|> group(:pessimistic) |> next_break_fits(:disabled)
else else
doc fun.(doc)
|> group()
|> fun.()
|> group()
end end
end end
@@ -2501,24 +2389,20 @@ defmodule Code.Formatter do
meta[:closing][:line] || @min_line meta[:closing][:line] || @min_line
end end
defp escape_atom(string, char) do
String.replace(string, <<char>>, <<?\\, char>>)
end
## Algebra helpers ## Algebra helpers
# Relying on the inner document is brittle and error prone. # Relying on the inner document is brittle and error prone.
# It would be best if we had a mechanism to apply this. # It would be best if we had a mechanism to apply this.
defp concat_to_last_group([left | right], concat) do defp concat_to_last_group({:doc_cons, left, right}, concat) do
[left | concat_to_last_group(right, concat)] {:doc_cons, left, concat_to_last_group(right, concat)}
end end
defp concat_to_last_group({:doc_group, group, mode}, concat) do defp concat_to_last_group({:doc_group, group, mode}, concat) do
{:doc_group, concat(group, concat), mode} {:doc_group, {:doc_cons, group, concat}, mode}
end end
defp concat_to_last_group(other, concat) do defp concat_to_last_group(other, concat) do
concat(other, concat) {:doc_cons, other, concat}
end end
defp ungroup_if_group({:doc_group, group, _mode}), do: group defp ungroup_if_group({:doc_group, group, _mode}), do: group
@@ -2550,7 +2434,7 @@ defmodule Code.Formatter do
end end
defp get_charlist_quotes(:heredoc, state) do defp get_charlist_quotes(:heredoc, state) do
if state.migrate_charlists_as_sigils do if state.normalize_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc} {@sigil_c_heredoc, @double_heredoc}
else else
{@single_heredoc, @single_heredoc} {@single_heredoc, @single_heredoc}
@@ -2559,7 +2443,7 @@ defmodule Code.Formatter do
defp get_charlist_quotes({:regular, chunks}, state) do defp get_charlist_quotes({:regular, chunks}, state) do
cond do cond do
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote} !state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote} Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
true -> {@sigil_c_double, @double_quote} true -> {@sigil_c_double, @double_quote}
end end
@@ -2568,46 +2452,4 @@ defmodule Code.Formatter do
defp has_double_quote?(chunk) do defp has_double_quote?(chunk) do
is_binary(chunk) and chunk =~ @double_quote is_binary(chunk) and chunk =~ @double_quote
end end
# Migration rewrites
@bool_operators [
:>,
:>=,
:<,
:<=,
:in
]
@guards [
:is_atom,
:is_boolean,
:is_nil,
:is_number,
:is_integer,
:is_float,
:is_binary,
:is_map,
:is_struct,
:is_non_struct_map,
:is_exception,
:is_list,
:is_tuple,
:is_function,
:is_reference,
:is_pid,
:is_port
]
defp negate_condition(condition) do
case condition do
{neg, _, [condition]} when neg in [:!, :not] -> condition
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
_ -> {:!, [], [condition]}
end
end
end end
+48 -306
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Code.Fragment do defmodule Code.Fragment do
@moduledoc """ @moduledoc """
This module provides conveniences for analyzing fragments of This module provides conveniences for analyzing fragments of
@@ -11,61 +8,6 @@ defmodule Code.Fragment do
@type position :: {line :: pos_integer(), column :: pos_integer()} @type position :: {line :: pos_integer(), column :: pos_integer()}
@typedoc """
Options for cursor context functions.
Currently, these options are not used but reserved for future extensibility.
"""
@type cursor_opts :: []
@typedoc """
Options for converting code fragments to quoted expressions.
"""
@type container_cursor_to_quoted_opts :: [
file: String.t(),
line: pos_integer(),
column: pos_integer(),
columns: boolean(),
token_metadata: boolean(),
literal_encoder: (term(), Macro.metadata() -> {:ok, Macro.t()} | {:error, binary()}),
preserve_sigils: boolean(),
trailing_fragment: String.t()
]
@doc ~S"""
Returns the list of lines in the given string, preserving their line endings.
Only the line endings recognized by the Elixir compiler are
considered, namely `\r\n` and `\n`. If you would like to retrieve
lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.
## Examples
iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
["foo\r\n", "bar\r\n", "baz"]
iex> Code.Fragment.lines("foo\nbar\nbaz")
["foo\n", "bar\n", "baz"]
iex> Code.Fragment.lines("")
[""]
"""
@doc since: "1.19.0"
@spec lines(String.t()) :: [String.t()]
def lines(string) do
lines(string, <<>>)
end
defp lines(<<?\n, rest::binary>>, acc),
do: [<<acc::binary, ?\n>> | lines(rest, <<>>)]
defp lines(<<char, rest::binary>>, acc),
do: lines(rest, <<acc::binary, char>>)
defp lines(<<>>, acc),
do: [acc]
@doc """ @doc """
Receives a string and returns the cursor context. Receives a string and returns the cursor context.
@@ -101,9 +43,6 @@ defmodule Code.Fragment do
or `{:local_or_var, charlist}` and `charlist` is a static part or `{:local_or_var, charlist}` and `charlist` is a static part
Examples are `__MODULE__.Submodule` or `@hello.Submodule` Examples are `__MODULE__.Submodule` or `@hello.Submodule`
* `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
catch, else, rescue) or a binary operator
* `{:dot, inside_dot, charlist}` - the context is a dot * `{:dot, inside_dot, charlist}` - the context is a dot
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`, where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot` `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
@@ -142,9 +81,6 @@ defmodule Code.Fragment do
* `{:anonymous_call, inside_caller}` - the context is an anonymous * `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`. call, such as `fun.(` and `@fun.(`.
* `{:capture_arg, charlist}` - the context is a capture argument,
such as `&1`
* `{:module_attribute, charlist}` - the context is a module attribute, * `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor` such as `@hello_wor`
@@ -162,8 +98,8 @@ defmodule Code.Fragment do
* `:none` - no context possible * `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning * `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`. Operators starting with `~`, such as of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`, are returned as :operator contexts `~>` and `~>>`
* `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`. * `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
`inside_struct` can either be a `charlist` in case of a static alias or an `inside_struct` can either be a `charlist` in case of a static alias or an
@@ -197,10 +133,9 @@ defmodule Code.Fragment do
references, and more. references, and more.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), cursor_opts()) :: @spec cursor_context(List.Chars.t(), keyword()) ::
{:alias, charlist} {:alias, charlist}
| {:alias, inside_alias, charlist} | {:alias, inside_alias, charlist}
| {:block_keyword_or_binary_operator, charlist}
| {:dot, inside_dot, charlist} | {:dot, inside_dot, charlist}
| {:dot_arity, inside_dot, charlist} | {:dot_arity, inside_dot, charlist}
| {:dot_call, inside_dot, charlist} | {:dot_call, inside_dot, charlist}
@@ -209,7 +144,6 @@ defmodule Code.Fragment do
| {:local_arity, charlist} | {:local_arity, charlist}
| {:local_call, charlist} | {:local_call, charlist}
| {:anonymous_call, inside_caller} | {:anonymous_call, inside_caller}
| {:capture_arg, charlist}
| {:module_attribute, charlist} | {:module_attribute, charlist}
| {:operator, charlist} | {:operator, charlist}
| {:operator_arity, charlist} | {:operator_arity, charlist}
@@ -251,15 +185,15 @@ defmodule Code.Fragment do
cursor_context(to_charlist(other), opts) cursor_context(to_charlist(other), opts)
end end
@operators ~c"\\<>+-*/:=|&~^%!$" @operators ~c"\\<>+-*/:=|&~^%!"
@starting_punctuation ~c",([{;" @starter_punctuation ~c",([{;"
@closing_punctuation ~c")]}\"'" @non_starter_punctuation ~c")]}\"'.$"
@space ~c"\t\s" @space ~c"\t\s"
@trailing_identifier ~c"?!" @trailing_identifier ~c"?!"
@tilde_op_prefix ~c"<=~" @tilde_op_prefix ~c"<=~"
@non_identifier @trailing_identifier ++ @non_identifier @trailing_identifier ++
@operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.] @operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space
@textual_operators ~w(when not and or in)c @textual_operators ~w(when not and or in)c
@keywords ~w(do end after else catch rescue fn true false nil)c @keywords ~w(do end after else catch rescue fn true false nil)c
@@ -289,11 +223,11 @@ defmodule Code.Fragment do
# A local arity definition # A local arity definition
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1)) [?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
# Starting a new expression # Starting a new expression
[h | _] when h in @starting_punctuation -> {:expr, 0} [h | _] when h in @starter_punctuation -> {:expr, 0}
# It is keyword, binary operator, a local or remote call without parens # It is a local or remote call without parens
rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces}) rest when spaces > 0 -> call_to_cursor_context({rest, spaces})
# It is an identifier # It is an identifier
_ -> identifier_to_cursor_context(reverse, spaces, false) _ -> identifier_to_cursor_context(reverse, 0, false)
end end
end end
@@ -308,8 +242,7 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count} {{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count} {{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count} {{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{{:sigil, _}, _} -> {:none, 0} {_, _} -> {:none, 0}
{_, _} -> {{:operator, ~c"/"}, 1}
end end
end end
@@ -333,16 +266,8 @@ defmodule Code.Fragment do
end end
end end
defp closing_or_call_to_cursor_context({reverse, spaces}) do
if closing?(reverse) do
{{:block_keyword_or_binary_operator, ~c""}, 0}
else
call_to_cursor_context({reverse, spaces})
end
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3} defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3} defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2} defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2} defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
@@ -392,42 +317,12 @@ defmodule Code.Fragment do
{~c"." ++ rest, count} when rest == [] or hd(rest) != ?. -> {~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
dot(rest, count + 1, acc) dot(rest, count + 1, acc)
{rest, rest_count} -> _ ->
response = {{:local_or_var, acc}, count}
if rest_count > count and closing?(rest),
do: :block_keyword_or_binary_operator,
else: :local_or_var
{{response, acc}, count}
end end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end end
end end
# If it is a closing punctuation
defp closing?([h | _]) when h in @closing_punctuation, do: true
# Closing bitstring (but deal with operators)
defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
# Keywords
defp closing?(rest) do
case split_non_identifier(rest, []) do
{~c"nil", _} -> true
{~c"true", _} -> true
{~c"false", _} -> true
{[digit | _], _} when digit in ?0..?9 -> true
{[upper | _], _} when upper in ?A..?Z -> true
{[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
{_, _} -> false
end
end
defp split_non_identifier([h | t], acc) when h not in @non_identifier,
do: split_non_identifier(t, [h | acc])
defp split_non_identifier(rest, acc), do: {acc, rest}
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??]) defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!]) defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
defp identifier(rest, count), do: check_identifier(rest, count, []) defp identifier(rest, count), do: check_identifier(rest, count, [])
@@ -467,14 +362,6 @@ defmodule Code.Fragment do
:none :none
end end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc) tokenize_identifier(rest, count, acc)
end end
@@ -662,7 +549,7 @@ defmodule Code.Fragment do
iex> Code.Fragment.surround_context("foo", {1, 1}) iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}} %{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
## Differences from `cursor_context/2` ## Differences to `cursor_context/2`
Because `surround_context/3` attempts to capture complex expressions, Because `surround_context/3` attempts to capture complex expressions,
it has some differences to `cursor_context/2`: it has some differences to `cursor_context/2`:
@@ -675,13 +562,13 @@ defmodule Code.Fragment do
`local_call`/`local_arity` and `local_or_var`, since the latter can `local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}` * `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`) (in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs * This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ~c""}` as context `{:struct, ''}` as context
* This function returns keywords as `{:keyword, ~c"do"}` * This function returns keywords as `{:keyword, 'do'}`
* This function never returns `:expr` * This function never returns `:expr`
@@ -689,7 +576,7 @@ defmodule Code.Fragment do
of examples and their return values. of examples and their return values.
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), cursor_opts()) :: @spec surround_context(List.Chars.t(), position(), keyword()) ::
%{begin: position, end: position, context: context} | :none %{begin: position, end: position, context: context} | :none
when context: when context:
{:alias, charlist} {:alias, charlist}
@@ -703,9 +590,7 @@ defmodule Code.Fragment do
| {:sigil, charlist} | {:sigil, charlist}
| {:struct, inside_struct} | {:struct, inside_struct}
| {:unquoted_atom, charlist} | {:unquoted_atom, charlist}
| {:keyword, charlist} | {:keyword, charlist},
| {:key, charlist}
| {:capture_arg, charlist},
inside_dot: inside_dot:
{:alias, charlist} {:alias, charlist}
| {:alias, inside_alias, charlist} | {:alias, inside_alias, charlist}
@@ -755,16 +640,7 @@ defmodule Code.Fragment do
maybe_operator(reversed_pre, post, line, opts) maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} -> {:identifier, reversed_post, rest} ->
{keyword_key?, rest} = {rest, _} = strip_spaces(rest, 0)
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
reversed = reversed_post ++ reversed_pre reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do case codepoint_cursor_context(reversed, opts) do
@@ -780,9 +656,6 @@ defmodule Code.Fragment do
{{:dot, _, [_ | _]} = dot, offset} -> {{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset) build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( -> {{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset) build_surround({:local_call, acc}, reversed, line, offset)
@@ -798,12 +671,6 @@ defmodule Code.Fragment do
{{:local_or_var, acc}, offset} -> {{:local_or_var, acc}, offset} ->
build_surround({:local_or_var, acc}, reversed, line, offset) build_surround({:local_or_var, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
build_surround({:keyword, acc}, reversed, line, offset)
{{:module_attribute, ~c""}, offset} -> {{:module_attribute, ~c""}, offset} ->
build_surround({:operator, ~c"@"}, reversed, line, offset) build_surround({:operator, ~c"@"}, reversed, line, offset)
@@ -816,9 +683,6 @@ defmodule Code.Fragment do
{{:unquoted_atom, acc}, offset} -> {{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset) build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ -> _ ->
maybe_operator(reversed_pre, post, line, opts) maybe_operator(reversed_pre, post, line, opts)
end end
@@ -851,16 +715,6 @@ defmodule Code.Fragment do
reversed = reversed_post ++ reversed_pre reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} -> {{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset) build_surround({:operator, acc}, reversed, line, offset)
@@ -1207,23 +1061,16 @@ defmodule Code.Fragment do
iex> Code.Fragment.container_cursor_to_quoted("foo +") iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}} {:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
In order to parse the left-side of `->` properly, which appears both
in anonymous functions and do-end blocks, the trailing fragment option
must be given with the rest of the contents:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
## Options ## Options
* `:file` - the filename to be reported in case of parsing errors. * `:file` - the filename to be reported in case of parsing errors.
Defaults to `"nofile"`. Defaults to `"nofile"`.
* `:line` - the starting line of the string being parsed. * `:line` - the starting line of the string being parsed.
Defaults to `1`. Defaults to 1.
* `:column` - the starting column of the string being parsed. * `:column` - the starting column of the string being parsed.
Defaults to `1`. Defaults to 1.
* `:columns` - when `true`, attach a `:column` key to the quoted * `:columns` - when `true`, attach a `:column` key to the quoted
metadata. Defaults to `false`. metadata. Defaults to `false`.
@@ -1236,151 +1083,46 @@ defmodule Code.Fragment do
* `:literal_encoder` - a function to encode literals in the AST. * `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information. See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
* `:preserve_sigils` (since v1.20.0) - preserve sigil cursor location
(see "Tracking sigils" section below)
## Tracking sigils
The `:preserve_sigils` option can be used to track cursor positions inside
a sigil.
If the sigil is terminated abruptly, the `sigil_*` call will have the cursor
as the second argument:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, {:__cursor__, [line: 1, column: 7], []}]}}
In case the sigil is completed and has zero or more modifiers, the cursor will
be nested in the list, with all previous delimiters specified:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i", preserve_sigils: true)
{:ok,
{:sigil_r, [delimiter: "/", line: 1],
[{:<<>>, [line: 1], ["foo"]}, [105, {:__cursor__, [line: 1, column: 9], []}]]}}
If the cursor is after the sigil, then it is discarded as everything else:
iex> Code.Fragment.container_cursor_to_quoted("~r/foo/i ", preserve_sigils: true)
{:ok, {:__cursor__, [line: 1], []}}
""" """
@doc since: "1.13.0" @doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) :: @spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}} {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
{preserve_sigils?, opts} = Keyword.pop(opts, :preserve_sigils, false)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder]) opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, preserve_sigils?, []}] ++ opts opts = [cursor_completion: true, emit_warnings: false] ++ opts
file = Keyword.get(opts, :file, "nofile") file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1) line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1) column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms}
end
{:ok, line, column, _warnings, rev_tokens, rev_terminators} -> {:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens = tokens = :lists.reverse(rev_tokens, rev_terminators)
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, false, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens. case :elixir.tokens_to_quoted(tokens, file, opts) do
# Otherwise we want to drop any newline so we drop the next tokens after eol. {:ok, ast} ->
trailing_tokens = {:ok, ast}
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0)) {:error, error} ->
else # In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators) tokens =
end :lists.reverse(
rev_tokens,
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
Enum.map(rev_terminators, fn tuple ->
{line, column, info} = elem(tuple, 1)
put_elem(tuple, 1, {line, column + 5, info})
end)
)
with {:ok, forms, _warnings} <- :elixir.tokens_to_quoted(tokens, file, opts) do case :elixir.tokens_to_quoted(tokens, file, opts) do
{:ok, forms} {:ok, ast} -> {:ok, ast}
{:error, _} -> {:error, error}
end
end end
{:error, info, _rest, _warnings, _so_far} -> {:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir_tokenizer.format_error(info)} {:error, :elixir.format_token_error(info)}
end end
end end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], _counter), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end end
+30 -66
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Identifier do defmodule Code.Identifier do
@moduledoc false @moduledoc false
@@ -65,7 +61,7 @@ defmodule Code.Identifier do
with "-" <> rest <- Atom.to_string(atom), with "-" <> rest <- Atom.to_string(atom),
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(), [trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
[arity, _inner, _count, ""] <- String.split(trailing, "-") do [arity, _inner, _count, ""] <- String.split(trailing, "-") do
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity} {reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
else else
_ -> :error _ -> :error
end end
@@ -75,11 +71,11 @@ defmodule Code.Identifier do
Escapes the given identifier. Escapes the given identifier.
""" """
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) :: @spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: binary(), remaining :: binary()} {escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1) def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do (is_integer(limit) and limit >= 0) do
escape(binary, char, limit, <<>>, fun) escape(binary, char, limit, [], fun)
end end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
@@ -87,96 +83,64 @@ defmodule Code.Identifier do
end end
defp escape(<<char, t::binary>>, char, count, acc, fun) do defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun) escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun) escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
end end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
if value = fun.(h) do escaped = if value = fun.(h), do: value, else: escape_char(h)
value = IO.iodata_to_binary(value) escape(t, char, decrement(count), [acc | escaped], fun)
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
else
escape(t, char, decrement(count), escape_char(h, acc), fun)
end
end end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun) escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
end end
defp escape(<<>>, _char, _count, acc, _fun) do defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>} {acc, <<>>}
end end
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>> defp escape_char(0), do: [?\\, ?0]
defp escape_char(char, acc) @escaped_bom :binary.bin_to_list("\\uFEFF")
# Some characters that are confusing (zero-width / alternative spaces) are displayed defp escape_char(65279), do: @escaped_bom
# using their unicode representation:
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
# BOM defp escape_char(char)
when char == 0xFEFF
# Mathematical invisibles
when char in 0x2061..0x2064
# Bidirectional neutral
when char in [0x061C, 0x200E, 0x200F]
# Bidirectional general (source of vulnerabilities)
when char in 0x202A..0x202E
when char in 0x2066..0x2069
# Interlinear annotations
when char in 0xFFF9..0xFFFC
# Zero-width joiners and non-joiners
when char in [0x200C, 0x200D, 0x034F]
# Non-break space / zero-width space
when char in [0x00A0, 0x200B, 0x2060]
# Line/paragraph separators
when char in [0x2028, 0x2029]
# Spaces
when char in 0x2000..0x200A
when char == 0x205F do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
end
defp escape_char(char, acc)
when char in 0x20..0x7E when char in 0x20..0x7E
when char in 0xA0..0xD7FF when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do when char in 0x10000..0x10FFFF do
<<acc::binary, char::utf8>> <<char::utf8>>
end end
defp escape_char(char, acc) when char < 0x80 do defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>> <<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>> [?\\, ?x, to_hex(a), to_hex(b)]
end end
defp escape_char(char, acc) when char < 0x10000 do defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>> <<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>> [?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end end
defp escape_char(char, acc) when char < 0x1000000 do defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>> <<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
<<acc::binary, ?\\, ?u, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
end end
defp escape_map(?\a), do: "\\a" defp escape_map(?\a), do: [?\\, ?a]
defp escape_map(?\b), do: "\\b" defp escape_map(?\b), do: [?\\, ?b]
defp escape_map(?\d), do: "\\d" defp escape_map(?\d), do: [?\\, ?d]
defp escape_map(?\e), do: "\\e" defp escape_map(?\e), do: [?\\, ?e]
defp escape_map(?\f), do: "\\f" defp escape_map(?\f), do: [?\\, ?f]
defp escape_map(?\n), do: "\\n" defp escape_map(?\n), do: [?\\, ?n]
defp escape_map(?\r), do: "\\r" defp escape_map(?\r), do: [?\\, ?r]
defp escape_map(?\t), do: "\\t" defp escape_map(?\t), do: [?\\, ?t]
defp escape_map(?\v), do: "\\v" defp escape_map(?\v), do: [?\\, ?v]
defp escape_map(?\\), do: "\\\\" defp escape_map(?\\), do: [?\\, ?\\]
defp escape_map(_), do: false defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1} @compile {:inline, to_hex: 1, decrement: 1}
+34 -73
View File
@@ -1,11 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
defmodule Code.Normalizer do defmodule Code.Normalizer do
@moduledoc false @moduledoc false
@do_end_keywords [:rescue, :catch, :else, :after]
defguard is_literal(x) defguard is_literal(x)
when is_integer(x) or when is_integer(x) or
is_float(x) or is_float(x) or
@@ -16,7 +11,6 @@ defmodule Code.Normalizer do
Wraps literals in the quoted expression to conform to the AST format expected Wraps literals in the quoted expression to conform to the AST format expected
by the formatter. by the formatter.
""" """
@spec normalize(Macro.t(), keyword()) :: Macro.t()
def normalize(quoted, opts \\ []) do def normalize(quoted, opts \\ []) do
line = Keyword.get(opts, :line, nil) line = Keyword.get(opts, :line, nil)
escape = Keyword.get(opts, :escape, true) escape = Keyword.get(opts, :escape, true)
@@ -64,13 +58,13 @@ defmodule Code.Normalizer do
{:.., meta, [left, right]} {:.., meta, [left, right]}
else else
step = do_normalize(step, state) step = do_normalize(step, state)
{:..//, meta, [left, right, step]} {:"..//", meta, [left, right, step]}
end end
end end
# Bit containers # Bit containers
defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do defp do_normalize({:<<>>, _, args} = quoted, state) when is_list(args) do
normalize_bitstring(quoted, state, state.escape) normalize_bitstring(quoted, state)
end end
# Atoms with interpolations # Atoms with interpolations
@@ -91,7 +85,13 @@ defmodule Code.Normalizer do
normalize_literal(:utf8, [], state) normalize_literal(:utf8, [], state)
end end
string = normalize_bitstring(string, state, state.escape) string =
if state.escape do
normalize_bitstring(string, state, true)
else
normalize_bitstring(string, state)
end
{{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]} {{:., dot_meta, [:erlang, :binary_to_atom]}, call_meta, [string, utf8]}
end end
@@ -114,7 +114,6 @@ defmodule Code.Normalizer do
end end
end) end)
parts = maybe_add_trailing_newline(call_meta, parts, state)
{{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} {{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]}
else else
normalize_call(quoted, state) normalize_call(quoted, state)
@@ -127,13 +126,13 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]} {:., meta, [Access, :get]}
end end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so # The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped. However, it should be escaped if applicable. # it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta) meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta}) left = do_normalize(left, %{state | parent_meta: meta})
right = maybe_escape_literal(right, state)
{:., meta, [left, right]} {:., meta, [left, right]}
end end
@@ -184,8 +183,7 @@ defmodule Code.Normalizer do
|> patch_meta_line(state.parent_meta) |> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"") |> Keyword.put_new(:delimiter, "\"")
string = normalize_bitstring(string, %{state | parent_meta: meta}, false) {sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [string, modifiers]}
else else
_ -> _ ->
normalize_call(quoted, state) normalize_call(quoted, state)
@@ -267,7 +265,7 @@ defmodule Code.Normalizer do
"Elixir." <> segments -> "Elixir." <> segments ->
segments segments
|> String.split(".") |> String.split(".")
|> Enum.map(&String.to_unsafe_atom/1) |> Enum.map(&String.to_atom/1)
end end
{:__aliases__, meta, segments} {:__aliases__, meta, segments}
@@ -291,18 +289,21 @@ defmodule Code.Normalizer do
# Lists # Lists
defp normalize_literal(list, meta, state) when is_list(list) do defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do if list != [] and List.ascii_printable?(list) do
# It's a charlist, we normalize it as a ~C sigil # It's a charlist
string = list =
if state.escape do if state.escape do
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil) {string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist) IO.iodata_to_binary(string) |> to_charlist()
else else
List.to_string(list) list
end end
meta = patch_meta_line([delimiter: "\""], state.parent_meta) meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:sigil_c, meta, [{:<<>>, [], [string]}, []]} {:__block__, meta, [list]}
else else
meta = meta =
if line = state.parent_meta[:line] do if line = state.parent_meta[:line] do
@@ -349,39 +350,28 @@ defmodule Code.Normalizer do
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args} {form, meta, args}
Keyword.has_key?(meta, :do) and kw_blocks?(last) -> Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
# def foo do :ok end # def foo do :ok end
# def foo, do: :ok # def foo, do: :ok
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and kw_blocks?(last) -> match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
# Non normalized kw blocks # Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line] line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]] meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state) normalize_kw_blocks(form, meta, args, state)
true -> true ->
# The formatter renders do-end blocks from the meta alone
meta = Keyword.drop(meta, [:do, :end])
args = normalize_args(args, %{state | parent_meta: meta}) args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, []) {last_arg, leading_args} = List.pop_at(args, -1, [])
last_args = last_args =
case last_arg do case last_arg do
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} -> {:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
cond do if key_meta[:format] == :keyword do
key_meta[:format] == :keyword -> last_args
[keyword] else
[last_arg]
block_keyword?(keyword) ->
[
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
{{:__block__, [format: :keyword] ++ meta, args}, value}
end)
]
true ->
[last_arg]
end end
[] -> [] ->
@@ -395,28 +385,11 @@ defmodule Code.Normalizer do
end end
end end
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
do: block_keyword?(tail)
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false
# Anything after the do block that is not a block keyword makes it a keyword list
defp kw_blocks?([{:do, _} | rest] = kw) do
Keyword.keyword?(kw) and Enum.all?(rest, &match?({key, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?([{{:__block__, _, [:do]}, _} | rest]) do
Enum.all?(rest, &match?({{:__block__, _, [key]}, _} when key in @do_end_keywords, &1))
end
defp kw_blocks?(_), do: false
defp allow_keyword?(:when, 2), do: true defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity) defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation) do defp normalize_bitstring({:<<>>, meta, parts}, state, escape_interpolation \\ false) do
meta = patch_meta_line(meta, state.parent_meta) meta = patch_meta_line(meta, state.parent_meta)
parts = parts =
@@ -435,21 +408,9 @@ defmodule Code.Normalizer do
end) end)
end end
parts = maybe_add_trailing_newline(meta, parts, state)
{:<<>>, meta, parts} {:<<>>, meta, parts}
end end
defp maybe_add_trailing_newline(meta, parts, state) do
with true <- state.escape and Keyword.get(meta, :delimiter) in ["\"\"\"", "'''"],
last = List.last(parts),
true <- is_binary(last) and not String.ends_with?(last, "\n") do
[_last | rest] = Enum.reverse(parts)
Enum.reverse([last <> "\n" | rest])
else
_ -> parts
end
end
defp normalize_interpolation_parts(parts, state, escape_interpolation) do defp normalize_interpolation_parts(parts, state, escape_interpolation) do
Enum.map(parts, fn Enum.map(parts, fn
{:"::", interpolation_meta, {:"::", interpolation_meta,
@@ -575,7 +536,7 @@ defmodule Code.Normalizer do
atom atom
|> Atom.to_string() |> Atom.to_string()
|> maybe_escape_literal(state) |> maybe_escape_literal(state)
|> String.to_unsafe_atom() |> String.to_atom()
end end
defp maybe_escape_literal(term, _) do defp maybe_escape_literal(term, _) do
+20 -20
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Code.Typespec do defmodule Code.Typespec do
@moduledoc false @moduledoc false
@@ -31,6 +27,11 @@ defmodule Code.Typespec do
end end
end end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno) meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type {:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -80,8 +81,7 @@ defmodule Code.Typespec do
Returns all types available from the module's BEAM code. Returns all types available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type`, `:opaque` and, on Erlang/OTP 28+, element is the type (`:typep`, `:type` and `:opaque`).
`:nominal`).
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The types will be in the Erlang located by the runtime system. The types will be in the Erlang
@@ -96,10 +96,9 @@ defmodule Code.Typespec do
types = types =
for {:attribute, _, kind, {name, _, args} = type} <- abstract_code, for {:attribute, _, kind, {name, _, args} = type} <- abstract_code,
kind in [:opaque, :type, :nominal] do kind in [:opaque, :type] do
cond do cond do
kind == :opaque -> {:opaque, type} kind == :opaque -> {:opaque, type}
kind == :nominal -> {:nominal, type}
{name, length(args)} in exported_types -> {:type, type} {name, length(args)} in exported_types -> {:type, type}
true -> {:typep, type} true -> {:typep, type}
end end
@@ -119,7 +118,7 @@ defmodule Code.Typespec do
element is spec name and arity and the second is the spec. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be The module must have a corresponding BEAM file which can be
located by the runtime system. The specs will be in the Erlang located by the runtime system. The types will be in the Erlang
Abstract Format. Abstract Format.
""" """
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error @spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
@@ -137,10 +136,10 @@ defmodule Code.Typespec do
Returns all callbacks available from the module's BEAM code. Returns all callbacks available from the module's BEAM code.
The result is returned as a list of tuples where the first The result is returned as a list of tuples where the first
element is the callback name and arity and the second is the callback. element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file The module must have a corresponding BEAM file
which can be located by the runtime system. The callbacks will be which can be located by the runtime system. The types will be
in the Erlang Abstract Format. in the Erlang Abstract Format.
""" """
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error @spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
@@ -193,8 +192,8 @@ defmodule Code.Typespec do
## To AST conversion ## To AST conversion
defp collect_vars({:ann_type, _anno, [_var, type]}) do defp collect_vars({:ann_type, _anno, args}) when is_list(args) do
collect_vars(type) []
end end
defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do defp collect_vars({:type, _anno, _kind, args}) when is_list(args) do
@@ -289,6 +288,7 @@ defmodule Code.Typespec do
end end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line] line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -317,6 +317,10 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}] [{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]} {:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end end
@@ -334,14 +338,10 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil} {erl_to_ex_var(var), meta(anno), nil}
end end
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]} {op, meta(anno), [typespec_to_quoted(arg)]}
end end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args) remote_type(anno, mod, name, args)
end end
@@ -401,10 +401,10 @@ defmodule Code.Typespec do
defp erl_to_ex_var(var) do defp erl_to_ex_var(var) do
case Atom.to_string(var) do case Atom.to_string(var) do
<<"_", c::utf8, rest::binary>> -> <<"_", c::utf8, rest::binary>> ->
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
<<c::utf8, rest::binary>> -> <<c::utf8, rest::binary>> ->
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}") String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
end end
end end
-20
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defprotocol Collectable do defprotocol Collectable do
@moduledoc """ @moduledoc """
A protocol to traverse data structures. A protocol to traverse data structures.
@@ -69,22 +65,6 @@ defprotocol Collectable do
iex> Enum.into([1, 2, 3], MapSet.new()) iex> Enum.into([1, 2, 3], MapSet.new())
MapSet.new([1, 2, 3]) MapSet.new([1, 2, 3])
## Halting
The `:halt` flag will be given whenever the collection won't
terminate correctly and must be used to clean up existing resources
(such as sockets, file handles, etc).
Note it is not guaranteed that the accumulator given to halt will
be the latest version of the accumulator returned by a previous call
with `{:cont, elem}`. Therefore, you must track the collected results
within the resource you intend to halt.
This is by design: ensuring halt is always called with the latest
accumulator would make pure collectables (the ones that do not implement
halt) expensive. However, given the collectables that must implement halt
already need to track state, the burden of tracking the accumulator
across invocations is put on them.
""" """
@type command :: {:cont, term} | :done | :halt @type command :: {:cont, term} | :done | :halt
+15 -60
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config do defmodule Config do
@moduledoc ~S""" @moduledoc ~S"""
A simple keyword-based configuration API. A simple keyword-based configuration API.
@@ -45,8 +41,7 @@ defmodule Config do
> application environment is effectively a global storage. Also note that > application environment is effectively a global storage. Also note that
> the `config/config.exs` of a library is not evaluated when the library is > 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 > used as a dependency, as configuration is always meant to configure the
> current project. For more information, see ["Using application configuration for > current project. For more information, read our [library guidelines](library-guidelines.md).
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
## Migrating from `use Mix.Config` ## Migrating from `use Mix.Config`
@@ -98,12 +93,6 @@ defmodule Config do
(assembled with `mix release`). (assembled with `mix release`).
""" """
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@opts_key {__MODULE__, :opts} @opts_key {__MODULE__, :opts}
@config_key {__MODULE__, :config} @config_key {__MODULE__, :config}
@imports_key {__MODULE__, :imports} @imports_key {__MODULE__, :imports}
@@ -141,6 +130,7 @@ defmodule Config do
config :logger, config :logger,
level: :warn, level: :warn,
backends: [:console]
config :logger, config :logger,
level: :info, level: :info,
@@ -148,13 +138,12 @@ defmodule Config do
will have a final configuration for `:logger` of: will have a final configuration for `:logger` of:
[level: :info, truncate: 1024] [level: :info, backends: [:console], truncate: 1024]
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), keyword()) :: keyword()
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}" raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end end
@@ -199,45 +188,17 @@ defmodule Config do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec config(atom(), atom(), term()) :: keyword()
def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do def config(root_key, key, opts) when is_atom(root_key) and is_atom(key) do
get_config!() get_config!()
|> __merge__([{root_key, [{key, opts}]}]) |> __merge__([{root_key, [{key, opts}]}])
|> put_config() |> put_config()
end end
@doc """
Reads the configuration for the given root key.
This function only reads the configuration from a previous
`config/2` or `config/3` call. If `root_key` points to an
application, it does not read its actual application environment.
Its main use case is to make it easier to access and share
configuration values across files.
If the `root_key` was not configured, it returns `nil`.
## Examples
# In config/config.exs
config :my_app, foo: :bar
# In config/dev.exs
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
"""
@doc since: "1.18.0"
@spec read_config(atom()) :: keyword() | nil
def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key]
end
@doc """ @doc """
Returns the environment this configuration file is executed on. Returns the environment this configuration file is executed on.
In Mix projects this function returns the environment this configuration In Mix projects this function returns the environment this configuration
file is executed on. file is executed on. In releases, the environment when `mix release` ran.
In releases, returns the `MIX_ENV` specified when running `mix release`.
This is most often used to execute conditional code: This is most often used to execute conditional code:
@@ -287,8 +248,8 @@ defmodule Config do
In case the file doesn't exist, an error is raised. In case the file doesn't exist, an error is raised.
If the file is relative, it will be expanded relative to the If file is a relative, it will be expanded relatively to the
directory of the current configuration file. directory the current configuration file is in.
## Examples ## Examples
@@ -315,7 +276,7 @@ defmodule Config do
end end
@doc false @doc false
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled} @spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
env = Keyword.get(opts, :env) env = Keyword.get(opts, :env)
target = Keyword.get(opts, :target) target = Keyword.get(opts, :target)
@@ -380,27 +341,21 @@ defmodule Config do
end end
end end
defp validate!(config, file) when is_list(config) do defp validate!(config, file) do
Enum.each(config, fn Enum.all?(config, fn
{app, value} when is_atom(app) -> {app, value} when is_atom(app) ->
if not Keyword.keyword?(value) do if Keyword.keyword?(value) do
true
else
raise ArgumentError, raise ArgumentError,
"expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <> "expected config for app #{inspect(app)} in #{Path.relative_to_cwd(file)} " <>
"to return keyword list, got: #{inspect(value)}" "to return keyword list, got: #{inspect(value)}"
end end
other -> _ ->
raise ArgumentError, false
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got entry: #{inspect(other)}"
end) end)
config config
end end
defp validate!(config, file) do
raise ArgumentError,
"expected config in #{Path.relative_to_cwd(file)} to be a keyword list " <>
"of {atom, keyword} pairs, got: #{inspect(config)}"
end
end end
+1 -16
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config.Provider do defmodule Config.Provider do
@moduledoc """ @moduledoc """
Specifies a provider API that loads configuration during boot. Specifies a provider API that loads configuration during boot.
@@ -25,7 +21,7 @@ defmodule Config.Provider do
For example, imagine you want to list some basic configuration For example, imagine you want to list some basic configuration
on Mix's built-in `config/runtime.exs` file, but you also want on Mix's built-in `config/runtime.exs` file, but you also want
to support additional configuration files. To do so, you can add to support additional configuration files. To do so, you can add
this inside the `def project` portion of your `mix.exs`: this inside the `def project` portion of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
@@ -111,16 +107,6 @@ defmodule Config.Provider do
""" """
@type config_path :: {:system, binary(), binary()} | binary() @type config_path :: {:system, binary(), binary()} | binary()
@typedoc """
Options for `init/3`.
"""
@type init_opts :: [
extra_config: config(),
prune_runtime_sys_config_after_boot: boolean(),
reboot_system_after_config: boolean(),
validate_compile_env: [{atom(), [atom()], term()}]
]
@doc """ @doc """
Invoked when initializing a config provider. Invoked when initializing a config provider.
@@ -206,7 +192,6 @@ defmodule Config.Provider do
@reboot_mode_key :config_provider_reboot_mode @reboot_mode_key :config_provider_reboot_mode
@doc false @doc false
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
validate_config_path!(config_path) validate_config_path!(config_path)
providers = for {provider, init} <- providers, do: {provider, provider.init(init)} providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
+4 -14
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Config.Reader do defmodule Config.Reader do
@moduledoc """ @moduledoc """
API for reading config files defined with `Config`. API for reading config files defined with `Config`.
@@ -16,7 +12,7 @@ defmodule Config.Reader do
For example, if you expect the target system to have a config file For example, if you expect the target system to have a config file
in an absolute path, you can add this inside the `def project` portion in an absolute path, you can add this inside the `def project` portion
of your `mix.exs`: of your `mix.exs`:
releases: [ releases: [
demo: [ demo: [
@@ -46,12 +42,6 @@ defmodule Config.Reader do
@behaviour Config.Provider @behaviour Config.Provider
@type config_opts :: [
imports: [Path.t()] | :disabled,
env: atom(),
target: atom()
]
@impl true @impl true
def init(opts) when is_list(opts) do def init(opts) when is_list(opts) do
{path, opts} = Keyword.pop!(opts, :path) {path, opts} = Keyword.pop!(opts, :path)
@@ -74,7 +64,7 @@ defmodule Config.Reader do
Accepts the same options as `read!/2`. Accepts the same options as `read!/2`.
""" """
@doc since: "1.11.0" @doc since: "1.11.0"
@spec eval!(Path.t(), binary, config_opts) :: keyword @spec eval!(Path.t(), binary, keyword) :: keyword
def eval!(file, contents, opts \\ []) def eval!(file, contents, opts \\ [])
when is_binary(file) and is_binary(contents) and is_list(opts) do when is_binary(file) and is_binary(contents) and is_list(opts) do
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0) Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
@@ -96,7 +86,7 @@ defmodule Config.Reader do
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec read!(Path.t(), config_opts) :: keyword @spec read!(Path.t(), keyword) :: keyword
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
file = Path.expand(file) file = Path.expand(file)
Config.__eval__!(file, File.read!(file), opts) |> elem(0) Config.__eval__!(file, File.read!(file), opts) |> elem(0)
@@ -110,7 +100,7 @@ defmodule Config.Reader do
option cannot be disabled in `read_imports!/2`. option cannot be disabled in `read_imports!/2`.
""" """
@doc since: "1.9.0" @doc since: "1.9.0"
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]} @spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
if opts[:imports] == :disabled do if opts[:imports] == :disabled do
raise ArgumentError, ":imports must be a list of paths" raise ArgumentError, ":imports must be a list of paths"
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Dict do defmodule Dict do
@moduledoc ~S""" @moduledoc ~S"""
Generic API for dictionaries. Generic API for dictionaries.
+77 -48
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule DynamicSupervisor do defmodule DynamicSupervisor do
@moduledoc ~S""" @moduledoc ~S"""
A supervisor optimized to only start children dynamically. A supervisor optimized to only start children dynamically.
@@ -16,7 +12,7 @@ defmodule DynamicSupervisor do
## Examples ## Examples
A dynamic supervisor is started with no children and often with a name: A dynamic supervisor is started with no children and often a name:
children = [ children = [
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one} {DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
@@ -137,6 +133,67 @@ defmodule DynamicSupervisor do
A supervisor is bound to the same name registration rules as a `GenServer`. A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`. Read more about these rules in the documentation for `GenServer`.
## Migrating from Supervisor's :simple_one_for_one
In case you were using the deprecated `:simple_one_for_one` strategy from
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
few steps.
Imagine the given "old" code:
defmodule MySupervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
Supervisor.start_child(__MODULE__, [foo, bar, baz])
end
@impl true
def init(init_arg) do
children = [
# Or the deprecated: worker(MyWorker, [init_arg])
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
]
Supervisor.init(children, strategy: :simple_one_for_one)
end
end
It can be upgraded to the DynamicSupervisor like this:
defmodule MySupervisor do
use DynamicSupervisor
def start_link(init_arg) do
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
def start_child(foo, bar, baz) do
# If MyWorker is not using the new child specs, we need to pass a map:
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
DynamicSupervisor.start_child(__MODULE__, spec)
end
@impl true
def init(init_arg) do
DynamicSupervisor.init(
strategy: :one_for_one,
extra_arguments: [init_arg]
)
end
end
The difference is that the `DynamicSupervisor` expects the child specification
at the moment `start_child/2` is called, and no longer on the init callback.
If there are any initial arguments given on initialization, such as `[initial_arg]`,
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
""" """
@behaviour GenServer @behaviour GenServer
@@ -158,7 +215,10 @@ defmodule DynamicSupervisor do
extra_arguments: [term()] extra_arguments: [term()]
} }
@typedoc "Options given to `start_link/1` and `init/1` functions" @typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@type init_option :: @type init_option ::
{:strategy, strategy()} {:strategy, strategy()}
| {:max_restarts, non_neg_integer()} | {:max_restarts, non_neg_integer()}
@@ -169,15 +229,7 @@ defmodule DynamicSupervisor do
@typedoc "Supported strategies" @typedoc "Supported strategies"
@type strategy :: :one_for_one @type strategy :: :one_for_one
@typedoc """ @typedoc "Return values of `start_child` functions"
Return values of `start_child` functions.
Unlike `Supervisor`, this module ignores the child spec ids,
so `{:error, {:already_started, pid}}` is not returned for child specs
given with the same id. `{:error, {:already_started, pid}}` is returned
however if a duplicate name is used when using
[name registration](`m:GenServer#module-name-registration`).
"""
@type on_start_child :: @type on_start_child ::
{:ok, pid} {:ok, pid}
| {:ok, pid, info :: term} | {:ok, pid, info :: term}
@@ -206,7 +258,6 @@ defmodule DynamicSupervisor do
See `Supervisor` for more information about child specifications. See `Supervisor` for more information about child specifications.
""" """
@doc since: "1.6.1" @doc since: "1.6.1"
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
def child_spec(options) when is_list(options) do def child_spec(options) when is_list(options) do
id = id =
case Keyword.get(options, :name, DynamicSupervisor) do case Keyword.get(options, :name, DynamicSupervisor) do
@@ -226,7 +277,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor @behaviour DynamicSupervisor
if not Module.has_attribute?(__MODULE__, :doc) do unless Module.has_attribute?(__MODULE__, :doc) do
@doc """ @doc """
Returns a specification to start this module under a supervisor. Returns a specification to start this module under a supervisor.
@@ -294,11 +345,9 @@ defmodule DynamicSupervisor do
specified in the child spec given to `start_child/2`. Defaults to specified in the child spec given to `start_child/2`. Defaults to
an empty list. an empty list.
* Any of the standard [GenServer options](`t:GenServer.option/0`)
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start() @spec start_link([option | init_option]) :: Supervisor.on_start()
def start_link(options) when is_list(options) do def start_link(options) when is_list(options) do
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy] keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
{sup_opts, start_opts} = Keyword.split(options, keys) {sup_opts, start_opts} = Keyword.split(options, keys)
@@ -332,15 +381,9 @@ defmodule DynamicSupervisor do
Note that a supervisor started with this function is linked to the parent Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits process and exits not only on crashes but also if the parent process exits
with `:normal` reason. with `:normal` reason.
## Options
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
callback.
""" """
@doc since: "1.6.0" @doc since: "1.6.0"
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start() @spec start_link(module, term, [option]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts) GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end end
@@ -348,17 +391,11 @@ defmodule DynamicSupervisor do
@doc """ @doc """
Dynamically adds a child specification to `supervisor` and starts that child. Dynamically adds a child specification to `supervisor` and starts that child.
`child_spec` should be a valid [child specification](`m:Supervisor#module-child-specification`). `child_spec` should be a valid child specification as detailed in the
The child process will be started as defined in the child specification. Note that while "Child specification" section of the documentation for `Supervisor`. The child
process will be started as defined in the child specification. Note that while
the `:id` field is still required in the spec, the value is ignored and the `:id` field is still required in the spec, the value is ignored and
therefore does not need to be unique. Unlike `Supervisor`, this module does not therefore does not need to be unique.
return `{:error, {:already_started, pid}}` for child specs given with the same id.
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
used when using [name registration](`m:GenServer#module-name-registration`).
This function will block the `DynamicSupervisor` until the child initializes.
When starting too many processes dynamically, you may want to use a
`PartitionSupervisor` to split the work across multiple processes.
If the child process start function returns `{:ok, child}` or `{:ok, child, If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and info}`, then child specification and PID are added to the supervisor and
@@ -463,14 +500,6 @@ defmodule DynamicSupervisor do
@doc """ @doc """
Terminates the given child identified by `pid`. Terminates the given child identified by `pid`.
This function will block the `DynamicSupervisor` until the child
terminates, which may take an arbitrary amount of time if the child
is trapping exits and implements its own terminate callback.
For this reason, it is often better to ask the child process
itself to terminate, often by declaring in its child spec it has
a restart strategy of `:transient` (or `:temporary`) and then
sending it a message to stop with reason `:shutdown`.
If successful, this function returns `:ok`. If there is no process with If successful, this function returns `:ok`. If there is no process with
the given PID, this function returns `{:error, :not_found}`. the given PID, this function returns `{:error, :not_found}`.
""" """
@@ -481,11 +510,11 @@ defmodule DynamicSupervisor do
end end
@doc """ @doc """
Returns a list with information about all children of the given supervisor. Returns a list with information about all children.
Note that calling this function when supervising a large number Note that calling this function when supervising a large number
of children under low memory conditions can bring the system down due to an of children under low memory conditions can cause an out of memory
out of memory error. exception.
This function returns a list of tuples containing: This function returns a list of tuples containing:
+158 -545
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83 -597
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
require Record require Record
defmodule File.Stat do defmodule File.Stat do
+7 -24
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule File.Stream do defmodule File.Stream do
@moduledoc """ @moduledoc """
Defines a `File.Stream` struct returned by `File.stream!/3`. Defines a `File.Stream` struct returned by `File.stream!/3`.
@@ -18,13 +14,7 @@ defmodule File.Stream do
defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
@type t :: %__MODULE__{ @type t :: %__MODULE__{}
path: Path.t(),
modes: [term()],
line_or_bytes: :line | pos_integer(),
raw: boolean(),
node: node()
}
@doc false @doc false
def __build__(path, line_or_bytes, modes) do def __build__(path, line_or_bytes, modes) do
@@ -125,7 +115,7 @@ defmodule File.Stream do
counter = fn device -> counter = fn device ->
device = skip_bom_and_offset(device, raw, modes) device = skip_bom_and_offset(device, raw, modes)
count_lines(device, path, pattern, read_function(stream), 0, :empty) count_lines(device, path, pattern, read_function(stream), 0)
end end
{:ok, open!(stream, modes, counter)} {:ok, open!(stream, modes, counter)}
@@ -235,28 +225,21 @@ defmodule File.Stream do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end end
defp count_lines(device, path, pattern, read, count, last_byte) do defp count_lines(device, path, pattern, read, count) do
case read.(device) do case read.(device) do
data when is_binary(data) and byte_size(data) > 0 ->
newlines = length(:binary.matches(data, pattern))
last = :binary.last(data)
count_lines(device, path, pattern, read, count + newlines, last)
data when is_binary(data) -> data when is_binary(data) ->
count_lines(device, path, pattern, read, count, last_byte) count_lines(device, path, pattern, read, count + count_lines(data, pattern))
:eof -> :eof ->
case last_byte do count
:empty -> 0
?\n -> count
_ -> count + 1
end
{:error, reason} -> {:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path raise File.Error, reason: reason, action: "stream", path: path
end end
end end
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size) defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size) defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
end end
+144 -207
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
import Kernel, except: [round: 1] import Kernel, except: [round: 1]
defmodule Float do defmodule Float do
@@ -25,7 +21,7 @@ defmodule Float do
and arithmetic due to the fact most decimal fractions cannot be and arithmetic due to the fact most decimal fractions cannot be
represented by a floating-point binary and most operations are not exact, represented by a floating-point binary and most operations are not exact,
but operate on approximations. Those issues are not specific but operate on approximations. Those issues are not specific
to Elixir, they are a property of floating-point representation itself. to Elixir, they are a property of floating point representation itself.
For example, the numbers 0.1 and 0.01 are two of them, what means the result For example, the numbers 0.1 and 0.01 are two of them, what means the result
of squaring 0.1 does not give 0.01 neither the closest representable. Here is of squaring 0.1 does not give 0.01 neither the closest representable. Here is
@@ -42,7 +38,7 @@ defmodule Float do
To learn more about floating-point arithmetic visit: To learn more about floating-point arithmetic visit:
* [0.30000000000000004.com](https://0.30000000000000004.com/) * [0.30000000000000004.com](http://0.30000000000000004.com/)
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/) * [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
""" """
@@ -143,8 +139,6 @@ defmodule Float do
iex> Float.parse("56.5xyz") iex> Float.parse("56.5xyz")
{56.5, "xyz"} {56.5, "xyz"}
iex> Float.parse(".12")
:error
iex> Float.parse("pi") iex> Float.parse("pi")
:error :error
iex> Float.parse("1.7976931348623159e+308") iex> Float.parse("1.7976931348623159e+308")
@@ -167,73 +161,41 @@ defmodule Float do
parse_unsigned(binary) parse_unsigned(binary)
end end
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_mantissa(binary, rest, false) do: parse_unsigned(rest, false, false, <<digit>>)
defp parse_unsigned(binary) when is_binary(binary), do: :error defp parse_unsigned(binary) when is_binary(binary), do: :error
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9, defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, dot?) do: parse_unsigned(rest, dot?, e?, <<acc::binary, digit>>)
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9, defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_mantissa(binary, rest, true) do: parse_unsigned(rest, true, false, <<acc::binary, ?., digit>>)
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and digit in ?0..?9, when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, digit>>)
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?) defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9, when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?) do: parse_unsigned(rest, true, true, <<add_dot(acc, dot?)::binary, ?e, sign, digit>>)
defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?) # When floats are expressed in scientific notation, :erlang.binary_to_float/1 can raise an
# ArgumentError if the e exponent is too big. For example, "1.0e400". Because of this, we
defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9, # rescue the ArgumentError here and return an error.
do: parse_exponent(binary, exp_pos, rest, dot?) defp parse_unsigned(rest, dot?, true = _e?, acc) do
:erlang.binary_to_float(add_dot(acc, dot?))
defp parse_exponent(binary, exp_pos, rest, dot?),
do: finish_exponent(binary, exp_pos, rest, dot?)
defp finish_mantissa(binary, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue rescue
ArgumentError -> :error ArgumentError -> :error
else
float -> {float, rest}
end end
# Bare integer: * 1.0 casts to the nearest float without building a new binary, defp parse_unsigned(rest, dot?, false = _e?, acc),
# and raises ArithmeticError on overflow (for example a 400-digit integer). do: {:erlang.binary_to_float(add_dot(acc, dot?)), rest}
defp finish_mantissa(binary, rest, _dot? = false) do
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
rescue
ArithmeticError -> :error
end
# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400". defp add_dot(acc, true), do: acc
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do defp add_dot(acc, false), do: acc <> ".0"
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end
# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
# form a valid float literal.
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
len = byte_size(binary) - byte_size(rest)
literal =
IO.iodata_to_binary([
:binary.part(binary, 0, exp_pos),
".0",
:binary.part(binary, exp_pos, len - exp_pos)
])
{:erlang.binary_to_float(literal), rest}
rescue
ArgumentError -> :error
end
defp consumed(binary, ""), do: binary
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))
@doc """ @doc """
Rounds a float to the largest float less than or equal to `number`. Rounds a float to the largest float less than or equal to `number`.
@@ -286,7 +248,7 @@ defmodule Float do
@doc """ @doc """
Rounds a float to the smallest float greater than or equal to `number`. Rounds a float to the smallest float greater than or equal to `number`.
`ceil/2` also accepts a precision to round a floating-point value up `ceil/2` also accepts a precision to round a floating-point value down
to an arbitrary number of fractional digits (between 0 and 15). to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a The operation is performed on the binary floating point, without a
@@ -355,7 +317,7 @@ defmodule Float do
and therefore the number above is internally represented as 5.567499999, and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals, which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance you must use a decimal library. The behavior above is also in accordance
with reference implementations, such as "Correctly Rounded Binary-Decimal and to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay. Decimal-Binary Conversions" by David M. Gay.
## Examples ## Examples
@@ -377,12 +339,15 @@ defmodule Float do
""" """
@spec round(float, precision_range) :: float @spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0) def round(float, precision \\ 0)
def round(float, 0) when float === 0.0 or float === -0.0, do: float def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do def round(float, 0) when is_float(float) do
case :erlang.round(float) * 1.0 do case float |> :erlang.round() |> :erlang.float() do
zero when zero == 0.0 and float < 0.0 -> -0.0 zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded rounded -> rounded
end end
@@ -396,170 +361,140 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision) raise ArgumentError, invalid_precision_message(precision)
end end
# Decimal-place rounding via exact rational scaling. This is the bignum
# core used by reference implementations like David M. Gay's "Correctly
# Rounded Binary-Decimal and Decimal-Binary Conversions" (cited in the
# @doc above), Python's round(), and Java's BigDecimal.setScale.
#
# 1. Decompose float exactly: |float| = mantissa / 2^shift.
# 2. Scale exactly: |float| * 10^precision = mantissa * 10^precision / 2^shift.
# Because precision is bounded to 0..15, the product fits in ~103 bits
# (53-bit mantissa + ~50-bit power of ten) and BEAM bignums handle it
# directly without approximation.
# 3. Round the exact rational to an integer per the requested mode
# (half_up / floor / ceil) using quotient and remainder.
# 4. Emit the float closest to rounded_int / 10^precision:
# - fast path: when rounded_int < 2^53, both operands are exactly
# representable as floats and IEEE division is correctly rounded.
# - slow path: bignum alignment + manual mantissa extraction with
# round-to-nearest-even for the trailing bit.
#
# The integer-rounding decision (step 3) and the binary-emission decision
# (step 4) are deliberately independent: step 3 picks the exact rational
# the user asked for, step 4 picks the closest float to that rational.
# Conflating them is the classic source of double-rounding bugs.
#
# Faster algorithms exist (Cox 2026's table-based uscale; Ryū / Schubfach
# for round-trip printing) but target different problems or assume
# fixed-width machine arithmetic that BEAM doesn't expose efficiently.
# At precision <= 15, the exact path is small, easy to audit, and fast
# enough that a more complex algorithm has not been justified by benchmarks.
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
defp round(float, precision, mode) do defp round(float, precision, rounding) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>> <<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count} = decompose(significant, 1)
count = count - exp + 1023
cond do cond do
# Subnormal — tiny but non-zero; treat per-mode (ceil(+) and floor(-) bump # Precision beyond 15 digits
# to 10^-precision; everything else rounds to signed zero). count >= 104 ->
exp == 0 -> case rounding do
tiny_round(sign, precision, mode) :ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end
# |float| >= 2^52 — has no fractional bits, return unchanged. # We are asking more precision than we have
exp - 1075 >= 0 -> count <= precision ->
float float
true -> true ->
mantissa = @power_of_2_to_52 ||| mantissa # Difference in precision between float and asked precision
shift = 1075 - exp # We subtract 1 because we need to calculate the remainder too
do_round(sign, mantissa, shift, precision, mode) diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# https://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 and sign == 1 ->
minus_zero()
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
end end
end end
# |float * 10^precision| < 0.5 — integer round is 0; ceil/floor still bump per sign. # TODO remove once we require Erlang/OTP 27+
defp do_round(sign, _mantissa, shift, precision, mode) when shift >= 104 do # This function tricks the compiler to avoid this bug in previous versions:
tiny_round(sign, precision, mode) # https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
end end
defp do_round(sign, mantissa, shift, precision, mode) do defp decompose(<<1::1, bits::bitstring>>, count, last_count, acc) do
power = power_of_10(precision) decompose(bits, count + 1, count, (acc <<< (count - last_count)) + 1)
product = mantissa * power end
half = 1 <<< (shift - 1)
quotient = product >>> shift
remainder = product - (quotient <<< shift)
rounded_int = round_step(mode, sign, quotient, remainder, half)
cond do defp decompose(<<0::1, bits::bitstring>>, count, last_count, acc) do
rounded_int == 0 -> decompose(bits, count + 1, last_count, acc)
signed_zero(sign) end
rounded_int < @power_of_2_to_52 <<< 1 -> defp decompose(<<>>, _count, last_count, acc) do
# Both rounded_int and power fit in 53 bits, so IEEE float division {acc, last_count}
# is correctly rounded. end
result = rounded_int / power
if sign == 1, do: -result, else: result
true -> defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
bignum_to_float(sign, rounded_int, power) defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
end end
end end
defp round_step(:half_up, _sign, quotient, remainder, half) do defp decimal_to_float(sign, num, den, exp) do
if remainder >= half, do: quotient + 1, else: quotient quo = div(num, den)
end rem = num - quo * den
defp round_step(:floor, 0, quotient, _remainder, _half), do: quotient tmp =
defp round_step(:floor, 1, quotient, remainder, _half) when remainder > 0, do: quotient + 1 case den >>> 1 do
defp round_step(:floor, 1, quotient, _remainder, _half), do: quotient den when rem > den -> quo + 1
den when rem < den -> quo
defp round_step(:ceil, 0, quotient, remainder, _half) when remainder > 0, do: quotient + 1 _ when (quo &&& 1) === 1 -> quo + 1
defp round_step(:ceil, 0, quotient, _remainder, _half), do: quotient _ -> quo
defp round_step(:ceil, 1, quotient, _remainder, _half), do: quotient
defp signed_zero(0), do: 0.0
defp signed_zero(1), do: -0.0
# Result of rounding a non-zero float whose |float * 10^precision| < 0.5.
# ceil(+) → +10^-precision, floor(-) → -10^-precision, others → signed 0.
defp tiny_round(0, precision, :ceil), do: 1.0 / power_of_10(precision)
defp tiny_round(1, precision, :floor), do: -1.0 / power_of_10(precision)
defp tiny_round(sign, _precision, _mode), do: signed_zero(sign)
# Slow path: emit float closest to `sign * rounded_int / power` when
# rounded_int >= 2^53. The binary emission step is always IEEE
# round-to-nearest-even, regardless of the integer-rounding mode.
defp bignum_to_float(sign, rounded_int, power) do
shift_adjust = bit_length(rounded_int) - bit_length(power) - 53
{numerator, denominator, exp} = align(rounded_int, power, shift_adjust)
quotient = div(numerator, denominator)
remainder = numerator - quotient * denominator
half = denominator >>> 1
mantissa =
cond do
remainder > half -> quotient + 1
remainder < half -> quotient
(quotient &&& 1) === 1 -> quotient + 1
true -> quotient
end end
# Carry-bit normalization: `mantissa` lives in [2^52, 2^53]. The upper tmp = tmp - @power_of_2_to_52
# bound `2^53` is reachable when `align/3` returns an upper-bound quotient <<tmp::float>> = <<sign::1, exp + 1023::11, tmp::52>>
# or when rounding carries. Rebalance into the canonical [2^52, 2^53) tmp
# range so the 52-bit packing below doesn't silently truncate.
{mantissa, exp} =
if mantissa == @power_of_2_to_52 <<< 1,
do: {@power_of_2_to_52, exp + 1},
else: {mantissa, exp}
<<result::float>> = <<sign::1, exp + 1023::11, mantissa - @power_of_2_to_52::52>>
result
end end
# Pick (numerator, denominator, exp) so that numerator/denominator ∈ [2^52, 2^53) defp rounding(:floor, 1, _num, div), do: div + 1
# and the resulting float = numerator/denominator * 2^(exp-52). defp rounding(:ceil, 0, _num, div), do: div + 1
defp align(rounded_int, power, shift_adjust) when shift_adjust >= 0 do
new_power = power <<< shift_adjust
if rounded_int < new_power <<< 53, defp rounding(:half_up, _sign, num, div) do
do: {rounded_int, new_power, 52 + shift_adjust}, case rem(num, 10) do
else: {rounded_int, new_power <<< 1, 53 + shift_adjust} rem when rem < 5 -> div
end rem when rem >= 5 -> div + 1
defp align(rounded_int, power, shift_adjust) do
shifted = rounded_int <<< -shift_adjust
cond do
shifted >= power <<< 53 -> {shifted, power <<< 1, 53 + shift_adjust}
shifted >= power <<< 52 -> {shifted, power, 52 + shift_adjust}
true -> {shifted <<< 1, power, 51 + shift_adjust}
end end
end end
defp bit_length(0), do: 0 defp rounding(_, _, _, div), do: div
defp bit_length(integer) when integer > 0, do: bit_length(integer, 0)
defp bit_length(integer, acc) when integer >= 1 <<< 64, do: bit_length(integer >>> 64, acc + 64)
defp bit_length(integer, acc) when integer >= 1 <<< 16, do: bit_length(integer >>> 16, acc + 16)
defp bit_length(integer, acc) when integer >= 1 <<< 4, do: bit_length(integer >>> 4, acc + 4)
defp bit_length(integer, acc) when integer >= 1, do: bit_length(integer >>> 1, acc + 1)
defp bit_length(_integer, acc), do: acc
Enum.reduce(0..15, 1, fn exponent, acc -> Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_10(unquote(exponent)), do: unquote(acc) defp power_of_10(unquote(x)), do: unquote(acc)
acc * 10 acc * 10
end) end)
Enum.reduce(0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end)
@doc """ @doc """
Returns a pair of integers whose ratio is exactly equal Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator. to the original float and with a positive denominator.
@@ -640,14 +575,18 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation Returns a charlist which corresponds to the shortest text representation
of the given float. of the given float.
It uses the algorithm presented in "Ryū: fast float-to-string conversion" The underlying algorithm changes depending on the Erlang/OTP version:
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation. * For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`. For a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples ## Examples
iex> Float.to_charlist(7.0) iex> Float.to_charlist(7.0)
@@ -655,7 +594,7 @@ defmodule Float do
""" """
@spec to_charlist(float) :: charlist @spec to_charlist(float) :: charlist
def to_charlist(float) do def to_charlist(float) when is_float(float) do
:erlang.float_to_list(float, [:short]) :erlang.float_to_list(float, [:short])
end end
@@ -675,8 +614,6 @@ defmodule Float do
For a configurable representation, use `:erlang.float_to_binary/2`. For a configurable representation, use `:erlang.float_to_binary/2`.
Inlined by the compiler.
## Examples ## Examples
iex> Float.to_string(7.0) iex> Float.to_string(7.0)
@@ -684,7 +621,7 @@ defmodule Float do
""" """
@spec to_string(float) :: String.t() @spec to_string(float) :: String.t()
def to_string(float) do def to_string(float) when is_float(float) do
:erlang.float_to_binary(float, [:short]) :erlang.float_to_binary(float, [:short])
end end
@@ -705,7 +642,7 @@ defmodule Float do
end end
defp invalid_precision_message(precision) do defp invalid_precision_message(precision) do
"precision #{inspect(precision)} is out of valid range of #{inspect(@precision_range)}" "precision #{precision} is out of valid range of #{inspect(@precision_range)}"
end end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t) defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
+6 -5
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Function do defmodule Function do
@moduledoc """ @moduledoc """
A set of functions for working with functions. A set of functions for working with functions.
@@ -69,6 +65,7 @@ defmodule Function do
| :name | :name
| :new_index | :new_index
| :new_uniq | :new_uniq
| :pid
| :type | :type
| :uniq | :uniq
@@ -111,6 +108,8 @@ defmodule Function do
When `fun` is an anonymous function (that is, the type is `:local`), the following When `fun` is an anonymous function (that is, the type is `:local`), the following
additional keys are returned: additional keys are returned:
* `:pid` - PID of the process that originally created the function.
* `:index` - (integer) an index into the module function table. * `:index` - (integer) an index into the module function table.
* `:new_index` - (integer) an index into the module function table. * `:new_index` - (integer) an index into the module function table.
@@ -156,7 +155,7 @@ defmodule Function do
`:module`, `:name`, `:arity`, `:env`, or `:type`. `:module`, `:name`, `:arity`, `:env`, or `:type`.
For anonymous functions, there is also information about any of the For anonymous functions, there is also information about any of the
atoms `:index`, `:new_index`, `:new_uniq`, and `:uniq`. atoms `:index`, `:new_index`, `:new_uniq`, `:uniq`, and `:pid`.
For a named function, the value of any of these items is always the For a named function, the value of any of these items is always the
atom `:undefined`. atom `:undefined`.
@@ -176,6 +175,8 @@ defmodule Function do
iex> fun = &String.length/1 iex> fun = &String.length/1
iex> Function.info(fun, :name) iex> Function.info(fun, :name)
{:name, :length} {:name, :length}
iex> Function.info(fun, :pid)
{:pid, :undefined}
""" """
@doc since: "1.7.0" @doc since: "1.7.0"
+3 -7
View File
@@ -1,7 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule GenEvent do defmodule GenEvent do
# Functions from this module are deprecated in elixir_dispatch. # Functions from this module are deprecated in elixir_dispatch.
@@ -36,7 +32,7 @@ defmodule GenEvent do
alternative. GenStage is an external Elixir library maintained by the Elixir alternative. GenStage is an external Elixir library maintained by the Elixir
team; it provides a tool to implement systems that exchange events in a team; it provides a tool to implement systems that exchange events in a
demand-driven way with built-in support for back-pressure. See the [GenStage demand-driven way with built-in support for back-pressure. See the [GenStage
documentation](https://gen-stage.hexdocs.pm) for more information. documentation](https://hexdocs.pm/gen_stage) for more information.
### `:gen_event` ### `:gen_event`
@@ -50,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) :: @callback init(args :: term) ::
{:ok, state} {:ok, state}
| {:ok, state, :hibernate} | {:ok, state, :hibernate}
| {:error, reason :: term} | {:error, reason :: any}
when state: term when state: any
@callback handle_event(event :: term, state :: term) :: @callback handle_event(event :: term, state :: term) ::
{:ok, new_state} {:ok, new_state}

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