Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e6d67c169 | ||
|
|
614ae049e6 | ||
|
|
b1492cb0cf | ||
|
|
237a71311c | ||
|
|
e40b280fd8 | ||
|
|
308255bda8 | ||
|
|
1e81a5b9a5 | ||
|
|
0627d7b0d4 | ||
|
|
99ec7522d3 | ||
|
|
d30c5c0185 | ||
|
|
3fc334ebc3 | ||
|
|
df8955c0dd | ||
|
|
6b87a54825 | ||
|
|
72ec902554 | ||
|
|
2c9fff0f93 | ||
|
|
c42f7af02b | ||
|
|
9c77960a26 | ||
|
|
a20f413841 | ||
|
|
ffbae20014 | ||
|
|
9c81578ab5 | ||
|
|
a9f7b8e18c | ||
|
|
61c3c34f44 | ||
|
|
d8faf5bb7b | ||
|
|
9d03032cfd | ||
|
|
3c4c68b3d5 | ||
|
|
bed0a68176 | ||
|
|
60642c340d | ||
|
|
35e4263934 | ||
|
|
43327512da | ||
|
|
32692f14ff | ||
|
|
f80bd3c478 | ||
|
|
23934b48ba | ||
|
|
c370a8a724 | ||
|
|
8c2237f80d | ||
|
|
a5e9c39023 | ||
|
|
6d918f5fb4 | ||
|
|
4347468d8a | ||
|
|
f044def5a1 | ||
|
|
05c9ccad12 | ||
|
|
7533e56c1b | ||
|
|
3f083795b6 | ||
|
|
1bbc959d1a | ||
|
|
1603f21b0a | ||
|
|
02c3957667 | ||
|
|
eeab9f5aa1 | ||
|
|
3f3e987a23 | ||
|
|
53c8b27ee2 | ||
|
|
3550dd10af | ||
|
|
7e44b88122 | ||
|
|
5c5259826f | ||
|
|
ccdefd904f | ||
|
|
ff95a59ec0 | ||
|
|
4081590742 | ||
|
|
693df5fe8c | ||
|
|
b4605374b5 | ||
|
|
36ec08e0c8 | ||
|
|
ae0ac9b3c9 | ||
|
|
e3479831ba | ||
|
|
ffbcc004ec | ||
|
|
e0d61c7875 | ||
|
|
b2059031a6 | ||
|
|
ed2829d8ea | ||
|
|
081f99cd37 | ||
|
|
7837cc5c59 | ||
|
|
b96fc39422 | ||
|
|
e1cb898198 | ||
|
|
88e2894925 | ||
|
|
f7cb5ae053 | ||
|
|
b4845e6854 | ||
|
|
0f1200f99b | ||
|
|
cec1a7190b | ||
|
|
53d1b98a05 | ||
|
|
4828f11cd2 | ||
|
|
501271cad0 | ||
|
|
fd4a6ad5cf | ||
|
|
196aab87ca | ||
|
|
59b266506a | ||
|
|
1c8e625d77 | ||
|
|
f9d7adbfdf | ||
|
|
f1721c8a58 | ||
|
|
57abca4c16 | ||
|
|
8e9ece0d62 | ||
|
|
54ba096d34 | ||
|
|
6042fd0bcc | ||
|
|
475b73b46f | ||
|
|
3cf6069ad9 | ||
|
|
48cee5aaa5 |
@@ -0,0 +1,87 @@
|
||||
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: 23
|
||||
otp_version: '23.3'
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
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
|
||||
@@ -13,8 +13,9 @@ jobs:
|
||||
name: Linux, ${{ matrix.otp_release }}, Ubuntu 16.04
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
otp_release: ['OTP-23.0', 'OTP-22.3', 'OTP-22.0', 'OTP-21.3.8', 'OTP-21.0']
|
||||
otp_release: ['OTP-24.0', 'OTP-23.0', 'OTP-22.3', 'OTP-22.0', 'OTP-21.3.8', 'OTP-21.0']
|
||||
development: [false]
|
||||
include:
|
||||
- otp_release: master
|
||||
@@ -23,17 +24,17 @@ jobs:
|
||||
development: true
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Erlang/OTP
|
||||
run: |
|
||||
cd $RUNNER_TEMP
|
||||
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${{ matrix.otp_release }}.tar.gz
|
||||
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-16.04/${{ matrix.otp_release }}.tar.gz
|
||||
mkdir -p otp
|
||||
tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
otp/Install -minimal $(pwd)/otp
|
||||
echo "::add-path::$(pwd)/otp/bin"
|
||||
echo "$(pwd)/otp/bin" >> $GITHUB_PATH
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
rm -rf .git
|
||||
@@ -61,7 +62,7 @@ jobs:
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Cache Erlang/OTP package
|
||||
@@ -88,7 +89,7 @@ jobs:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: "Release pre built"
|
||||
description: "Builds elixir release, ExDoc and generates docs"
|
||||
inputs:
|
||||
otp:
|
||||
description: "The major OTP version"
|
||||
otp_version:
|
||||
description: "The exact OTP version (major.minor[.patch])"
|
||||
build_docs:
|
||||
description: "If docs have to be built or not"
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{ inputs.otp_version }}
|
||||
version-type: strict
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
- name: Get latest stable ExDoc version
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ inputs.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
|
||||
path: ex_doc
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
mv ex_doc ../ex_doc
|
||||
cd ../ex_doc
|
||||
../elixir/bin/mix do local.rebar --force, local.hex --force, deps.get, compile
|
||||
cd ../elixir
|
||||
- name: Build Docs
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
make Docs.zip
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
+99
-9
@@ -14,7 +14,7 @@ Elixir v1.11 builds on top of the recently added compilation tracers to track ap
|
||||
|
||||
These two conditions may seem contradictory. After all, if a module is available, it must have come from a dependency. This is not true in two scenarios:
|
||||
|
||||
* Modules from Elixir and Erlang/OTP are always available - even if their applications are not explicitly listed as a dependency
|
||||
* Modules from Elixir and Erlang/OTP are always available - even if their applications are not listed as a dependency
|
||||
|
||||
* In an umbrella project, because all child applications are compiled within the same VM, you may have a module from a sibling project available, even if you don't depend on said sibling
|
||||
|
||||
@@ -36,7 +36,7 @@ on :ssl. To fix this, you must do one of:
|
||||
to your "def project" in mix.exs
|
||||
```
|
||||
|
||||
This comes with extra benefits in umbrella projects, as it requires child applications to explicitly list their dependencies, completely rejecting cyclic dependencies between siblings.
|
||||
This comes with extra benefits in umbrella projects, as it requires applications to depend on the siblings they depend on, which will fail if there are any cyclic dependencies.
|
||||
|
||||
## Compiler checks: data constructors
|
||||
|
||||
@@ -243,7 +243,93 @@ The Calendar module ships with a new `Calendar.strftime/3` function, which provi
|
||||
|
||||
Mix also includes two new tasks: `mix app.config`, for application runtime configuration, and `mix test.coverage`, which generates aggregated coverage reports for umbrella projects and for test suites partitioned across processes.
|
||||
|
||||
## v1.11.0-dev
|
||||
## v1.11.4 (2021-03-16)
|
||||
|
||||
This release introduces fixes for better support of Erlang/OTP 24+.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Update formatting when printing warnings and errors from Erlang/OTP 24+
|
||||
* [Kernel] Support float-16 on bitstrings
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix local.rebar] This task will now install rebar3 version 3.14.4, compiled with Erlang/OTP 21
|
||||
|
||||
## v1.11.3 (2021-01-04)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Macro] Add `Macro.unique_var/2` and `Macro.generate_unique_arguments/2`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Do not raise when checking for operator ambiguity when `:static_atoms_encoder` is set in `Code.string_to_quoted`
|
||||
* [Kernel] Emit undefined function warnings from `with`
|
||||
* [Kernel] Do not fail type checking when literals are used in guards
|
||||
* [Module] Do not warn for attributes used in `@after_compile`
|
||||
* [Record] Make sure nested record names do not clobber each other
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Do not crash if there are macros and module attributes on the left side of `++`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Do not use Unicode chars if ANSI is disabled
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps.compile] Fix compatibility with rebar v3.14
|
||||
* [mix release] Do not use private `ram_file:compress/1`
|
||||
* [mix xref] Do not crash when retrieving calls for modules in memory
|
||||
|
||||
## v1.11.2 (2020-11-03)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Do not crash when getting docs for missing `erts` appdir
|
||||
* [Kernel] Raise meaningful error if `:erlang.is_record` is used in guards
|
||||
* [Kernel] Prune tracers when fetching `__ENV__` inside functions
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Fix regression where aliases could not call themselves recursively
|
||||
* [mix compile] Do not discard tracers that are set programatically
|
||||
|
||||
## v1.11.1 (2020-10-16)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Ignore tracers if lexical tracker is dead or explicitly nil when evaling code with an environment
|
||||
* [GenServer] Do not show warning when using `super` in `GenServer.child_spec/1`
|
||||
* [Kernel] Do not crash when :reduce is set to `nil` in comprehensions
|
||||
* [Kernel] Fix a scenario where undefined function warnings were not being emitted
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Properly handle tags inside typespec when showing Erlang docs
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Do not deadlock Logger if handler crashes on sync mode
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Add inet6 fallback to Mix usage of httpc
|
||||
* [mix compile.app] Do not list apps that do not match the current target
|
||||
|
||||
## v1.11.0 (2020-10-06)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
@@ -284,6 +370,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
|
||||
* [ExUnit] Add `@tag :tmp_dir` support to ExUnit. The temporary directory is automatically created and pruned before each test
|
||||
* [ExUnit] Add file and line to ExUnit's `--trace`
|
||||
* [ExUnit.Assertion] Allow receive timeouts to be computed at runtime
|
||||
* [ExUnit.Case] Add `register_test/6` to speed up compilation of custom tests
|
||||
* [ExUnit.Doctest] Allow users to add tags to doctests
|
||||
|
||||
#### IEx
|
||||
@@ -305,17 +392,17 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
|
||||
* [mix] Allow requirements for a Mix task to be listed via the `@requirements` module attribute
|
||||
* [mix] Allow optional dependencies to be defined in `:extra_applications` and `:applications`
|
||||
* [mix app.config] Add new `mix app.config` task that compiles applications and loads runtime configuration
|
||||
* [mix archive.install] Support `--repo` option on hex packages
|
||||
* [mix archive.install] Support `--repo` option on Hex packages
|
||||
* [mix compile] Support the `__mix_recompile__?/0` callback for custom behaviour on when Mix should recompile a given module
|
||||
* [mix compile.elixir] Mark modules for path dependencies as "Export dependencies" if they changed but their public interface is the same
|
||||
* [mix compile.elixir] Track application boundaries in the Elixir compiler. If you invoke code from Erlang or Elixir standard libraries and you don't depend on the proper applications, a warning will be emitted. A warning will also be emitted if you invoke code from an umbrella sibling that you don't depend on - effectively forbidding cyclic dependencies between apps
|
||||
* [mix deps] Sort the dependencies alphabetically before printing
|
||||
* [mix deps] Use `origin/HEAD` as the default git ref in dependencies
|
||||
* [mix deps] Use `origin/HEAD` as the default Git ref in dependencies
|
||||
* [mix deps] Redact Git `username`/`password` in output log
|
||||
* [mix deps] Support rebar3's `git_subdir` resource type
|
||||
* [mix deps.compile] Allow local deps to be skipped on `mix deps.compile`
|
||||
* [mix deps.unlock] Print which dependencies get unlocked when using the `--unused` flag
|
||||
* [mix escript.install] Support `--repo` option on hex packages
|
||||
* [mix escript.install] Support `--repo` option on Hex packages
|
||||
* [mix new] Add `@impl` to application generated by `mix new --sup`
|
||||
* [mix release] Enable overriding `sys.config` location via `RELEASE_SYS_CONFIG` env var
|
||||
* [mix release] Boot a release under configuration in interactive mode and then swap to embedded mode (if running on Erlang/OTP 23+)
|
||||
@@ -343,9 +430,12 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
|
||||
* [Kernel] Properly parse `&//2` (i.e. the capture of the division operator)
|
||||
* [Kernel] Raise `CompileError` when trying to define reserved types
|
||||
* [Kernel] Improve compiler error message when using `|` in a `def` signature
|
||||
* [Kernel] Improve error message when trying to use invalid list operators in guards
|
||||
* [Kernel.SpecialForms] Add `|/2` to the list of special forms to avoid inconsistent behaviour on overrides
|
||||
* [Keyword] Enforce keys to be atoms in `Keyword.keys/1`
|
||||
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to https://url.spec.whatwg.org/#application/x-www-form-urlencoded
|
||||
* [Record] Keep lexical ordering when creating records
|
||||
* [Registry] Do not crash when a process with key-value has been registered using `:via` and it fails to start on `init`
|
||||
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to [URL's living standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) - note this behaviour is not specified in the spec implemented by the URI module, so the living standard was chosen
|
||||
* [Version] Add defaults and enforce keys in `Version` struct
|
||||
|
||||
#### ExUnit
|
||||
@@ -357,7 +447,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix tokenizer emitting repeated warnings in the REPL
|
||||
* [IEx] Ensure `dot_iex_path` is preserved when restarting the evaluator
|
||||
* [IEx] Ensure `--dot-iex` is preserved when restarting the evaluator and after shell respawn
|
||||
* [IEx.Pry] Ensure `IEx.pry` can be triggered more than twice when invoked from the same process
|
||||
|
||||
#### Mix
|
||||
@@ -395,7 +485,7 @@ Mix also includes two new tasks: `mix app.config`, for application runtime confi
|
||||
|
||||
* [Supervisor] Deprecate `Supervisor.start_child/2` and `Supervisor.terminate_child/2` in favor of `DynamicSupervisor`
|
||||
* [Supervisor.Spec] Deprecate `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` in favor of the new typespecs
|
||||
* [System] Deprecate `System.stracktrace/0` in favor of `__STACKTRACE__`
|
||||
* [System] Deprecate `System.stacktrace/0` in favor of `__STACKTRACE__`
|
||||
|
||||
#### Mix
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
#CANONICAL := vMAJOR.MINOR/
|
||||
CANONICAL ?= master/
|
||||
CANONICAL := v1.11/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||
@@ -179,40 +178,48 @@ clean_residual_files:
|
||||
|
||||
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
|
||||
DOCS_FORMAT = html
|
||||
COMPILE_DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
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_elixir: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (elixir)"
|
||||
$(Q) rm -rf doc/elixir
|
||||
$(call COMPILE_DOCS,Elixir,elixir,Kernel,--config "lib/elixir/docs.exs")
|
||||
$(call DOCS_COMPILE,Elixir,elixir,Kernel,--config "lib/elixir/scripts/elixir_docs.exs")
|
||||
$(call DOCS_CONFIG,elixir)
|
||||
|
||||
docs_eex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (eex)"
|
||||
$(Q) rm -rf doc/eex
|
||||
$(call COMPILE_DOCS,EEx,eex,EEx)
|
||||
$(call DOCS_COMPILE,EEx,eex,EEx,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,eex)
|
||||
|
||||
docs_mix: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (mix)"
|
||||
$(Q) rm -rf doc/mix
|
||||
$(call COMPILE_DOCS,Mix,mix,Mix)
|
||||
$(call DOCS_COMPILE,Mix,mix,Mix,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,mix)
|
||||
|
||||
docs_iex: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (iex)"
|
||||
$(Q) rm -rf doc/iex
|
||||
$(call COMPILE_DOCS,IEx,iex,IEx)
|
||||
$(call DOCS_COMPILE,IEx,iex,IEx,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,iex)
|
||||
|
||||
docs_ex_unit: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (ex_unit)"
|
||||
$(Q) rm -rf doc/ex_unit
|
||||
$(call COMPILE_DOCS,ExUnit,ex_unit,ExUnit)
|
||||
$(call DOCS_COMPILE,ExUnit,ex_unit,ExUnit,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,ex_unit)
|
||||
|
||||
docs_logger: compile ../ex_doc/bin/ex_doc
|
||||
@ echo "==> ex_doc (logger)"
|
||||
$(Q) rm -rf doc/logger
|
||||
$(call COMPILE_DOCS,Logger,logger,Logger)
|
||||
$(call DOCS_COMPILE,Logger,logger,Logger,--config "lib/elixir/scripts/mix_docs.exs")
|
||||
$(call DOCS_CONFIG,logger)
|
||||
|
||||
../ex_doc/bin/ex_doc:
|
||||
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
|
||||
@@ -221,23 +228,23 @@ docs_logger: compile ../ex_doc/bin/ex_doc
|
||||
#==> Zip tasks
|
||||
|
||||
Docs.zip: docs
|
||||
rm -f Docs-v$(VERSION).zip
|
||||
zip -9 -r Docs-v$(VERSION).zip CHANGELOG.md doc NOTICE LICENSE README.md
|
||||
@ echo "Docs file created $(CURDIR)/Docs-v$(VERSION).zip"
|
||||
rm -f Docs.zip
|
||||
zip -9 -r Docs.zip CHANGELOG.md doc NOTICE LICENSE README.md
|
||||
@ echo "Docs file created $(CURDIR)/Docs.zip"
|
||||
|
||||
Precompiled.zip: build_man compile
|
||||
rm -f Precompiled-v$(VERSION).zip
|
||||
zip -9 -r Precompiled-v$(VERSION).zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE man NOTICE README.md VERSION
|
||||
@ echo "Precompiled file created $(CURDIR)/Precompiled-v$(VERSION).zip"
|
||||
rm -f Precompiled.zip
|
||||
zip -9 -r Precompiled.zip bin CHANGELOG.md lib/*/ebin lib/*/lib LICENSE man NOTICE README.md VERSION
|
||||
@ echo "Precompiled file created $(CURDIR)/Precompiled.zip"
|
||||
|
||||
zips: Precompiled.zip Docs.zip
|
||||
@ echo ""
|
||||
@ echo "### Checksums"
|
||||
@ echo ""
|
||||
@ shasum -a 1 < Precompiled-v$(VERSION).zip | sed -e "s/-//" | xargs echo " * Precompiled.zip SHA1:"
|
||||
@ shasum -a 512 < Precompiled-v$(VERSION).zip | sed -e "s/-//" | xargs echo " * Precompiled.zip SHA512:"
|
||||
@ shasum -a 1 < Docs-v$(VERSION).zip | sed -e "s/-//" | xargs echo " * Docs.zip SHA1:"
|
||||
@ shasum -a 512 < Docs-v$(VERSION).zip | sed -e "s/-//" | xargs echo " * Docs.zip SHA512:"
|
||||
@ shasum -a 1 < Precompiled.zip | sed -e "s/-//" | xargs echo " * Precompiled.zip SHA1:"
|
||||
@ shasum -a 512 < Precompiled.zip | sed -e "s/-//" | xargs echo " * Precompiled.zip SHA512:"
|
||||
@ shasum -a 1 < Docs.zip | sed -e "s/-//" | xargs echo " * Docs.zip SHA1:"
|
||||
@ shasum -a 512 < Docs.zip | sed -e "s/-//" | xargs echo " * Docs.zip SHA512:"
|
||||
@ echo ""
|
||||
|
||||
#==> Test tasks
|
||||
|
||||
+2
-3
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
| Elixir version | Support
|
||||
| -------------- | ------------------------------
|
||||
| 1.11 | Development
|
||||
| 1.10 | Bug fixes and security patches
|
||||
| 1.11 | Bug fixes and security patches
|
||||
| 1.10 | Security patches only
|
||||
| 1.9 | Security patches only
|
||||
| 1.8 | Security patches only
|
||||
| 1.7 | Security patches only
|
||||
| 1.6 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ defmodule EEx.Compiler do
|
||||
# Generates the buffers by handling each expression from the tokenizer.
|
||||
# It returns Macro.t/0 or it raises.
|
||||
|
||||
defp generate_buffer([{:text, chars} | rest], buffer, scope, state) do
|
||||
defp generate_buffer([{:text, _line, _column, chars} | rest], buffer, scope, state) do
|
||||
buffer = state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
@@ -179,7 +179,7 @@ defmodule EEx.Compiler do
|
||||
# Look middle expressions that immediately follow a start_expr
|
||||
|
||||
defp look_ahead_middle(
|
||||
[{:text, text}, {:middle_expr, line, _column, _, chars} | rest] = tokens,
|
||||
[{:text, _, _, text}, {:middle_expr, line, _, _, chars} | rest] = tokens,
|
||||
start,
|
||||
contents
|
||||
) do
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule EEx.Tokenizer do
|
||||
@type column :: non_neg_integer
|
||||
@type marker :: '=' | '/' | '|' | ''
|
||||
@type token ::
|
||||
{:text, content}
|
||||
{:text, line, column, content}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, line, column, marker, content}
|
||||
| {:eof, line, column}
|
||||
|
||||
@@ -17,7 +17,7 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
It returns {:ok, list} with the following tokens:
|
||||
|
||||
* `{:text, content}`
|
||||
* `{:text, line, column, content}`
|
||||
* `{:expr, line, column, marker, content}`
|
||||
* `{:start_expr, line, column, marker, content}`
|
||||
* `{:middle_expr, line, column, marker, content}`
|
||||
@@ -36,8 +36,11 @@ defmodule EEx.Tokenizer do
|
||||
def tokenize(list, line, column, opts)
|
||||
when is_list(list) and is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
|
||||
column = opts.indentation + column
|
||||
{list, line, column} = (opts.trim && trim_init(list, line, column)) || {list, line, column}
|
||||
tokenize(list, line, column, opts, [], [])
|
||||
|
||||
{list, line, column} =
|
||||
(opts.trim && trim_init(list, line, column, opts)) || {list, line, column}
|
||||
|
||||
tokenize(list, line, column, opts, [{line, column}], [])
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, column, opts, buffer, acc) do
|
||||
@@ -53,7 +56,8 @@ defmodule EEx.Tokenizer do
|
||||
{rest, new_line, new_column, buffer} =
|
||||
trim_if_needed(rest, new_line, new_column, opts, buffer)
|
||||
|
||||
tokenize(rest, new_line, new_column, opts, buffer, acc)
|
||||
acc = tokenize_text(buffer, acc)
|
||||
tokenize(rest, new_line, new_column, opts, [{new_line, new_column}], acc)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,10 +69,10 @@ defmodule EEx.Tokenizer do
|
||||
error
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
key =
|
||||
{key, expr} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, tokens} -> token_key(tokens)
|
||||
{:error, _, _, _} -> :expr
|
||||
{:ok, tokens} -> token_key(tokens, expr)
|
||||
{:error, _, _, _} -> {:expr, expr}
|
||||
end
|
||||
|
||||
{rest, new_line, new_column, buffer} =
|
||||
@@ -76,7 +80,7 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {key, line, column, marker, expr}
|
||||
tokenize(rest, new_line, new_column, opts, [], [final | acc])
|
||||
tokenize(rest, new_line, new_column, opts, [{new_line, new_column}], [final | acc])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -122,87 +126,109 @@ defmodule EEx.Tokenizer do
|
||||
end
|
||||
|
||||
# Receives tokens and check if it is a start, middle or an end token.
|
||||
defp token_key(tokens) do
|
||||
defp token_key(tokens, expr) do
|
||||
case {tokens, Enum.reverse(tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
:middle_expr
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
:start_expr
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
:middle_expr
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
:middle_expr
|
||||
{:middle_expr, expr}
|
||||
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if end_index > fn_index do
|
||||
:start_expr
|
||||
{:start_expr, expr}
|
||||
else
|
||||
:middle_expr
|
||||
{:middle_expr, expr}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> :end_expr
|
||||
_ -> :expr
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_append_space([?\s]), do: [?\s]
|
||||
defp maybe_append_space([h]), do: [h, ?\s]
|
||||
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
|
||||
|
||||
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
|
||||
defp closing_bracket?(_), do: false
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
|
||||
defp tokenize_text([], acc) do
|
||||
defp tokenize_text([{_line, _column}], acc) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp tokenize_text(buffer, acc) do
|
||||
[{:text, Enum.reverse(buffer)} | acc]
|
||||
[{line, column} | buffer] = Enum.reverse(buffer)
|
||||
[{:text, line, column, buffer} | acc]
|
||||
end
|
||||
|
||||
defp trim_if_needed(rest, line, column, opts, buffer) do
|
||||
if opts.trim do
|
||||
buffer = trim_left(buffer, 0)
|
||||
{rest, line, column} = trim_right(rest, line, column, 0)
|
||||
{rest, line, column} = trim_right(rest, line, column, 0, opts)
|
||||
{rest, line, column, buffer}
|
||||
else
|
||||
{rest, line, column, buffer}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_init([h | t], line, column) when h in @spaces, do: trim_init(t, line, column + 1)
|
||||
defp trim_init([?\r, ?\n | t], line, _column), do: trim_init(t, line + 1, 1)
|
||||
defp trim_init([?\n | t], line, _column), do: trim_init(t, line + 1, 1)
|
||||
defp trim_init([?<, ?% | _] = rest, line, column), do: {rest, line, column}
|
||||
defp trim_init(_, _, _), do: false
|
||||
defp trim_init([h | t], line, column, opts) when h in @spaces,
|
||||
do: trim_init(t, line, column + 1, opts)
|
||||
|
||||
defp trim_init([?\r, ?\n | t], line, _column, opts),
|
||||
do: trim_init(t, line + 1, opts.indentation + 1, opts)
|
||||
|
||||
defp trim_init([?\n | t], line, _column, opts),
|
||||
do: trim_init(t, line + 1, opts.indentation + 1, opts)
|
||||
|
||||
defp trim_init([?<, ?% | _] = rest, line, column, _opts),
|
||||
do: {rest, line, column}
|
||||
|
||||
defp trim_init(_, _, _, _), do: false
|
||||
|
||||
defp trim_left(buffer, count) do
|
||||
case trim_whitespace(buffer) do
|
||||
[?\n, ?\r | rest] -> trim_left(rest, count + 1)
|
||||
[?\n | rest] -> trim_left(rest, count + 1)
|
||||
case trim_whitespace(buffer, 0) do
|
||||
{[?\n, ?\r | rest], _} -> trim_left(rest, count + 1)
|
||||
{[?\n | rest], _} -> trim_left(rest, count + 1)
|
||||
_ when count > 0 -> [?\n | buffer]
|
||||
_ -> buffer
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_right(rest, line, column, count) do
|
||||
case trim_whitespace(rest) do
|
||||
[?\r, ?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
|
||||
[?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
|
||||
[] -> {[], line, column + length(rest)}
|
||||
_ when count > 0 -> {[?\n | rest], line - 1, column}
|
||||
_ -> {rest, line, column}
|
||||
defp trim_right(rest, line, column, last_column, opts) do
|
||||
case trim_whitespace(rest, column) do
|
||||
{[?\r, ?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, opts.indentation + 1, column + 1, opts)
|
||||
|
||||
{[?\n | rest], column} ->
|
||||
trim_right(rest, line + 1, opts.indentation + 1, column, opts)
|
||||
|
||||
{[], column} ->
|
||||
{[], line, column}
|
||||
|
||||
_ when last_column > 0 ->
|
||||
{[?\n | rest], line - 1, last_column}
|
||||
|
||||
_ ->
|
||||
{rest, line, column}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_whitespace([h | t]) when h in @spaces, do: trim_whitespace(t)
|
||||
defp trim_whitespace(list), do: list
|
||||
defp trim_whitespace([h | t], column) when h in @spaces, do: trim_whitespace(t, column + 1)
|
||||
defp trim_whitespace(list, column), do: {list, column}
|
||||
end
|
||||
|
||||
@@ -7,36 +7,36 @@ defmodule EEx.TokenizerTest do
|
||||
@opts %{indentation: 0, trim: false}
|
||||
|
||||
test "simple chars lists" do
|
||||
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
|
||||
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 1, 1, 'foo'}, {:eof, 1, 4}]}
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
|
||||
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 1, 1, 'foo'}, {:eof, 1, 4}]}
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert T.tokenize('foo <% bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert T.tokenize('foo <%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with embedded slash code" do
|
||||
assert T.tokenize('foo <%/ bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with embedded pipe code" do
|
||||
assert T.tokenize('foo <%| bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
|
||||
{:ok, [{:text, 1, 1, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert T.tokenize('foo\n<%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
|
||||
{:ok, [{:text, 1, 1, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
@@ -48,11 +48,11 @@ defmodule EEx.TokenizerTest do
|
||||
'''
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:expr, 1, 5, '=', ' bar\n\nbaz '},
|
||||
{:text, '\n'},
|
||||
{:text, 3, 7, '\n'},
|
||||
{:expr, 4, 1, '', ' foo '},
|
||||
{:text, '\n'},
|
||||
{:text, 4, 10, '\n'},
|
||||
{:eof, 5, 1}
|
||||
]
|
||||
|
||||
@@ -61,21 +61,21 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "quotation" do
|
||||
assert T.tokenize('foo <%% true %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo <% true %>'}, {:eof, 1, 16}]}
|
||||
{:ok, [{:text, 1, 1, 'foo <% true %>'}, {:eof, 1, 16}]}
|
||||
end
|
||||
|
||||
test "quotation with do/end" do
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
|
||||
{:ok, [{:text, 1, 1, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
|
||||
end
|
||||
|
||||
test "quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, 'a <% b '},
|
||||
{:text, 1, 1, 'a <% b '},
|
||||
{:expr, 1, 9, '=', ' c '},
|
||||
{:text, ' '},
|
||||
{:text, 1, 17, ' '},
|
||||
{:expr, 1, 18, '=', ' d '},
|
||||
{:text, ' e %> f'},
|
||||
{:text, 1, 26, ' e %> f'},
|
||||
{:eof, 1, 33}
|
||||
]
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "improperly formatted quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, '<%% a <%= b %> c %>'},
|
||||
{:text, 1, 1, '<%% a <%= b %> c %>'},
|
||||
{:eof, 1, 22}
|
||||
]
|
||||
|
||||
@@ -93,7 +93,7 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "eex comments" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:eof, 1, 16}
|
||||
]
|
||||
|
||||
@@ -102,7 +102,8 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "eex comments with do/end" do
|
||||
exprs = [
|
||||
{:text, 'foo bar'},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:text, 1, 19, 'bar'},
|
||||
{:eof, 1, 32}
|
||||
]
|
||||
|
||||
@@ -111,7 +112,7 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "elixir comments" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:expr, 1, 5, [], ' true # this is a boolean '},
|
||||
{:eof, 1, 35}
|
||||
]
|
||||
@@ -122,7 +123,7 @@ defmodule EEx.TokenizerTest do
|
||||
test "elixir comments with do/end" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, [], ' if true do # startif '},
|
||||
{:text, 'text'},
|
||||
{:text, 1, 27, 'text'},
|
||||
{:end_expr, 1, 31, [], ' end # closeif '},
|
||||
{:eof, 1, 50}
|
||||
]
|
||||
@@ -133,9 +134,9 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "strings with embedded do end" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:text, 1, 21, 'bar'},
|
||||
{:end_expr, 1, 24, '', ' end '},
|
||||
{:eof, 1, 33}
|
||||
]
|
||||
@@ -145,12 +146,12 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' cond do '},
|
||||
{:middle_expr, 1, 18, '', ' false -> '},
|
||||
{:text, 'bar'},
|
||||
{:text, 1, 32, 'bar'},
|
||||
{:middle_expr, 1, 35, '', ' true -> '},
|
||||
{:text, 'baz'},
|
||||
{:text, 1, 48, 'baz'},
|
||||
{:end_expr, 1, 51, '', ' end '},
|
||||
{:eof, 1, 60}
|
||||
]
|
||||
@@ -162,9 +163,9 @@ defmodule EEx.TokenizerTest do
|
||||
test "strings with multiple callbacks" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 'foo'},
|
||||
{:text, 1, 15, 'foo'},
|
||||
{:middle_expr, 1, 18, '', ' end, fn -> '},
|
||||
{:text, 'bar'},
|
||||
{:text, 1, 34, 'bar'},
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
]
|
||||
@@ -176,9 +177,9 @@ defmodule EEx.TokenizerTest do
|
||||
test "strings with callback followed by do block" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 'foo'},
|
||||
{:text, 1, 15, 'foo'},
|
||||
{:middle_expr, 1, 18, '', ' end do '},
|
||||
{:text, 'bar'},
|
||||
{:text, 1, 30, 'bar'},
|
||||
{:end_expr, 1, 33, '', ' end '},
|
||||
{:eof, 1, 42}
|
||||
]
|
||||
@@ -188,11 +189,11 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:text, 1, 1, 'foo '},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:text, 1, 21, 'bar'},
|
||||
{:middle_expr, 1, 24, '', ' else '},
|
||||
{:text, 'baz'},
|
||||
{:text, 1, 34, 'baz'},
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
]
|
||||
@@ -206,9 +207,9 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
exprs = [
|
||||
{:start_expr, 1, 2, '=', ' if true do '},
|
||||
{:text, '\n TRUE \n'},
|
||||
{:text, 1, 20, '\n TRUE \n'},
|
||||
{:middle_expr, 3, 3, '', ' else '},
|
||||
{:text, '\n FALSE \n'},
|
||||
{:text, 3, 13, '\n FALSE \n'},
|
||||
{:end_expr, 5, 3, '', ' end '},
|
||||
{:eof, 7, 3}
|
||||
]
|
||||
@@ -218,7 +219,7 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "trim mode with comment" do
|
||||
exprs = [
|
||||
{:text, '\n123'},
|
||||
{:text, 1, 19, '\n123'},
|
||||
{:eof, 2, 4}
|
||||
]
|
||||
|
||||
@@ -227,9 +228,9 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "trim mode with CRLF" do
|
||||
exprs = [
|
||||
{:text, '0\n'},
|
||||
{:text, 1, 1, '0\n'},
|
||||
{:expr, 2, 3, '=', ' 12 '},
|
||||
{:text, '\n34'},
|
||||
{:text, 2, 15, '\n34'},
|
||||
{:eof, 3, 3}
|
||||
]
|
||||
|
||||
@@ -238,9 +239,9 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
test "trim mode set to false" do
|
||||
exprs = [
|
||||
{:text, ' '},
|
||||
{:text, 1, 1, ' '},
|
||||
{:expr, 1, 2, '=', ' 12 '},
|
||||
{:text, ' \n'},
|
||||
{:text, 1, 11, ' \n'},
|
||||
{:eof, 2, 1}
|
||||
]
|
||||
|
||||
|
||||
@@ -127,6 +127,17 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
test "trim mode with no spaces" do
|
||||
string = """
|
||||
<%=if true do%>
|
||||
this
|
||||
<%else%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n this\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
|
||||
@@ -111,9 +111,9 @@ defmodule Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
defp __behaviour__doc_value(:none), do: nil
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(%{"en" => doc}), do: doc
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(_), do: nil
|
||||
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
|
||||
@@ -8,6 +8,12 @@ defmodule Calendar.ISO do
|
||||
applied for all time, consequently the dates give different results
|
||||
before the year 1583 from when the Gregorian calendar was adopted.
|
||||
|
||||
Given this is the default calendar used by Elixir, it has one
|
||||
difference compared to the ISO8601 specification in that it allows
|
||||
a whitespace instead of `T` as a seperator between date and times
|
||||
both when parsing and formatting. Strict formatting can be done
|
||||
by using the `to_iso8601` found in `NaiveDateTime` and `DateTime`.
|
||||
|
||||
Note that while ISO 8601 allows times and datetimes to specify
|
||||
24:00:00 as the zero hour of the next day, this notation is not
|
||||
supported by Elixir.
|
||||
|
||||
+11
-3
@@ -1348,7 +1348,7 @@ defmodule Code do
|
||||
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
|
||||
when annotation: :erl_anno.anno(),
|
||||
beam_language: :elixir | :erlang | atom(),
|
||||
doc_content: %{required(binary) => binary} | :none | :hidden,
|
||||
doc_content: %{optional(binary) => binary} | :none | :hidden,
|
||||
doc_element:
|
||||
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
|
||||
metadata},
|
||||
@@ -1373,8 +1373,16 @@ defmodule Code do
|
||||
:error ->
|
||||
case :code.which(module) do
|
||||
:preloaded ->
|
||||
path = Path.join([:code.lib_dir(:erts), "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
# The erts directory is not necessarily included in releases
|
||||
# unless it is listed as an extra application.
|
||||
case :code.lib_dir(:erts) do
|
||||
path when is_list(path) ->
|
||||
path = Path.join([path, "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :chunk_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :module_not_found}
|
||||
|
||||
@@ -64,8 +64,8 @@ defmodule Config do
|
||||
## config/runtime.exs
|
||||
|
||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||
It is executed after your Mix project is compiled and also before a
|
||||
release (assembled with `mix release`) starts.
|
||||
It is executed right before applications start in both Mix and releases
|
||||
(assembled with `mix release`).
|
||||
"""
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@@ -160,7 +160,10 @@ defmodule Config do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the environemnt 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
|
||||
file is executed on. In releases, the environment when `mix release` ran.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
|
||||
+31
-23
@@ -473,15 +473,25 @@ defmodule Enum do
|
||||
@doc """
|
||||
Chunks the `enumerable` with fine grained control when every chunk is emitted.
|
||||
|
||||
`chunk_fun` receives the current element and the accumulator and
|
||||
must return `{:cont, chunk, acc}` to emit the given chunk and
|
||||
continue with accumulator or `{:cont, acc}` to not emit any chunk
|
||||
and continue with the return accumulator.
|
||||
`chunk_fun` receives the current element and the accumulator and must return:
|
||||
|
||||
`after_fun` is invoked when iteration is done and must also return
|
||||
`{:cont, chunk, acc}` or `{:cont, acc}`.
|
||||
* `{:cont, chunk, acc}` to emit a chunk and continue with the accumulator
|
||||
* `{:cont, acc}` to not emit any chunk and continue with the accumulator
|
||||
* `{:halt, acc}` to halt chunking over the `enumerable`.
|
||||
|
||||
Returns a list of lists.
|
||||
`after_fun` is invoked with the final accumulator when iteration is
|
||||
finished (or `halt`ed) to handle any trailing elements that were returned
|
||||
as part of an accumulator, but were not emited as a chunk by `chunk_fun`.
|
||||
It must return:
|
||||
|
||||
* `{:cont, chunk, acc}` to emit a chunk. The chunk will be appended to the
|
||||
list of already emitted chunks.
|
||||
* `{:cont, acc}` to not emit a chunk
|
||||
|
||||
The `acc` in `after_fun` is required in order to mirror the tuple format
|
||||
from `chunk_fun` but it will be discarded since the traversal is complete.
|
||||
|
||||
Returns a list of emitted chunks.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -498,6 +508,8 @@ defmodule Enum do
|
||||
...> end
|
||||
iex> Enum.chunk_while(1..10, [], chunk_fun, after_fun)
|
||||
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
|
||||
iex> Enum.chunk_while([1, 2, 3, 5, 7], [], chunk_fun, after_fun)
|
||||
[[1, 2], [3, 5, 7]]
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@@ -512,7 +524,7 @@ defmodule Enum do
|
||||
{_, {res, acc}} =
|
||||
Enumerable.reduce(enumerable, {:cont, {[], acc}}, fn entry, {buffer, acc} ->
|
||||
case chunk_fun.(entry, acc) do
|
||||
{:cont, emit, acc} -> {:cont, {[emit | buffer], acc}}
|
||||
{:cont, chunk, acc} -> {:cont, {[chunk | buffer], acc}}
|
||||
{:cont, acc} -> {:cont, {buffer, acc}}
|
||||
{:halt, acc} -> {:halt, {buffer, acc}}
|
||||
end
|
||||
@@ -520,7 +532,7 @@ defmodule Enum do
|
||||
|
||||
case after_fun.(acc) do
|
||||
{:cont, _acc} -> :lists.reverse(res)
|
||||
{:cont, elem, _acc} -> :lists.reverse([elem | res])
|
||||
{:cont, chunk, _acc} -> :lists.reverse([chunk | res])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1593,7 +1605,11 @@ defmodule Enum do
|
||||
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
|
||||
|
||||
@doc false
|
||||
@spec max_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
|
||||
@spec max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(() -> empty_result) | (element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def max_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
@@ -1644,12 +1660,6 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
@spec max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
@@ -1767,7 +1777,11 @@ defmodule Enum do
|
||||
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
|
||||
|
||||
@doc false
|
||||
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(() -> empty_result) | (element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def min_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
@@ -1818,12 +1832,6 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
|
||||
@@ -110,6 +110,8 @@ defmodule File do
|
||||
|
||||
@type stream_mode ::
|
||||
encoding_mode()
|
||||
| :append
|
||||
| :compressed
|
||||
| :trim_bom
|
||||
| {:read_ahead, pos_integer | false}
|
||||
| {:delayed_write, non_neg_integer, non_neg_integer}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullet_text "• "
|
||||
@bullet_text_unicode "• "
|
||||
@bullet_text_ascii "* "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@spaces [" ", "\n", "\t"]
|
||||
|
||||
@@ -219,7 +220,10 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
|
||||
["#{indent} ", @bullet_text | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(entries, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
|
||||
@@ -231,12 +235,19 @@ defmodule IO.ANSI.Docs do
|
||||
types =
|
||||
for {:li, _, lines} <- entries,
|
||||
line <- lines,
|
||||
do: ["#{indent} ", line, ?\n]
|
||||
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
|
||||
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
if types != [] do
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
for {:li, _, lines} <- entries do
|
||||
["#{indent} ", @bullet_text | handle_erlang_html_text(lines, indent <> " ", options)]
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -420,7 +431,7 @@ defmodule IO.ANSI.Docs do
|
||||
case stripped do
|
||||
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
|
||||
write_text(text, indent, options)
|
||||
process_list(@bullet_text, item, rest, count, indent, options)
|
||||
process_list(bullet_text(options), item, rest, count, indent, options)
|
||||
|
||||
<<d1, ?., ?\s, item::binary>> when d1 in ?0..?9 ->
|
||||
write_text(text, indent, options)
|
||||
@@ -926,6 +937,10 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
defp bullet_text(options) do
|
||||
if options[:enabled], do: @bullet_text_unicode, else: @bullet_text_ascii
|
||||
end
|
||||
|
||||
defp color(style, colors) do
|
||||
IO.ANSI.format_fragment(colors[style], colors[:enabled])
|
||||
end
|
||||
|
||||
@@ -3123,7 +3123,15 @@ defmodule Kernel do
|
||||
|
||||
not function? and __CALLER__.context == :match ->
|
||||
raise ArgumentError,
|
||||
"invalid write attribute syntax, you probably meant to use: @#{name} expression"
|
||||
"""
|
||||
invalid write attribute syntax. If you want to define an attribute, don't do this:
|
||||
|
||||
@foo = :value
|
||||
|
||||
Instead, do this:
|
||||
|
||||
@foo :value
|
||||
"""
|
||||
|
||||
# Typespecs attributes are currently special cased by the compiler
|
||||
is_list(args) and typespec?(name) ->
|
||||
@@ -3219,7 +3227,26 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
# All other cases
|
||||
# Error cases
|
||||
defp do_at([{call, meta, ctx_or_args}, [{:do, _} | _] = kw], _meta, name, _function?, _env) do
|
||||
args =
|
||||
cond do
|
||||
is_atom(ctx_or_args) -> []
|
||||
true -> ctx_or_args
|
||||
end
|
||||
|
||||
code = "\n@#{name} (#{Macro.to_string({call, meta, args ++ [kw]})})"
|
||||
|
||||
raise ArgumentError, """
|
||||
expected 0 or 1 argument for @#{name}, got 2.
|
||||
|
||||
It seems you are trying to use the do-syntax with @module attributes \
|
||||
but the do-block is binding to the attribute name. You probably want \
|
||||
to wrap the argument value in parentheses, like this:
|
||||
#{String.replace(code, "\n", "\n ")}
|
||||
"""
|
||||
end
|
||||
|
||||
defp do_at(args, _meta, name, _function?, _env) do
|
||||
raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}"
|
||||
end
|
||||
|
||||
@@ -233,9 +233,9 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Sizes for types are a bit more nuanced. The default size for integers is 8.
|
||||
|
||||
For floats, it is 64. For floats, `size * unit` must result in 32 or 64,
|
||||
For floats, it is 64. For floats, `size * unit` must result in 16, 32, or 64,
|
||||
corresponding to [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point)
|
||||
binary32 and binary64, respectively.
|
||||
binary16, binary32, and binary64, respectively.
|
||||
|
||||
For binaries, the default is the size of the binary. Only the last binary in a
|
||||
match can use the default size. All others must have their size specified
|
||||
|
||||
@@ -23,9 +23,8 @@ defmodule Kernel.Typespec do
|
||||
for {{:type, name, arity}, _, _, doc, _} <- docs do
|
||||
case doc do
|
||||
%{"en" => doc_string} -> {{name, arity}, doc_string}
|
||||
:none -> {{name, arity}, nil}
|
||||
# Hidden or unknown format are ignored
|
||||
_ -> {{name, arity}, false}
|
||||
:hidden -> {{name, arity}, false}
|
||||
_ -> {{name, arity}, nil}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -668,10 +668,19 @@ defmodule Keyword do
|
||||
@doc since: "1.11.0"
|
||||
@spec replace(t, key, value) :: t
|
||||
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> [{key, value} | delete(keywords, key)]
|
||||
false -> keywords
|
||||
end
|
||||
do_replace(keywords, key, value)
|
||||
end
|
||||
|
||||
defp do_replace([{key, _} | keywords], key, value) do
|
||||
[{key, value} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp do_replace([{_, _} = e | keywords], key, value) do
|
||||
[e | do_replace(keywords, key, value)]
|
||||
end
|
||||
|
||||
defp do_replace([], _key, _value) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -704,7 +713,7 @@ defmodule Keyword do
|
||||
[e | replace!(keywords, key, value, original)]
|
||||
end
|
||||
|
||||
defp replace!([], key, _value, original) when is_atom(key) do
|
||||
defp replace!([], key, _value, original) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
|
||||
+58
-7
@@ -337,8 +337,13 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates AST nodes for a given number of required argument variables using
|
||||
`Macro.var/2`.
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.var/2`.
|
||||
|
||||
Note the arguments are not unique. If you later on want
|
||||
to access this same varibles, you can invoke this function
|
||||
with the same inputs. Use `generate_unique_arguments/2` to
|
||||
generate a unique arguments that can't be overridden.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -349,19 +354,47 @@ defmodule Macro do
|
||||
@doc since: "1.5.0"
|
||||
@spec generate_arguments(0, context :: atom) :: []
|
||||
@spec generate_arguments(pos_integer, context) :: [{atom, [], context}, ...] when context: atom
|
||||
def generate_arguments(amount, context)
|
||||
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
|
||||
|
||||
def generate_arguments(0, context) when is_atom(context), do: []
|
||||
@doc """
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.unique_var/2`.
|
||||
|
||||
def generate_arguments(amount, context)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: var(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
## Examples
|
||||
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
||||
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
||||
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
||||
iex> is_integer(c1) and is_integer(c2)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) :: [
|
||||
{atom, [counter: integer], context},
|
||||
...
|
||||
]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
|
||||
defp generate_arguments(0, context, _fun) when is_atom(context), do: []
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates an AST node representing the variable given
|
||||
by the atoms `var` and `context`.
|
||||
|
||||
Note this variable is not unique. If you later on want
|
||||
to access this same varible, you can invoke `var/2`
|
||||
again with the same argument. Use `unique_var/2` to
|
||||
generate a unique variable that can't be overridden.
|
||||
|
||||
## Examples
|
||||
|
||||
In order to build a variable, a context is expected.
|
||||
@@ -383,6 +416,24 @@ defmodule Macro do
|
||||
{var, [], context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates an AST node representing a unique variable
|
||||
given by the atoms `var` and `context`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
||||
iex> is_integer(c)
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec unique_var(var, context) :: {var, [counter: integer], context}
|
||||
when var: atom, context: atom
|
||||
def unique_var(var, context) when is_atom(var) and is_atom(context) do
|
||||
{var, [counter: :elixir_module.next_counter(context)], context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a depth-first traversal of quoted expressions
|
||||
using an accumulator.
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
import Module.Types.Helpers
|
||||
alias Module.Types.{Expr, Pattern, Infer}
|
||||
alias Module.Types.{Expr, Pattern, Unify}
|
||||
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
@@ -57,7 +57,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
|
||||
head_stack = push_expr_stack(def_expr, stack)
|
||||
head_stack = Unify.push_expr_stack(def_expr, stack)
|
||||
|
||||
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
|
||||
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
|
||||
@@ -300,9 +300,9 @@ defmodule Module.Types do
|
||||
|
||||
[
|
||||
"incompatible types:\n\n ",
|
||||
Infer.format_type(left, simplify_left?),
|
||||
Unify.format_type(left, simplify_left?),
|
||||
" !~ ",
|
||||
Infer.format_type(right, simplify_right?),
|
||||
Unify.format_type(right, simplify_right?),
|
||||
"\n\n",
|
||||
format_expr(expr, location),
|
||||
traces,
|
||||
@@ -352,7 +352,7 @@ defmodule Module.Types do
|
||||
"where \"",
|
||||
Macro.to_string(var),
|
||||
"\" was given the type ",
|
||||
Infer.format_type(type, simplify?),
|
||||
Unify.format_type(type, simplify?),
|
||||
hint,
|
||||
" in:\n\n # ",
|
||||
format_location(location),
|
||||
@@ -518,8 +518,8 @@ defmodule Module.Types do
|
||||
defp map_type?(_other), do: false
|
||||
|
||||
defp atom_type?(:atom), do: true
|
||||
defp atom_type?(:boolean), do: true
|
||||
defp atom_type?({:atom, _}), do: false
|
||||
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
|
||||
defp atom_type?(_other), do: false
|
||||
|
||||
defp integer_type?(:integer), do: true
|
||||
|
||||
@@ -2,7 +2,7 @@ defmodule Module.Types.Expr do
|
||||
@moduledoc false
|
||||
|
||||
alias Module.Types.{Of, Pattern}
|
||||
import Module.Types.{Helpers, Infer}
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
|
||||
of_expr(expr, %{stack | context: :expr}, context)
|
||||
@@ -109,8 +109,7 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# var
|
||||
def of_expr(var, _stack, context) when is_var(var) do
|
||||
{type, context} = new_var(var, context)
|
||||
{:ok, type, context}
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -204,7 +203,7 @@ defmodule Module.Types.Expr do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context),
|
||||
:ok <- of_clauses(clauses, stack, context),
|
||||
{:ok, context} <- of_clauses(clauses, stack, context),
|
||||
do: {:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
@@ -213,7 +212,7 @@ defmodule Module.Types.Expr do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_clauses(clauses, stack, context) do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
{:ok, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
@@ -225,29 +224,37 @@ defmodule Module.Types.Expr do
|
||||
def of_expr({:try, _meta, [blocks]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
result =
|
||||
each_ok(blocks, fn
|
||||
{:rescue, clauses} ->
|
||||
each_ok(clauses, fn
|
||||
{:->, _, [[{:in, _, [var, _exceptions]}], body]} ->
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:rescue, clauses}, context ->
|
||||
reduce_ok(clauses, context, fn
|
||||
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
of_expr_ok(body, stack, context)
|
||||
|
||||
{:->, _, [[var], body]} ->
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{:->, _, [[var], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
of_expr_ok(body, stack, context)
|
||||
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
end)
|
||||
|
||||
{block, body} when block in @try_blocks ->
|
||||
of_expr_ok(body, stack, context)
|
||||
{block, body}, context = acc when block in @try_blocks ->
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{block, clauses} when block in @try_clause_blocks ->
|
||||
{block, clauses}, context when block in @try_clause_blocks ->
|
||||
of_clauses(clauses, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -255,33 +262,42 @@ defmodule Module.Types.Expr do
|
||||
def of_expr({:receive, _meta, [blocks]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
result =
|
||||
each_ok(blocks, fn
|
||||
{:do, {:__block__, _, []}} ->
|
||||
:ok
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:do, {:__block__, _, []}}, context ->
|
||||
{:ok, context}
|
||||
|
||||
{:do, clauses} ->
|
||||
{:do, clauses}, context ->
|
||||
of_clauses(clauses, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [head, body]}]} ->
|
||||
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
|
||||
with {:ok, _type, context} <- of_expr(head, stack, context),
|
||||
{:ok, _type, _context} <- of_expr(body, stack, context),
|
||||
do: :ok
|
||||
{:ok, _type, context} <- of_expr(body, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
# for pat <- expr do expr end
|
||||
def of_expr({:for, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
|
||||
|
||||
case reduce_ok(args, context, &for_clause(&1, stack, &2)) do
|
||||
{:ok, _context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
|
||||
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
|
||||
if Keyword.has_key?(opts, :reduce) do
|
||||
with {:ok, context} <- of_clauses(block, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
else
|
||||
with {:ok, _type, context} <- of_expr(block, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -290,7 +306,7 @@ defmodule Module.Types.Expr do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do
|
||||
{:ok, _context} -> {:ok, :dynamic, context}
|
||||
{:ok, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
@@ -379,6 +395,13 @@ defmodule Module.Types.Expr do
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
|
||||
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause(list, stack, context) when is_list(list) do
|
||||
reduce_ok(list, context, &for_option(&1, stack, &2))
|
||||
end
|
||||
@@ -399,17 +422,6 @@ defmodule Module.Types.Expr do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
defp for_option({:do, [{:->, _, [pattern, body]}]}, stack, context) do
|
||||
case Pattern.of_pattern(pattern, stack, context) do
|
||||
{:ok, _pattern_type, context} -> of_expr_context(body, stack, context)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp for_option({:do, body}, stack, context) do
|
||||
of_expr_context(body, stack, context)
|
||||
end
|
||||
|
||||
defp with_clause({:<-, _, [left, expr]}, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
@@ -433,22 +445,23 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
defp with_option({:else, clauses}, stack, context) do
|
||||
case of_clauses(clauses, stack, context) do
|
||||
:ok -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
of_clauses(clauses, stack, context)
|
||||
end
|
||||
|
||||
defp of_clauses(clauses, stack, context) do
|
||||
each_ok(clauses, fn {:->, _meta, [head, body]} ->
|
||||
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context = acc ->
|
||||
{patterns, guards} = extract_head(head)
|
||||
|
||||
with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context),
|
||||
{:ok, _expr_type, _context} <- of_expr(body, stack, context),
|
||||
do: :ok
|
||||
{:ok, _expr_type, context} <- of_expr(body, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp keep_warnings(context, %{warnings: warnings}) do
|
||||
%{context | warnings: warnings}
|
||||
end
|
||||
|
||||
defp extract_head([{:when, _meta, args}]) do
|
||||
case Enum.split(args, -1) do
|
||||
{patterns, [guards]} -> {patterns, flatten_when(guards)}
|
||||
@@ -475,13 +488,6 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
end
|
||||
|
||||
defp of_expr_ok(expr, stack, context) do
|
||||
case of_expr(expr, stack, context) do
|
||||
{:ok, _type, _context} -> :ok
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
|
||||
{:dynamic, context}
|
||||
end
|
||||
|
||||
@@ -25,16 +25,6 @@ defmodule Module.Types.Helpers do
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | last_expr: expr}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
@@ -91,19 +81,6 @@ defmodule Module.Types.Helpers do
|
||||
|
||||
defp do_map_ok([], acc, _fun), do: {:ok, Enum.reverse(acc)}
|
||||
|
||||
@doc """
|
||||
Like `Enum.each/2` but only continues while `fun` returns `:ok`
|
||||
and stops on `{:error, reason}`.
|
||||
"""
|
||||
def each_ok([head | tail], fun) do
|
||||
case fun.(head) do
|
||||
:ok -> each_ok(tail, fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def each_ok([], _fun), do: :ok
|
||||
|
||||
@doc """
|
||||
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
|
||||
@@ -6,17 +6,27 @@ defmodule Module.Types.Of do
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
alias Module.Types.Infer
|
||||
alias Module.ParallelChecker
|
||||
|
||||
import Module.Types.Helpers
|
||||
import Module.Types.Unify
|
||||
|
||||
@doc """
|
||||
Handles open maps (with dynamic => dynamic).
|
||||
"""
|
||||
def open_map(args, stack, context, fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
|
||||
{:ok, {:map, pairs_to_unions(pairs, context) ++ [{:optional, :dynamic, :dynamic}]}, context}
|
||||
pairs =
|
||||
for {key, value} <- pairs, not has_unbound_var?(key, context) do
|
||||
if singleton?(key, context) do
|
||||
{key, value}
|
||||
else
|
||||
{key, to_union([value, :dynamic], context)}
|
||||
end
|
||||
end
|
||||
|
||||
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
|
||||
{:ok, {:map, triplets}, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,7 +35,7 @@ defmodule Module.Types.Of do
|
||||
"""
|
||||
def closed_map(args, stack, context, fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
|
||||
{:ok, {:map, pairs_to_unions(pairs, context)}, context}
|
||||
{:ok, {:map, closed_to_unions(pairs, context)}, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,10 +47,10 @@ defmodule Module.Types.Of do
|
||||
end)
|
||||
end
|
||||
|
||||
defp pairs_to_unions([{key, value}], _context), do: [{:required, key, value}]
|
||||
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
|
||||
|
||||
defp pairs_to_unions(pairs, context) do
|
||||
case Enum.split_with(pairs, fn {key, _value} -> Infer.has_unbound_var?(key, context) end) do
|
||||
defp closed_to_unions(pairs, context) do
|
||||
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
|
||||
{[], pairs} -> pairs_to_unions(pairs, [], context)
|
||||
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
|
||||
end
|
||||
@@ -57,17 +67,17 @@ defmodule Module.Types.Of do
|
||||
find_subtype_values(ahead, key, context) ++
|
||||
find_subtype_values(behind, key, context)
|
||||
|
||||
pairs_to_unions(ahead, [{key, Infer.to_union(all_values, context)} | behind], context)
|
||||
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
|
||||
end
|
||||
|
||||
defp pairs_to_unions([], acc, context) do
|
||||
acc
|
||||
|> Enum.sort(&Infer.subtype?(elem(&1, 0), elem(&2, 0), context))
|
||||
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|
||||
|> Enum.map(fn {key, value} -> {:required, key, value} end)
|
||||
end
|
||||
|
||||
defp find_subtype_values(pairs, key, context) do
|
||||
for {pair_key, pair_value} <- pairs, Infer.subtype?(pair_key, key, context), do: pair_value
|
||||
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
|
||||
end
|
||||
|
||||
defp find_matching_values([{key, value} | ahead], key, acc, values) do
|
||||
@@ -153,16 +163,11 @@ defmodule Module.Types.Of do
|
||||
|
||||
true ->
|
||||
with {:ok, type, context} <- fun.(expr, stack, context),
|
||||
{:ok, _type, context} <- Infer.unify(type, expected_type, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this clause once we properly handle comprehensions
|
||||
defp binary_segment({:<-, _, _}, _stack, context, _fun) do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
|
||||
@@ -173,7 +178,7 @@ defmodule Module.Types.Of do
|
||||
fun.(other)
|
||||
end
|
||||
|
||||
defp binary_type(:expr, {:float, _, _}), do: :number
|
||||
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
|
||||
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
|
||||
|
||||
@@ -2,7 +2,7 @@ defmodule Module.Types.Pattern do
|
||||
@moduledoc false
|
||||
|
||||
alias Module.Types.Of
|
||||
import Module.Types.{Helpers, Infer}
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
@doc """
|
||||
Handles patterns and guards at once.
|
||||
@@ -24,113 +24,14 @@ defmodule Module.Types.Pattern do
|
||||
of_pattern(pattern, %{stack | context: :pattern}, context)
|
||||
end
|
||||
|
||||
# :atom
|
||||
def of_pattern(atom, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
def of_pattern(literal, _stack, context) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
def of_pattern(literal, _stack, context) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
def of_pattern(literal, _stack, context) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_pattern({:<<>>, _meta, args}, stack, context) do
|
||||
result = Of.binary(args, stack, context, &of_pattern/3)
|
||||
|
||||
case result do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
def of_pattern({:|, _meta, [left_expr, []]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
of_pattern(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
def of_pattern({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
def of_pattern([], _stack, context) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_pattern(exprs, stack, context) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
def of_pattern(
|
||||
{{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr,
|
||||
stack,
|
||||
context
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pattern(left_expr, stack, context) do
|
||||
{:ok, {:list, left}, context} ->
|
||||
case of_pattern(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# _
|
||||
def of_pattern({:_, _meta, atom}, _stack, context) when is_atom(atom) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# ^var
|
||||
def of_pattern({:^, _meta, [var]}, stack, context) do
|
||||
of_pattern(var, stack, context)
|
||||
def of_pattern({:^, _meta, [var]}, _stack, context) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# var
|
||||
@@ -139,21 +40,6 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_pattern({left, right}, stack, context) do
|
||||
of_pattern({:{}, [], [left, right]}, stack, context)
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_pattern({:{}, _meta, exprs} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_pattern({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
@@ -163,23 +49,6 @@ defmodule Module.Types.Pattern do
|
||||
do: unify(left_type, right_type, stack, context)
|
||||
end
|
||||
|
||||
# %{...}
|
||||
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
Of.open_map(args, stack, context, &of_pattern/3)
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
# %_{...}
|
||||
def of_pattern(
|
||||
{:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]} = expr,
|
||||
@@ -194,22 +63,22 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
# %^var{...}
|
||||
def of_pattern({:%, meta1, [{:^, _meta2, [var]}, args]}, stack, context) do
|
||||
of_pattern({:%, meta1, [var, args]}, stack, context)
|
||||
end
|
||||
|
||||
# %var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do
|
||||
# %var{...} and %^var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when not is_atom(var) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {var_type, context} = new_var(var, context),
|
||||
with {:ok, var_type, context} = of_pattern(var, stack, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context),
|
||||
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
|
||||
end
|
||||
end
|
||||
|
||||
def of_pattern(expr, stack, context) do
|
||||
of_shared(expr, stack, context, &of_pattern/3)
|
||||
end
|
||||
|
||||
def unify_kinds(:required, _), do: :required
|
||||
def unify_kinds(_, :required), do: :required
|
||||
def unify_kinds(:optional, :optional), do: :optional
|
||||
@@ -217,43 +86,45 @@ defmodule Module.Types.Pattern do
|
||||
## GUARDS
|
||||
|
||||
# TODO: Some guards can be changed to intersection types or higher order types
|
||||
@boolean {:union, [{:atom, true}, {:atom, false}]}
|
||||
@number {:union, [:integer, :float]}
|
||||
|
||||
@guard_functions %{
|
||||
{:is_atom, 1} => {[:atom], :boolean},
|
||||
{:is_binary, 1} => {[:binary], :boolean},
|
||||
{:is_bitstring, 1} => {[:binary], :boolean},
|
||||
{:is_boolean, 1} => {[:boolean], :boolean},
|
||||
{:is_float, 1} => {[:float], :boolean},
|
||||
{:is_function, 1} => {[:fun], :boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], :boolean},
|
||||
{:is_integer, 1} => {[:integer], :boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
|
||||
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :boolean},
|
||||
{:is_atom, 1} => {[:atom], @boolean},
|
||||
{:is_binary, 1} => {[:binary], @boolean},
|
||||
{:is_bitstring, 1} => {[:binary], @boolean},
|
||||
{:is_boolean, 1} => {[@boolean], @boolean},
|
||||
{:is_float, 1} => {[:float], @boolean},
|
||||
{:is_function, 1} => {[:fun], @boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], @boolean},
|
||||
{:is_integer, 1} => {[:integer], @boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
|
||||
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
|
||||
{:is_number, 1} => {[:number], :boolean},
|
||||
{:is_pid, 1} => {[:pid], :boolean},
|
||||
{:is_port, 1} => {[:port], :boolean},
|
||||
{:is_reference, 1} => {[:reference], :boolean},
|
||||
{:is_tuple, 1} => {[:tuple], :boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:*, 2} => {[:number, :number], :number},
|
||||
{:+, 1} => {[:number], :number},
|
||||
{:+, 2} => {[:number, :number], :number},
|
||||
{:-, 1} => {[:number], :number},
|
||||
{:-, 2} => {[:number, :number], :number},
|
||||
{:/, 2} => {[:number, :number], :number},
|
||||
{:abs, 1} => {[:number], :number},
|
||||
{:ceil, 1} => {[:number], :integer},
|
||||
{:floor, 1} => {[:number], :integer},
|
||||
{:round, 1} => {[:number], :integer},
|
||||
{:trunc, 1} => {[:number], :integer},
|
||||
{:is_number, 1} => {[@number], @boolean},
|
||||
{:is_pid, 1} => {[:pid], @boolean},
|
||||
{:is_port, 1} => {[:port], @boolean},
|
||||
{:is_reference, 1} => {[:reference], @boolean},
|
||||
{:is_tuple, 1} => {[:tuple], @boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:*, 2} => {[@number, @number], @number},
|
||||
{:+, 1} => {[@number], @number},
|
||||
{:+, 2} => {[@number, @number], @number},
|
||||
{:-, 1} => {[@number], @number},
|
||||
{:-, 2} => {[@number, @number], @number},
|
||||
{:/, 2} => {[@number, @number], @number},
|
||||
{:abs, 1} => {[@number], @number},
|
||||
{:ceil, 1} => {[@number], :integer},
|
||||
{:floor, 1} => {[@number], :integer},
|
||||
{:round, 1} => {[@number], :integer},
|
||||
{:trunc, 1} => {[@number], :integer},
|
||||
{:element, 2} => {[:integer, :tuple], :dynamic},
|
||||
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:length, 1} => {[{:list, :dynamic}], :integer},
|
||||
@@ -265,7 +136,7 @@ defmodule Module.Types.Pattern do
|
||||
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
|
||||
{:bit_size, 1} => {[:binary], :integer},
|
||||
{:byte_size, 1} => {[:binary], :integer},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
|
||||
{:div, 2} => {[:integer, :integer], :integer},
|
||||
{:rem, 2} => {[:integer, :integer], :integer},
|
||||
{:node, 0} => {[], :atom},
|
||||
@@ -276,15 +147,15 @@ defmodule Module.Types.Pattern do
|
||||
{:bxor, 2} => {[:integer, :integer], :integer},
|
||||
{:bsl, 2} => {[:integer, :integer], :integer},
|
||||
{:bsr, 2} => {[:integer, :integer], :integer},
|
||||
{:or, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:and, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:xor, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:not, 1} => {[:boolean], :boolean}
|
||||
{:or, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:and, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:xor, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:not, 1} => {[@boolean], @boolean}
|
||||
|
||||
# Following guards are matched explicitly to handle
|
||||
# type guard functions such as is_atom/1
|
||||
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
|
||||
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
|
||||
}
|
||||
|
||||
@type_guards [
|
||||
@@ -320,9 +191,9 @@ defmodule Module.Types.Pattern do
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, @boolean, stack, context),
|
||||
do: {:ok, @boolean, context}
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
|
||||
@@ -332,8 +203,8 @@ defmodule Module.Types.Pattern do
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
do: {:ok, @boolean, context}
|
||||
end
|
||||
|
||||
# The unary operators + and - are special cased to avoid common warnings until
|
||||
@@ -404,10 +275,8 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# other literals
|
||||
def of_guard(expr, stack, context) do
|
||||
# Fall back to of_pattern/3 for literals
|
||||
of_pattern(expr, stack, context)
|
||||
of_shared(expr, stack, context, &of_guard/3)
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
@@ -553,4 +422,136 @@ defmodule Module.Types.Pattern do
|
||||
defp type_guard?(name) do
|
||||
name in @type_guards
|
||||
end
|
||||
|
||||
## Shared
|
||||
|
||||
# :atom
|
||||
defp of_shared(atom, _stack, context, _fun) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
defp of_shared(literal, _stack, context, _fun) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
defp of_shared(literal, _stack, context, _fun) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
defp of_shared(literal, _stack, context, _fun) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
defp of_shared({:<<>>, _meta, args}, stack, context, fun) do
|
||||
case Of.binary(args, stack, context, fun) do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
defp of_shared({:|, _meta, [left_expr, []]} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fun.(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
defp of_shared({:|, _meta, [left_expr, right_expr]} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case fun.(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case fun.(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
defp of_shared([], _stack, context, _fun) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
defp of_shared(exprs, stack, context, fun) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
defp of_shared(
|
||||
{{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr,
|
||||
stack,
|
||||
context,
|
||||
fun
|
||||
) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case fun.(left_expr, stack, context) do
|
||||
{:ok, {:list, left}, context} ->
|
||||
case fun.(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, {:list, to_union([left, right], context)}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
defp of_shared({left, right}, stack, context, fun) do
|
||||
of_shared({:{}, [], [left, right]}, stack, context, fun)
|
||||
end
|
||||
|
||||
# {...}
|
||||
defp of_shared({:{}, _meta, exprs} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# %{...}
|
||||
defp of_shared({:%{}, _meta, args} = expr, stack, context, fun) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
Of.open_map(args, stack, context, fun)
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context, fun)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, fun) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Module.Types.Infer do
|
||||
defmodule Module.Types.Unify do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
@@ -21,11 +21,6 @@ defmodule Module.Types.Infer do
|
||||
# {:union, [type]}
|
||||
# {:map, [{:required | :optional, key_type, value_type}]}
|
||||
#
|
||||
# TODO: Those types should be removed:
|
||||
#
|
||||
# :boolean
|
||||
# :number
|
||||
#
|
||||
# Once new types are added, they should be considered in:
|
||||
#
|
||||
# * unify (all)
|
||||
@@ -171,8 +166,11 @@ defmodule Module.Types.Infer do
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, var_type, context} ->
|
||||
context = refine_var(var, var_type, stack, context)
|
||||
{:ok, {:var, ^var}, context} ->
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:ok, res_type, context} ->
|
||||
context = refine_var(var, res_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -306,6 +304,23 @@ defmodule Module.Types.Infer do
|
||||
|
||||
defp error(type, reason, context), do: {:error, {type, reason, context}}
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | last_expr: expr}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a variable.
|
||||
"""
|
||||
def get_var!(var, context) do
|
||||
Map.fetch!(context.vars, var_name(var))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variable.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
@@ -487,14 +502,33 @@ defmodule Module.Types.Infer do
|
||||
|
||||
def has_unbound_var?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Returns true if it is a singleton type.
|
||||
|
||||
Only atoms are singleton types. Unbound vars are not
|
||||
considered singleton types.
|
||||
"""
|
||||
def singleton?({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> singleton?(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def singleton?({:atom, _}, _context), do: true
|
||||
def singleton?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
|
||||
This function assumes that:
|
||||
|
||||
* dynamic is not considered a subtype of all other types but the top type
|
||||
* unbound variables are not subtype of anything
|
||||
|
||||
* dynamic is not considered a subtype of all other types but the top type.
|
||||
This allows this function can be used for ordering, in other cases, you
|
||||
may need to check for both sides
|
||||
|
||||
"""
|
||||
def subtype?(type, type, _context), do: true
|
||||
|
||||
@@ -515,11 +549,6 @@ defmodule Module.Types.Infer do
|
||||
def subtype?(_, :dynamic, _context), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
|
||||
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
|
||||
def subtype?(:boolean, :atom, _context), do: true
|
||||
def subtype?(:float, :number, _context), do: true
|
||||
def subtype?(:integer, :number, _context), do: true
|
||||
|
||||
# Composite
|
||||
|
||||
def subtype?({:tuple, _, _}, :tuple, _context), do: true
|
||||
+4
-16
@@ -573,27 +573,15 @@ defmodule Path do
|
||||
@moduledoc false
|
||||
|
||||
def read_link_info(file) do
|
||||
call({:read_link_info, file})
|
||||
:file.read_link_info(file)
|
||||
end
|
||||
|
||||
def list_dir(dir) do
|
||||
case call({:list_dir, dir}) do
|
||||
{:ok, files} ->
|
||||
{:ok, for(file <- files, hd(file) != ?., do: file)}
|
||||
|
||||
other ->
|
||||
other
|
||||
case :file.list_dir(dir) do
|
||||
{:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, call: 1}
|
||||
|
||||
defp call(tuple) do
|
||||
x = :erlang.dt_spread_tag(true)
|
||||
y = :gen_server.call(:file_server_2, tuple)
|
||||
:erlang.dt_restore_tag(x)
|
||||
y
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -447,12 +447,12 @@ defmodule Record do
|
||||
end
|
||||
end
|
||||
|
||||
defp hoist_expressions(keyword, %{context: nil}) do
|
||||
defp hoist_expressions(keyword, %{context: nil, module: module}) do
|
||||
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
|
||||
if simple_argument?(expr) do
|
||||
{{key, expr}, acc}
|
||||
else
|
||||
var = Macro.var(key, __MODULE__)
|
||||
var = Macro.unique_var(key, module)
|
||||
{{key, var}, [{:=, [], [var, expr]} | acc]}
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -280,7 +280,7 @@ defmodule Regex do
|
||||
Returns `true` if the given `term` is a regex.
|
||||
Otherwise returns `false`.
|
||||
"""
|
||||
# TODO: Remove this on Elixir v1.15
|
||||
# TODO: deprecate permanently on Elixir v1.15
|
||||
@doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead"
|
||||
def regex?(term)
|
||||
def regex?(%Regex{}), do: true
|
||||
|
||||
@@ -600,9 +600,17 @@ defmodule System do
|
||||
"""
|
||||
# TODO: Once Erlang/OTP 23 is required, remove conditional, and update @doc accordingly.
|
||||
# The warning is emitted by the compiler - so a @doc annotation is enough
|
||||
# Note Elixir may be compiled in an earlier Erlang version but runs on a
|
||||
# newer one, so we need the check at compilation time and runtime.
|
||||
@doc deprecated: "Use __STACKTRACE__ instead"
|
||||
if function_exported?(:erlang, :get_stacktrace, 0) do
|
||||
def stacktrace, do: apply(:erlang, :get_stacktrace, [])
|
||||
def stacktrace do
|
||||
if function_exported?(:erlang, :get_stacktrace, 0) do
|
||||
apply(:erlang, :get_stacktrace, [])
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
else
|
||||
def stacktrace, do: []
|
||||
end
|
||||
|
||||
@@ -491,6 +491,38 @@ defmodule Task do
|
||||
stream = Task.async_stream(collection, Mod, :expensive_fun, [], ordered: false)
|
||||
Stream.run(stream)
|
||||
|
||||
## Attention: async + take
|
||||
|
||||
Given items in an async stream are processed concurrently, doing
|
||||
`async_stream` followed by `Enum.take/2` may cause more items than
|
||||
requested to be processed. Let's see an example:
|
||||
|
||||
1..100
|
||||
|> Task.async_stream(fn i ->
|
||||
Process.sleep(100)
|
||||
IO.puts(to_string(i))
|
||||
end)
|
||||
|> Enum.take(10)
|
||||
|
||||
For a machine with 8 cores, the above will process 16 items instead
|
||||
of 10. The reason is that `async_stream/5` always have 8 elements
|
||||
processing at once. So by the time `Enum` says it got all elements
|
||||
it needed, there are still 6 elements left to be processed.
|
||||
|
||||
The solution here is to use `Stream.take/2` instead of `Enum.take/2`
|
||||
to filter elements before-hand:
|
||||
|
||||
1..100
|
||||
|> Stream.take(10)
|
||||
|> Task.async_stream(fn i ->
|
||||
Process.sleep(100)
|
||||
IO.puts(to_string(i))
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
|
||||
If for some reason you cannot take the elements before hand,
|
||||
you can use `:max_concurrency` to limit how many elements
|
||||
may be over processed at the cost of reducing concurrency.
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
|
||||
|
||||
@@ -174,4 +174,4 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.11]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -127,11 +127,18 @@ Conveniently, Elixir allows developers to hide modules and functions from the do
|
||||
end
|
||||
end
|
||||
|
||||
However, keep in mind that adding `@doc false` does not make the function private. The function above can still be invoked as `MyApp.Sample.add(1, 2)`. Not only that, if `MyApp.Sample` is imported, the `add/2` function will also be imported into the caller. For those reasons, be cautious when adding `@doc false` to functions, instead use one of these two options:
|
||||
In case you don't want to hide a whole module, you can hide functions individually:
|
||||
|
||||
defmodule MyApp.Sample do
|
||||
@doc false
|
||||
def add(a, b), do: a + b
|
||||
end
|
||||
|
||||
However, keep in mind `@moduledoc false` or `@doc false` do not make a function private. The function above can still be invoked as `MyApp.Sample.add(1, 2)`. Not only that, if `MyApp.Sample` is imported, the `add/2` function will also be imported into the caller. For those reasons, be cautious when adding `@doc false` to functions, instead use one of these two options:
|
||||
|
||||
* Move the undocumented function to a module with `@moduledoc false`, like `MyApp.Hidden`, ensuring the function won't be accidentally exposed or imported. Remember you can use `@moduledoc false` to hide a whole module and still document each function with `@doc`. Tools will still ignore the module.
|
||||
|
||||
* Start the function name with one or two underscores, for example, `__add__/2`, and add `@doc false`. The compiler does not import functions with leading underscores and they hint to anyone reading the code of their intended private usage.
|
||||
* Start the function name with one or two underscores, for example, `__add__/2`. Functions starting with underscore are autoamtically treated as hidden, although you can also be explicit and add `@doc false`. The compiler does not import functions with leading underscores and they hint to anyone reading the code of their intended private usage.
|
||||
|
||||
## Code.fetch_docs/1
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Generate docs_config.js for version chooser in ExDoc
|
||||
[app] = System.argv()
|
||||
|
||||
{text_tags, 0} = System.cmd("git", ["tag"])
|
||||
skipped = Version.parse!("1.0.3")
|
||||
|
||||
list_contents =
|
||||
text_tags
|
||||
|> String.split()
|
||||
|> Enum.map(fn "v" <> rest -> Version.parse!(rest) end)
|
||||
|> Enum.filter(&(Version.compare(&1, skipped) == :gt))
|
||||
|> Enum.sort({:desc, Version})
|
||||
|> Enum.map_intersperse(", ", fn version ->
|
||||
version_string = to_string(version)
|
||||
~s[{"version":"v#{version_string}", "url":"https://hexdocs.pm/#{app}/#{version_string}"}]
|
||||
end)
|
||||
|
||||
File.mkdir_p!("doc/#{app}")
|
||||
File.write!("doc/#{app}/docs_config.js", ["var versionNodes = [", list_contents, "];\n"])
|
||||
@@ -0,0 +1,12 @@
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
|
||||
[
|
||||
deps: [
|
||||
eex: "https://hexdocs.pm/eex/#{canonical}",
|
||||
elixir: "https://hexdocs.pm/elixir/#{canonical}",
|
||||
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
|
||||
iex: "https://hexdocs.pm/iex/#{canonical}",
|
||||
logger: "https://hexdocs.pm/logger/#{canonical}",
|
||||
mix: "https://hexdocs.pm/mix/#{canonical}"
|
||||
]
|
||||
]
|
||||
@@ -204,21 +204,23 @@ env_for_eval(Env, Opts) ->
|
||||
false -> nil
|
||||
end,
|
||||
|
||||
Tracers = case lists:keyfind(tracers, 1, Opts) of
|
||||
TempTracers = case lists:keyfind(tracers, 1, Opts) of
|
||||
{tracers, TracersOpt} when is_list(TracersOpt) -> TracersOpt;
|
||||
false -> []
|
||||
end,
|
||||
|
||||
LexicalTracker = case lists:keyfind(lexical_tracker, 1, Opts) of
|
||||
%% If there is a dead PID or lexical tracker is nil,
|
||||
%% we assume the tracers also cannot be (re)used.
|
||||
{LexicalTracker, Tracers} = case lists:keyfind(lexical_tracker, 1, Opts) of
|
||||
{lexical_tracker, Pid} when is_pid(Pid) ->
|
||||
case is_process_alive(Pid) of
|
||||
true -> Pid;
|
||||
false -> nil
|
||||
true -> {Pid, TempTracers};
|
||||
false -> {nil, []}
|
||||
end;
|
||||
{lexical_tracker, nil} ->
|
||||
nil;
|
||||
{nil, []};
|
||||
false ->
|
||||
nil
|
||||
{nil, TempTracers}
|
||||
end,
|
||||
|
||||
FA = case lists:keyfind(function, 1, Opts) of
|
||||
|
||||
@@ -319,8 +319,13 @@ build_spec(Meta, _Size, Unit, Type, _Endianness, Sign, Spec, E) when Type == bin
|
||||
build_spec(Meta, Size, Unit, Type, Endianness, Sign, Spec, E) when Type == integer; Type == float ->
|
||||
NumberSize = number_size(Size, Unit),
|
||||
if
|
||||
Type == float, is_integer(NumberSize), NumberSize /= 32, NumberSize /= 64 ->
|
||||
form_error(Meta, E, ?MODULE, {bittype_float_size, NumberSize});
|
||||
Type == float, is_integer(NumberSize) ->
|
||||
case valid_float_size(NumberSize) of
|
||||
true ->
|
||||
add_spec(Type, add_spec(Endianness, add_spec(Sign, Spec)));
|
||||
false ->
|
||||
form_error(Meta, E, ?MODULE, {bittype_float_size, NumberSize})
|
||||
end;
|
||||
Size == default, Unit /= default ->
|
||||
form_error(Meta, E, ?MODULE, bittype_unit);
|
||||
true ->
|
||||
@@ -331,6 +336,12 @@ number_size(Size, default) when is_integer(Size) -> Size;
|
||||
number_size(Size, Unit) when is_integer(Size) -> Size * Unit;
|
||||
number_size(Size, _) -> Size.
|
||||
|
||||
%% TODO: Simplify when we require OTP 24
|
||||
valid_float_size(16) -> erlang:system_info(otp_release) >= "24";
|
||||
valid_float_size(32) -> true;
|
||||
valid_float_size(64) -> true;
|
||||
valid_float_size(_) -> false.
|
||||
|
||||
add_spec(default, Spec) -> Spec;
|
||||
add_spec(Key, Spec) -> [{Key, [], []} | Spec].
|
||||
|
||||
@@ -372,7 +383,12 @@ format_error(bittype_signed) ->
|
||||
format_error(bittype_unit) ->
|
||||
"integer and float types require a size specifier if the unit specifier is given";
|
||||
format_error({bittype_float_size, Other}) ->
|
||||
io_lib:format("float requires size*unit to be 32 or 64 (default), got: ~p", [Other]);
|
||||
Message =
|
||||
case erlang:system_info(otp_release) >= "24" of
|
||||
true -> "16, 32, or 64";
|
||||
false -> "32 or 64"
|
||||
end,
|
||||
io_lib:format("float requires size*unit to be ~s (default), got: ~p", [Message, Other]);
|
||||
format_error({invalid_literal, Literal}) ->
|
||||
io_lib:format("invalid literal ~ts in <<>>", ['Elixir.Macro':to_string(Literal)]);
|
||||
format_error({undefined_bittype, Expr}) ->
|
||||
|
||||
@@ -166,8 +166,8 @@ bootstrap_files() ->
|
||||
<<"lib/elixir/lib/module/locals_tracker.ex">>,
|
||||
<<"lib/elixir/lib/module/parallel_checker.ex">>,
|
||||
<<"lib/elixir/lib/module/types/helpers.ex">>,
|
||||
<<"lib/elixir/lib/module/types/unify.ex">>,
|
||||
<<"lib/elixir/lib/module/types/of.ex">>,
|
||||
<<"lib/elixir/lib/module/types/infer.ex">>,
|
||||
<<"lib/elixir/lib/module/types/pattern.ex">>,
|
||||
<<"lib/elixir/lib/module/types/expr.ex">>,
|
||||
<<"lib/elixir/lib/module/types.ex">>,
|
||||
|
||||
@@ -94,6 +94,8 @@ handle_file_warning(true, _File, {_Line, sys_core_fold, {nomatch_shadow, _}}) ->
|
||||
|
||||
%% Those we implement ourselves
|
||||
handle_file_warning(_, _File, {_Line, v3_core, {map_key_repeated, _}}) -> ok;
|
||||
handle_file_warning(_, _File, {_Line, sys_core_fold, {ignored, useless_building}}) -> ok;
|
||||
%% TODO: remove when we require Erlang/OTP 24
|
||||
handle_file_warning(_, _File, {_Line, sys_core_fold, useless_building}) -> ok;
|
||||
|
||||
%% Ignore all linting errors (only come up on parse transforms)
|
||||
@@ -112,11 +114,11 @@ handle_file_error(File, {Line, Module, Desc}) ->
|
||||
elixir_errors:compile_error([{line, Line}], File, Message).
|
||||
|
||||
%% Mention the capture operator in make_fun
|
||||
custom_format(sys_core_fold, {no_effect, {erlang, make_fun, 3}}) ->
|
||||
custom_format(sys_core_fold, {ignored, {no_effect, {erlang, make_fun, 3}}}) ->
|
||||
"the result of the capture operator & (:erlang.make_fun/3) is never used";
|
||||
|
||||
%% Make no_effect clauses pretty
|
||||
custom_format(sys_core_fold, {no_effect, {erlang, F, A}}) ->
|
||||
custom_format(sys_core_fold, {ignored, {no_effect, {erlang, F, A}}}) ->
|
||||
{Fmt, Args} = case erl_internal:comp_op(F, A) of
|
||||
true -> {"use of operator ~ts has no effect", [elixir_utils:erlang_comparison_op_to_elixir(F)]};
|
||||
false ->
|
||||
@@ -127,16 +129,11 @@ custom_format(sys_core_fold, {no_effect, {erlang, F, A}}) ->
|
||||
end,
|
||||
io_lib:format(Fmt, Args);
|
||||
|
||||
%% Rewrite nomatch_guard to be more generic it can happen inside if, unless, and the like
|
||||
custom_format(sys_core_fold, nomatch_guard) ->
|
||||
%% Rewrite nomatch to be more generic, it can happen inside if, unless, and the like
|
||||
custom_format(sys_core_fold, {nomatch, X}) when X == guard; X == no_clause ->
|
||||
"this check/guard will always yield the same result";
|
||||
|
||||
%% Handle literal eval failures
|
||||
custom_format(sys_core_fold, {eval_failure, Error}) ->
|
||||
#{'__struct__' := Struct} = 'Elixir.Exception':normalize(error, Error),
|
||||
["this expression will fail with ", elixir_aliases:inspect(Struct)];
|
||||
|
||||
custom_format(sys_core_fold, {nomatch_shadow,Line,{ErlName,ErlArity}}) ->
|
||||
custom_format(sys_core_fold, {nomatch, {shadow, Line, {ErlName, ErlArity}}}) ->
|
||||
{Name, Arity} = elixir_utils:erl_fa_to_elixir_fa(ErlName, ErlArity),
|
||||
|
||||
io_lib:format(
|
||||
@@ -144,6 +141,28 @@ custom_format(sys_core_fold, {nomatch_shadow,Line,{ErlName,ErlArity}}) ->
|
||||
[Name, Arity, Line]
|
||||
);
|
||||
|
||||
%% Handle literal eval failures
|
||||
custom_format(sys_core_fold, {failed, {eval_failure, {Mod, Name, Arity}, Error}}) ->
|
||||
#{'__struct__' := Struct} = 'Elixir.Exception':normalize(error, Error),
|
||||
{ExMod, ExName, ExArgs} = elixir_rewrite:erl_to_ex(Mod, Name, lists:duplicate(Arity, nil)),
|
||||
Call = 'Elixir.Exception':format_mfa(ExMod, ExName, length(ExArgs)),
|
||||
Trimmed = case Call of
|
||||
<<"Kernel.", Rest/binary>> -> Rest;
|
||||
_ -> Call
|
||||
end,
|
||||
["the call to ", Trimmed, " will fail with ", elixir_aliases:inspect(Struct)];
|
||||
|
||||
%% TODO: remove when we require OTP 24
|
||||
custom_format(sys_core_fold, {nomatch_shadow, Line, FA}) ->
|
||||
custom_format(sys_core_fold, {nomatch, {shadow, Line, FA}});
|
||||
custom_format(sys_core_fold, nomatch_guard) ->
|
||||
custom_format(sys_core_fold, {nomatch, guard});
|
||||
custom_format(sys_core_fold, {no_effect, X}) ->
|
||||
custom_format(sys_core_fold, {ignored, {no_effect, X}});
|
||||
custom_format(sys_core_fold, {eval_failure, Error}) ->
|
||||
#{'__struct__' := Struct} = 'Elixir.Exception':normalize(error, Error),
|
||||
["this expression will fail with ", elixir_aliases:inspect(Struct)];
|
||||
|
||||
custom_format([], Desc) ->
|
||||
io_lib:format("~p", [Desc]);
|
||||
|
||||
|
||||
@@ -469,7 +469,7 @@ expand(Other, E) ->
|
||||
escape_env_entries(Meta, #{current_vars := {Read, Write}, unused_vars := {Unused, Version}} = Env0) ->
|
||||
Env1 = case Env0 of
|
||||
#{function := nil} -> Env0;
|
||||
_ -> Env0#{lexical_tracker := nil}
|
||||
_ -> Env0#{lexical_tracker := nil, tracers := []}
|
||||
end,
|
||||
Current = {maybe_escape_map(Read), maybe_escape_map(Write)},
|
||||
Env2 = Env1#{current_vars := Current, unused_vars := {maybe_escape_map(Unused), Version}},
|
||||
@@ -631,7 +631,13 @@ maybe_warn_underscored_var_access(Meta, Name, Kind, E) ->
|
||||
maybe_warn_deprecated_super_in_gen_server_callback(Meta, Function, SuperMeta, E) ->
|
||||
case lists:keyfind(context, 1, SuperMeta) of
|
||||
{context, 'Elixir.GenServer'} ->
|
||||
elixir_errors:form_warn(Meta, E, ?MODULE, {super_in_genserver, Function});
|
||||
case Function of
|
||||
{child_spec, 1} ->
|
||||
ok;
|
||||
|
||||
_ ->
|
||||
elixir_errors:form_warn(Meta, E, ?MODULE, {super_in_genserver, Function})
|
||||
end;
|
||||
|
||||
_ ->
|
||||
ok
|
||||
|
||||
@@ -139,9 +139,9 @@ compile(Line, Module, Block, Vars, E) ->
|
||||
},
|
||||
|
||||
Binary = elixir_erl:compile(ModuleMap),
|
||||
warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes),
|
||||
autoload_module(Module, Binary, CompileOpts, NE),
|
||||
eval_callbacks(Line, DataBag, after_compile, [NE, Binary], NE),
|
||||
warn_unused_attributes(File, DataSet, DataBag, PersistedAttributes),
|
||||
make_module_available(Module, Binary, ModuleMap),
|
||||
{module, Module, Binary, Result}
|
||||
catch
|
||||
|
||||
@@ -327,6 +327,10 @@ guard_rewrite(Receiver, DotMeta, Right, Meta, Args) ->
|
||||
_ -> {error, {invalid_guard, Receiver, Right, length(Args)}}
|
||||
end.
|
||||
|
||||
%% erlang:is_record/2-3 are compiler guards in Erlang which we
|
||||
%% need to explicitly forbid as they are allowed in erl_internal.
|
||||
allowed_guard(is_record, 2) -> false;
|
||||
allowed_guard(is_record, 3) -> false;
|
||||
allowed_guard(Right, Arity) ->
|
||||
erl_internal:guard_bif(Right, Arity) orelse elixir_utils:guard_op(Right, Arity).
|
||||
|
||||
|
||||
@@ -477,8 +477,8 @@ tokenize([$:, H | T] = Original, Line, Column, Scope, Tokens) when ?is_quote(H)
|
||||
|
||||
tokenize([$: | String] = Original, Line, Column, Scope, Tokens) ->
|
||||
case tokenize_identifier(String, Line, Column, Scope, false) of
|
||||
{_Kind, Atom, Rest, Length, _Ascii, _Special} ->
|
||||
NewScope = maybe_warn_for_ambiguous_bang_before_equals(atom, Atom, Rest, Scope, Line),
|
||||
{_Kind, Unencoded, Atom, Rest, Length, _Ascii, _Special} ->
|
||||
NewScope = maybe_warn_for_ambiguous_bang_before_equals(atom, Unencoded, Rest, Scope, Line),
|
||||
Token = {atom, {Line, Column, nil}, Atom},
|
||||
tokenize(Rest, Line, Column + 1 + Length, NewScope, [Token | Tokens]);
|
||||
empty ->
|
||||
@@ -559,7 +559,7 @@ tokenize([$. | T], Line, Column, Scope, Tokens) ->
|
||||
|
||||
tokenize(String, Line, Column, Scope, Tokens) ->
|
||||
case tokenize_identifier(String, Line, Column, Scope, not previous_was_dot(Tokens)) of
|
||||
{Kind, Atom, Rest, Length, Ascii, Special} ->
|
||||
{Kind, Unencoded, Atom, Rest, Length, Ascii, Special} ->
|
||||
HasAt = lists:member($@, Special),
|
||||
|
||||
case Rest of
|
||||
@@ -583,7 +583,7 @@ tokenize(String, Line, Column, Scope, Tokens) ->
|
||||
tokenize_alias(Rest, Line, Column, Atom, Length, Ascii, Special, Scope, Tokens);
|
||||
|
||||
_ when Kind == identifier ->
|
||||
NewScope = maybe_warn_for_ambiguous_bang_before_equals(identifier, Atom, Rest, Scope, Line),
|
||||
NewScope = maybe_warn_for_ambiguous_bang_before_equals(identifier, Unencoded, Rest, Scope, Line),
|
||||
Token = check_call_identifier(Line, Column, Atom, Rest),
|
||||
tokenize(Rest, Line, Column + Length, NewScope, [Token | Tokens]);
|
||||
|
||||
@@ -1142,7 +1142,7 @@ tokenize_identifier(String, Line, Column, Scope, MaybeKeyword) ->
|
||||
{keyword, Atom, Type} ->
|
||||
{keyword, Atom, Type, Rest, Length};
|
||||
{ok, Atom} ->
|
||||
{Kind, Atom, Rest, Length, Ascii, Special};
|
||||
{Kind, Acc, Atom, Rest, Length, Ascii, Special};
|
||||
{error, _Reason} = Error ->
|
||||
Error
|
||||
end;
|
||||
@@ -1462,11 +1462,11 @@ maybe_warn_too_many_of_same_char(_Token, _Rest, _Line, Scope) ->
|
||||
Scope.
|
||||
|
||||
%% TODO: Turn into an error on v2.0
|
||||
maybe_warn_for_ambiguous_bang_before_equals(Kind, Atom, [$= | _], Scope, Line) ->
|
||||
maybe_warn_for_ambiguous_bang_before_equals(Kind, Unencoded, [$= | _], Scope, Line) ->
|
||||
{What, Identifier} =
|
||||
case Kind of
|
||||
atom -> {"atom", [$: | atom_to_list(Atom)]};
|
||||
identifier -> {"identifier", atom_to_list(Atom)}
|
||||
atom -> {"atom", [$: | Unencoded]};
|
||||
identifier -> {"identifier", Unencoded}
|
||||
end,
|
||||
|
||||
case lists:last(Identifier) of
|
||||
|
||||
@@ -27,9 +27,9 @@ guard_op('orelse', 2) ->
|
||||
guard_op(Op, Arity) ->
|
||||
try erl_internal:op_type(Op, Arity) of
|
||||
arith -> true;
|
||||
list -> true;
|
||||
comp -> true;
|
||||
bool -> true;
|
||||
list -> false;
|
||||
send -> false
|
||||
catch
|
||||
_:_ -> false
|
||||
|
||||
@@ -85,11 +85,21 @@ defmodule CodeTest do
|
||||
assert CodeTest.Sample.eval_quoted_info() == {CodeTest.Sample, "sample.ex", 13}
|
||||
end
|
||||
|
||||
test "eval_quoted/2 with a %Macro.Env{} struct as the second argument" do
|
||||
test "eval_quoted/2 with %Macro.Env{} at runtime" do
|
||||
alias :lists, as: MyList
|
||||
quoted = quote(do: MyList.flatten([[1, 2, 3]]))
|
||||
|
||||
assert Code.eval_quoted(quote(do: MyList.flatten([[1, 2, 3]])), [], __ENV__) ==
|
||||
{[1, 2, 3], []}
|
||||
assert Code.eval_quoted(quoted, [], __ENV__) == {[1, 2, 3], []}
|
||||
|
||||
# Let's check it discards tracers since the lexical tracker is explicitly nil
|
||||
assert Code.eval_quoted(quoted, [], %{__ENV__ | tracers: [:bad]}) == {[1, 2, 3], []}
|
||||
end
|
||||
|
||||
test "eval_quoted/2 with %Macro.Env{} at compile time" do
|
||||
defmodule CompileTimeEnv do
|
||||
alias String.Chars
|
||||
{"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__)
|
||||
end
|
||||
end
|
||||
|
||||
test "eval_file/1" do
|
||||
@@ -214,6 +224,14 @@ defmodule CodeTest do
|
||||
Code.string_to_quoted("there_is_no_such_var", static_atoms_encoder: encoder)
|
||||
end
|
||||
|
||||
test "static_atoms_encoder ambiguities" do
|
||||
encoder = fn string, _meta -> {:ok, {:atom, string}} end
|
||||
|
||||
# We check a=1 for precedence issues with a!=1, make sure it works
|
||||
assert Code.string_to_quoted!("a = 1", static_atoms_encoder: encoder)
|
||||
assert Code.string_to_quoted!("a=1", static_atoms_encoder: encoder)
|
||||
end
|
||||
|
||||
test "static_atoms_encoder does not encode keywords" do
|
||||
encoder = fn atom, _meta -> raise "shouldn't be invoked for #{atom}" end
|
||||
|
||||
|
||||
@@ -116,6 +116,17 @@ defmodule EnumTest do
|
||||
[[0], [1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
|
||||
|
||||
assert Enum.chunk_while([5, 7, 9, 11], [], chunk_fun, after_fun) == [[5, 7, 9]]
|
||||
|
||||
assert Enum.chunk_while([1, 2, 3, 5, 7], [], chunk_fun, after_fun) == [[1, 2], [3, 5, 7]]
|
||||
|
||||
chunk_fn2 = fn
|
||||
-1, acc -> {:cont, acc, 0}
|
||||
i, acc -> {:cont, acc + i}
|
||||
end
|
||||
|
||||
after_fn2 = fn acc -> {:cont, acc, 0} end
|
||||
|
||||
assert Enum.chunk_while([1, -1, 2, 3, -1, 4, 5, 6], 0, chunk_fn2, after_fn2) == [1, 5, 15]
|
||||
end
|
||||
|
||||
test "concat/1" do
|
||||
|
||||
@@ -446,11 +446,12 @@ defmodule ExceptionTest do
|
||||
* min/1
|
||||
"""
|
||||
|
||||
assert blame_message(:erlang, & &1.gt_cookie()) == """
|
||||
function :erlang.gt_cookie/0 is undefined or private. Did you mean one of:
|
||||
assert blame_message(:erlang, & &1.hal()) == """
|
||||
function :erlang.hal/0 is undefined or private. Did you mean one of:
|
||||
|
||||
* get_cookie/0
|
||||
* set_cookie/2
|
||||
* halt/0
|
||||
* halt/1
|
||||
* halt/2
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -192,6 +192,11 @@ defmodule IO.ANSI.DocsTest do
|
||||
assert result == " • one\n • two\n • three\n\e[0m"
|
||||
end
|
||||
|
||||
test "* list is converted without ansi" do
|
||||
result = format_markdown("* one\n* two\n* three\n", enabled: false)
|
||||
assert result == " * one\n * two\n * three"
|
||||
end
|
||||
|
||||
test "* list surrounded by text is converted" do
|
||||
result = format_markdown("Count:\n\n* one\n* two\n* three\n\nDone")
|
||||
assert result == "Count:\n\e[0m\n • one\n • two\n • three\n\e[0m\nDone\n\e[0m"
|
||||
@@ -727,6 +732,8 @@ defmodule IO.ANSI.DocsTest do
|
||||
end
|
||||
|
||||
test "typespecs" do
|
||||
assert format_erlang([{:ul, [class: "types"], [{:li, [], []}]}]) == ""
|
||||
|
||||
assert format_erlang([{:ul, [class: "types"], [{:li, [], ["Hello"]}, {:li, [], ["World"]}]}]) ==
|
||||
"""
|
||||
Typespecs:
|
||||
@@ -735,6 +742,17 @@ defmodule IO.ANSI.DocsTest do
|
||||
World
|
||||
|
||||
"""
|
||||
|
||||
assert format_erlang([
|
||||
{:ul, [class: "types"], [{:li, [], ["Hello", {:code, [], ["World"]}]}]}
|
||||
]) ==
|
||||
"""
|
||||
Typespecs:
|
||||
|
||||
Hello
|
||||
\e[36mWorld\e[0m
|
||||
|
||||
"""
|
||||
end
|
||||
|
||||
test "extra markup" do
|
||||
|
||||
@@ -240,6 +240,19 @@ defmodule Kernel.ComprehensionTest do
|
||||
assert acc == %{1 => 'olleh', 3 => 'olleh'}
|
||||
end
|
||||
|
||||
test "for comprehensions with matched reduce" do
|
||||
acc =
|
||||
for entry <- [1, 2, 3], reduce: {:ok, nil} do
|
||||
{:ok, _} ->
|
||||
{:ok, entry}
|
||||
|
||||
{:error, _} = error ->
|
||||
error
|
||||
end
|
||||
|
||||
assert acc == {:ok, 3}
|
||||
end
|
||||
|
||||
## List generators (inlined by the compiler)
|
||||
|
||||
test "list for comprehensions" do
|
||||
|
||||
@@ -692,16 +692,6 @@ defmodule Kernel.ErrorsTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "match attribute in module" do
|
||||
msg = "invalid write attribute syntax, you probably meant to use: @foo expression"
|
||||
|
||||
assert_raise ArgumentError, msg, fn ->
|
||||
defmodule MatchAttributeInModule do
|
||||
@foo = 42
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "invalid case clauses" do
|
||||
assert_eval_raise CompileError,
|
||||
"nofile:1: expected one argument for :do clauses (->) in \"case\"",
|
||||
@@ -828,7 +818,7 @@ defmodule Kernel.ErrorsTest do
|
||||
"nofile:3: function exit/1 imported from both :erlang and Kernel, call is ambiguous",
|
||||
'''
|
||||
defmodule Kernel.ErrorsTest.FunctionImportConflict do
|
||||
import :erlang, warn: false
|
||||
import :erlang, only: [exit: 1], warn: false
|
||||
def foo, do: exit(:test)
|
||||
end
|
||||
'''
|
||||
@@ -1202,7 +1192,7 @@ defmodule Kernel.ErrorsTest do
|
||||
rescue
|
||||
ArgumentError ->
|
||||
assert [
|
||||
{:erlang, :apply, [1, :foo, []], []},
|
||||
{:erlang, :apply, [1, :foo, []], _},
|
||||
{__MODULE__, :bad_remote_call, 1, [file: _, line: _]} | _
|
||||
] = __STACKTRACE__
|
||||
end
|
||||
|
||||
@@ -294,6 +294,7 @@ defmodule Kernel.ExpansionTest do
|
||||
test "__ENV__" do
|
||||
env = %{__ENV__ | line: 0}
|
||||
assert expand_env(quote(do: __ENV__), env) == {Macro.escape(env), env}
|
||||
assert %{lexical_tracker: nil, tracers: []} = __ENV__
|
||||
end
|
||||
|
||||
test "__ENV__.accessor" do
|
||||
@@ -302,6 +303,9 @@ defmodule Kernel.ExpansionTest do
|
||||
|
||||
assert expand_env(quote(do: __ENV__.unknown), env) ==
|
||||
{quote(do: unquote(Macro.escape(env)).unknown), env}
|
||||
|
||||
assert __ENV__.lexical_tracker == nil
|
||||
assert __ENV__.tracers == []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2383,17 +2387,49 @@ defmodule Kernel.ExpansionTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "raises for invalid size * unit for floats" do
|
||||
message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 128"
|
||||
# TODO: Simplify when we require OTP 24
|
||||
if System.otp_release() >= "24" do
|
||||
test "16-bit floats" do
|
||||
import Kernel, except: [-: 2]
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::32*4>>))
|
||||
assert expand(quote(do: <<12.3::float-16>>)) |> clean_meta([:alignment]) ==
|
||||
quote(do: <<12.3::float()-size(16)>>)
|
||||
end
|
||||
|
||||
message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 256"
|
||||
test "raises for invalid size * unit for floats" do
|
||||
message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 128"
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::256>>))
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::32*4>>))
|
||||
end
|
||||
|
||||
message = ~r"float requires size\*unit to be 16, 32, or 64 \(default\), got: 256"
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::256>>))
|
||||
end
|
||||
end
|
||||
else
|
||||
test "16-bit floats" do
|
||||
message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 16"
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::16>>))
|
||||
end
|
||||
end
|
||||
|
||||
test "raises for invalid size * unit for floats" do
|
||||
message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 128"
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::32*4>>))
|
||||
end
|
||||
|
||||
message = ~r"float requires size\*unit to be 32 or 64 \(default\), got: 256"
|
||||
|
||||
assert_raise CompileError, message, fn ->
|
||||
expand(quote(do: <<12.3::256>>))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -310,6 +310,38 @@ defmodule Kernel.GuardTest do
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise CompileError,
|
||||
~r"cannot invoke remote function :erlang\.is_record/2 inside guards",
|
||||
fn ->
|
||||
defmodule IsRecord2Usage do
|
||||
defguard foo(rec) when :erlang.is_record(rec, :tag)
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise CompileError,
|
||||
~r"cannot invoke remote function :erlang\.is_record/3 inside guards",
|
||||
fn ->
|
||||
defmodule IsRecord3Usage do
|
||||
defguard foo(rec) when :erlang.is_record(rec, :tag, 7)
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise CompileError,
|
||||
~r"cannot invoke remote function :erlang\.\+\+/2 inside guards",
|
||||
fn ->
|
||||
defmodule ListSubtractionUsage do
|
||||
defguard foo(list) when list ++ []
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise CompileError,
|
||||
~r"cannot invoke remote function :erlang\.\-\-/2 inside guards",
|
||||
fn ->
|
||||
defmodule ListSubtractionUsage do
|
||||
defguard foo(list) when list -- []
|
||||
end
|
||||
end
|
||||
|
||||
assert_raise CompileError, ~r"invalid expression in guard", fn ->
|
||||
defmodule LocalCallUsage do
|
||||
defguard foo(local, call) when local.(call)
|
||||
|
||||
@@ -931,26 +931,33 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
# TODO: Simplify when we require OTP 24
|
||||
if System.otp_release() >= "24" do
|
||||
@argument_error_message "the call to :erlang.atom_to_binary/2"
|
||||
@arithmetic_error_message "the call to +/2"
|
||||
else
|
||||
@argument_error_message "this expression"
|
||||
@arithmetic_error_message "this expression"
|
||||
end
|
||||
|
||||
test "eval failure warning" do
|
||||
assert capture_err(fn ->
|
||||
assert_raise ArgumentError, fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
Atom.to_string "abc"
|
||||
end
|
||||
""")
|
||||
Code.eval_string("""
|
||||
defmodule Sample1 do
|
||||
def foo, do: Atom.to_string "abc"
|
||||
end
|
||||
end) =~ ~r"this expression will fail with ArgumentError\n.*nofile:2"
|
||||
""")
|
||||
end) =~ "#{@argument_error_message} will fail with ArgumentError\n nofile:2"
|
||||
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
defmodule Sample2 do
|
||||
def foo, do: 1 + nil
|
||||
end
|
||||
""")
|
||||
end) =~ ~r"this expression will fail with ArithmeticError\n.*nofile:2"
|
||||
end) =~ "#{@arithmetic_error_message} will fail with ArithmeticError\n nofile:2"
|
||||
after
|
||||
purge([Sample])
|
||||
purge([Sample1, Sample2])
|
||||
end
|
||||
|
||||
test "undefined function for behaviour" do
|
||||
@@ -1147,31 +1154,6 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "with and do clauses emit errors, else clauses do not" do
|
||||
assert capture_err(fn ->
|
||||
Code.compile_string("""
|
||||
with {:first, int} when is_integer(int) <- {:second, Integer.gcd(2, 4)} do
|
||||
int
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match"
|
||||
|
||||
assert capture_err(fn ->
|
||||
Code.compile_string("""
|
||||
with {:first, int1} when is_integer(int1) <- {:first, Integer.gcd(2, 4)},
|
||||
{:second, int2} when is_integer(int2) <- {:second, Integer.gcd(2, 4)} do
|
||||
{:ok, int1 + int2}
|
||||
else
|
||||
{:first, nil} -> {:error, "first number is not integer"}
|
||||
{:second, nil} -> {:error, "second number is not integer"}
|
||||
end
|
||||
""")
|
||||
end) == ""
|
||||
after
|
||||
purge(Sample1)
|
||||
purge(Sample2)
|
||||
end
|
||||
|
||||
test "warning on code point escape" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("? ")
|
||||
@@ -1709,7 +1691,7 @@ defmodule Kernel.WarningTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "deprecated GenServer super" do
|
||||
test "deprecated GenServer super on callbacks" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
@@ -1725,6 +1707,22 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "super is allowed on GenServer.child_spec/1" do
|
||||
refute capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
use GenServer
|
||||
|
||||
def child_spec(opts) do
|
||||
super(opts)
|
||||
end
|
||||
end
|
||||
""")
|
||||
end) =~ "calling super for GenServer callback child_spec/1 is deprecated"
|
||||
after
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "nested comparison operators" do
|
||||
message =
|
||||
capture_err(fn ->
|
||||
|
||||
@@ -592,16 +592,6 @@ defmodule KernelTest do
|
||||
_ = a
|
||||
end
|
||||
|
||||
test "setting attribute with uppercase" do
|
||||
message = "module attributes set via @ cannot start with an uppercase letter"
|
||||
|
||||
assert_raise ArgumentError, message, fn ->
|
||||
defmodule UpcaseAttrSample do
|
||||
@Upper
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "in module body" do
|
||||
defmodule InSample do
|
||||
@foo [:a, :b]
|
||||
@@ -782,6 +772,40 @@ defmodule KernelTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "@" do
|
||||
test "setting attribute with do-block" do
|
||||
exception =
|
||||
catch_error(
|
||||
defmodule UpcaseAttrSample do
|
||||
@foo quote do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
assert exception.message =~ "expected 0 or 1 argument for @foo, got 2"
|
||||
assert exception.message =~ "You probably want to wrap the argument value in parentheses"
|
||||
end
|
||||
|
||||
test "setting attribute with uppercase" do
|
||||
message = "module attributes set via @ cannot start with an uppercase letter"
|
||||
|
||||
assert_raise ArgumentError, message, fn ->
|
||||
defmodule UpcaseAttrSample do
|
||||
@Upper
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "matching attribute" do
|
||||
assert_raise ArgumentError, ~r"invalid write attribute syntax", fn ->
|
||||
defmodule MatchAttributeInModule do
|
||||
@foo = 42
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "defdelegate" do
|
||||
defdelegate my_flatten(list), to: List, as: :flatten
|
||||
|
||||
|
||||
@@ -250,27 +250,43 @@ defmodule Module.Types.ExprTest do
|
||||
) == {:ok, {:var, 0}}
|
||||
end
|
||||
|
||||
test "for" do
|
||||
assert quoted_expr(
|
||||
[list],
|
||||
for(
|
||||
foo <- list,
|
||||
is_integer(foo),
|
||||
do: foo == 123
|
||||
)
|
||||
) == {:ok, :dynamic}
|
||||
|
||||
assert quoted_expr(
|
||||
[list, bar],
|
||||
(
|
||||
describe "for comprehension" do
|
||||
test "with generators and filters" do
|
||||
assert quoted_expr(
|
||||
[list],
|
||||
for(
|
||||
foo <- list,
|
||||
is_integer(bar),
|
||||
is_integer(foo),
|
||||
do: foo == 123
|
||||
)
|
||||
) == {:ok, :dynamic}
|
||||
end
|
||||
|
||||
bar
|
||||
)
|
||||
) == {:ok, {:var, 0}}
|
||||
test "with unused return" do
|
||||
assert quoted_expr(
|
||||
[list, bar],
|
||||
(
|
||||
for(
|
||||
foo <- list,
|
||||
is_integer(bar),
|
||||
do: foo == 123
|
||||
)
|
||||
|
||||
bar
|
||||
)
|
||||
) == {:ok, {:var, 0}}
|
||||
end
|
||||
|
||||
test "with reduce" do
|
||||
assert quoted_expr(
|
||||
[],
|
||||
for(i <- [1, 2, 3], do: (acc -> i + acc), reduce: 0)
|
||||
) == {:ok, :dynamic}
|
||||
|
||||
assert quoted_expr(
|
||||
[],
|
||||
for(i <- [1, 2, 3], do: (_ -> i), reduce: nil)
|
||||
) == {:ok, :dynamic}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Code.require_file("../../test_helper.exs", __DIR__)
|
||||
Code.require_file("type_helper.exs", __DIR__)
|
||||
|
||||
defmodule Module.Types.IntegrationTest do
|
||||
use ExUnit.Case
|
||||
|
||||
@@ -297,7 +297,7 @@ defmodule Module.Types.MapTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "with bound var keys" do
|
||||
test "map creation with bound var keys" do
|
||||
assert quoted_expr(
|
||||
[atom, bool, true = var],
|
||||
[is_atom(atom) and is_boolean(bool)],
|
||||
@@ -307,7 +307,8 @@ defmodule Module.Types.MapTest do
|
||||
{:map,
|
||||
[
|
||||
{:required, {:atom, true}, {:atom, true}},
|
||||
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, {:union, [atom: true, atom: false]},
|
||||
{:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
|
||||
]}}
|
||||
|
||||
@@ -320,7 +321,8 @@ defmodule Module.Types.MapTest do
|
||||
{:map,
|
||||
[
|
||||
{:required, {:atom, true}, {:atom, true}},
|
||||
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, {:union, [atom: true, atom: false]},
|
||||
{:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
|
||||
]}}
|
||||
|
||||
@@ -333,12 +335,13 @@ defmodule Module.Types.MapTest do
|
||||
{:map,
|
||||
[
|
||||
{:required, {:atom, true}, {:atom, true}},
|
||||
{:required, :boolean, {:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, {:union, [atom: true, atom: false]},
|
||||
{:union, [{:atom, :bool}, {:atom, true}]}},
|
||||
{:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}}
|
||||
]}}
|
||||
end
|
||||
|
||||
test "with unbound var keys" do
|
||||
test "map creation with unbound var keys" do
|
||||
assert quoted_expr(
|
||||
[var, struct],
|
||||
(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Code.require_file("../../test_helper.exs", __DIR__)
|
||||
Code.require_file("type_helper.exs", __DIR__)
|
||||
|
||||
defmodule Module.Types.PatternTest do
|
||||
use ExUnit.Case, async: true
|
||||
@@ -118,13 +118,18 @@ defmodule Module.Types.PatternTest do
|
||||
{:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}}
|
||||
|
||||
assert quoted_pattern(%{123 => a}) ==
|
||||
{:ok, {:map, [{:required, :integer, {:var, 0}}, {:optional, :dynamic, :dynamic}]}}
|
||||
{:ok,
|
||||
{:map,
|
||||
[
|
||||
{:required, :integer, {:union, [{:var, 0}, :dynamic]}},
|
||||
{:optional, :dynamic, :dynamic}
|
||||
]}}
|
||||
|
||||
assert quoted_pattern(%{123 => :foo, 456 => :bar}) ==
|
||||
{:ok,
|
||||
{:map,
|
||||
[
|
||||
{:required, :integer, {:union, [{:atom, :foo}, {:atom, :bar}]}},
|
||||
{:required, :integer, :dynamic},
|
||||
{:optional, :dynamic, :dynamic}
|
||||
]}}
|
||||
|
||||
@@ -285,10 +290,10 @@ defmodule Module.Types.PatternTest do
|
||||
{:ok, [{:union, [:tuple, :atom]}]}
|
||||
|
||||
assert quoted_head([x], [is_boolean(x) and is_atom(x)]) ==
|
||||
{:ok, [:boolean]}
|
||||
{:ok, [{:union, [atom: true, atom: false]}]}
|
||||
|
||||
assert quoted_head([x], [is_atom(x) and is_boolean(x)]) ==
|
||||
{:ok, [:boolean]}
|
||||
{:ok, [{:union, [atom: true, atom: false]}]}
|
||||
|
||||
assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]}
|
||||
|
||||
@@ -307,7 +312,7 @@ defmodule Module.Types.PatternTest do
|
||||
assert {:error, {:unable_unify, {:tuple, :atom, _}}} =
|
||||
quoted_head([x], [is_tuple(x) and is_atom(x)])
|
||||
|
||||
assert {:error, {:unable_unify, {:boolean, :tuple, _}}} =
|
||||
assert {:error, {:unable_unify, {{:union, [atom: true, atom: false]}, :tuple, _}}} =
|
||||
quoted_head([x], [is_tuple(is_atom(x))])
|
||||
end
|
||||
|
||||
@@ -322,13 +327,16 @@ defmodule Module.Types.PatternTest do
|
||||
assert {:error, {:unable_unify, {{:atom, :foo}, {:list, :dynamic}, _}}} =
|
||||
quoted_head([x], [length(:foo)])
|
||||
|
||||
assert {:error, {:unable_unify, {:boolean, {:list, :dynamic}, _}}} =
|
||||
assert {:error,
|
||||
{:unable_unify, {{:union, [atom: true, atom: false]}, {:list, :dynamic}, _}}} =
|
||||
quoted_head([x], [length(is_tuple(x))])
|
||||
|
||||
assert {:error, {:unable_unify, {:boolean, :tuple, _}}} =
|
||||
assert {:error, {:unable_unify, {{:union, [atom: true, atom: false]}, :tuple, _}}} =
|
||||
quoted_head([x], [elem(is_tuple(x), 0)])
|
||||
|
||||
assert {:error, {:unable_unify, {:boolean, :number, _}}} =
|
||||
assert {:error,
|
||||
{:unable_unify,
|
||||
{{:union, [atom: true, atom: false]}, {:union, [:integer, :float]}, _}}} =
|
||||
quoted_head([x], [elem({}, is_tuple(x))])
|
||||
|
||||
assert quoted_head([x], [elem({}, 1)]) == {:ok, [var: 0]}
|
||||
@@ -371,7 +379,11 @@ defmodule Module.Types.PatternTest do
|
||||
assert quoted_head([%{true: bool}], [is_boolean(bool)]) ==
|
||||
{:ok,
|
||||
[
|
||||
{:map, [{:required, {:atom, true}, :boolean}, {:optional, :dynamic, :dynamic}]}
|
||||
{:map,
|
||||
[
|
||||
{:required, {:atom, true}, {:union, [atom: true, atom: false]}},
|
||||
{:optional, :dynamic, :dynamic}
|
||||
]}
|
||||
]}
|
||||
|
||||
assert quoted_head([%{true: true} = foo, %{false: false} = foo]) ==
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Code.require_file("../../test_helper.exs", __DIR__)
|
||||
Code.require_file("type_helper.exs", __DIR__)
|
||||
|
||||
defmodule Module.Types.TypesTest do
|
||||
use ExUnit.Case, async: true
|
||||
@@ -13,20 +13,21 @@ defmodule Module.Types.TypesTest do
|
||||
expr = TypeHelper.expand_expr(patterns, guards, body, __CALLER__)
|
||||
|
||||
quote do
|
||||
unquote(Macro.escape(expr))
|
||||
|> Module.Types.TypesTest.__expr__()
|
||||
|> to_warning()
|
||||
Module.Types.TypesTest.__expr__(unquote(Macro.escape(expr)))
|
||||
end
|
||||
end
|
||||
|
||||
def __expr__({patterns, guards, body}) do
|
||||
with {:ok, _types, context} <-
|
||||
Pattern.of_head(patterns, guards, TypeHelper.new_stack(), TypeHelper.new_context()),
|
||||
{:ok, type, context} <- Expr.of_expr(body, TypeHelper.new_stack(), context) do
|
||||
flunk("expexted error, got: #{inspect(Types.lift_type(type, context))}")
|
||||
{:ok, _type, context} <- Expr.of_expr(body, TypeHelper.new_stack(), context) do
|
||||
case context.warnings do
|
||||
[warning] -> to_message(:warning, warning)
|
||||
_ -> :none
|
||||
end
|
||||
else
|
||||
{:error, {type, reason, context}} ->
|
||||
{:error, {type, reason, context}}
|
||||
to_message(:error, {type, reason, context})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,12 +49,18 @@ defmodule Module.Types.TypesTest do
|
||||
min
|
||||
end
|
||||
|
||||
defp to_warning({:error, {type, reason, context}}) do
|
||||
defp to_message(:warning, {module, warning, _location}) do
|
||||
warning
|
||||
|> module.format_warning()
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp to_message(:error, {type, reason, context}) do
|
||||
{Module.Types, error, _location} = Module.Types.error_to_warning(type, reason, context)
|
||||
|
||||
error
|
||||
|> Module.Types.format_warning()
|
||||
|> List.to_string()
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing("\nConflict found at")
|
||||
end
|
||||
|
||||
@@ -69,6 +76,17 @@ defmodule Module.Types.TypesTest do
|
||||
assert Types.expr_to_string(quote(do: :erlang.element(:erlang.+(a, 1), b))) == "elem(b, a)"
|
||||
end
|
||||
|
||||
test "undefined function warnings" do
|
||||
assert warning([], URI.unknown("foo")) ==
|
||||
"URI.unknown/1 is undefined or private"
|
||||
|
||||
assert warning([], if(true, do: URI.unknown("foo"))) ==
|
||||
"URI.unknown/1 is undefined or private"
|
||||
|
||||
assert warning([], try(do: :ok, after: URI.unknown("foo"))) ==
|
||||
"URI.unknown/1 is undefined or private"
|
||||
end
|
||||
|
||||
describe "function head warnings" do
|
||||
test "warns on literals" do
|
||||
string = warning([var = 123, var = "abc"], var)
|
||||
@@ -452,4 +470,20 @@ defmodule Module.Types.TypesTest do
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
describe "regressions" do
|
||||
test "recursive map fields" do
|
||||
assert warning(
|
||||
[queried],
|
||||
with(
|
||||
true <- is_nil(queried.foo.bar),
|
||||
_ = queried.foo
|
||||
) do
|
||||
%{foo: %{other_id: _other_id} = foo} = queried
|
||||
%{other_id: id} = foo
|
||||
%{id: id}
|
||||
end
|
||||
) == :none
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+31
-35
@@ -1,8 +1,8 @@
|
||||
Code.require_file("../../test_helper.exs", __DIR__)
|
||||
Code.require_file("type_helper.exs", __DIR__)
|
||||
|
||||
defmodule Module.Types.InferTest do
|
||||
defmodule Module.Types.UnifyTest do
|
||||
use ExUnit.Case, async: true
|
||||
import Module.Types.Infer
|
||||
import Module.Types.Unify
|
||||
alias Module.Types
|
||||
|
||||
defp unify_lift(left, right, context \\ new_context()) do
|
||||
@@ -53,27 +53,17 @@ defmodule Module.Types.InferTest do
|
||||
assert unify_lift(:integer, :integer) == {:ok, :integer}
|
||||
assert unify_lift(:binary, :binary) == {:ok, :binary}
|
||||
assert unify_lift(:atom, :atom) == {:ok, :atom}
|
||||
assert unify_lift(:boolean, :boolean) == {:ok, :boolean}
|
||||
|
||||
assert {:error, {:unable_unify, {:integer, :boolean, _}}} = unify_lift(:integer, :boolean)
|
||||
assert {:error, {:unable_unify, {:integer, :atom, _}}} = unify_lift(:integer, :atom)
|
||||
end
|
||||
|
||||
test "subtype undirected" do
|
||||
assert unify_lift(:boolean, :atom) == {:ok, :boolean}
|
||||
assert unify_lift(:atom, :boolean) == {:ok, :boolean}
|
||||
assert unify_lift(:boolean, {:atom, true}) == {:ok, {:atom, true}}
|
||||
assert unify_lift({:atom, true}, :boolean) == {:ok, {:atom, true}}
|
||||
assert unify_lift(:atom, {:atom, true}) == {:ok, {:atom, true}}
|
||||
assert unify_lift({:atom, true}, :atom) == {:ok, {:atom, true}}
|
||||
end
|
||||
|
||||
test "subtype directed" do
|
||||
assert unify_directed_lift(:boolean, :atom) == {:ok, :boolean}
|
||||
assert unify_directed_lift({:atom, true}, :boolean) == {:ok, {:atom, true}}
|
||||
assert unify_directed_lift({:atom, true}, :atom) == {:ok, {:atom, true}}
|
||||
|
||||
assert {:error, _} = unify_directed_lift(:atom, :boolean)
|
||||
assert {:error, _} = unify_directed_lift(:boolean, {:atom, true})
|
||||
assert {:error, _} = unify_directed_lift(:atom, {:atom, true})
|
||||
end
|
||||
|
||||
@@ -83,8 +73,8 @@ defmodule Module.Types.InferTest do
|
||||
assert unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}) ==
|
||||
{:ok, {:tuple, 1, [:integer]}}
|
||||
|
||||
assert unify_lift({:tuple, 1, [:boolean]}, {:tuple, 1, [:atom]}) ==
|
||||
{:ok, {:tuple, 1, [:boolean]}}
|
||||
assert unify_lift({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}) ==
|
||||
{:ok, {:tuple, 1, [{:atom, :foo}]}}
|
||||
|
||||
assert {:error, {:unable_unify, {{:tuple, 1, [:integer]}, {:tuple, 0, []}, _}}} =
|
||||
unify_lift({:tuple, 1, [:integer]}, {:tuple, 0, []})
|
||||
@@ -134,10 +124,10 @@ defmodule Module.Types.InferTest do
|
||||
{:ok, {:map, [{:required, :integer, :atom}]}}
|
||||
|
||||
assert unify_lift(
|
||||
{:map, [{:required, {:atom, :foo}, :boolean}]},
|
||||
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
|
||||
{:map, [{:required, {:atom, :foo}, :atom}]}
|
||||
) ==
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
|
||||
|
||||
assert {:error,
|
||||
{:unable_unify,
|
||||
@@ -166,28 +156,28 @@ defmodule Module.Types.InferTest do
|
||||
|
||||
test "map required/optional key" do
|
||||
assert unify_lift(
|
||||
{:map, [{:required, {:atom, :foo}, :boolean}]},
|
||||
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
|
||||
{:map, [{:required, {:atom, :foo}, :atom}]}
|
||||
) ==
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
|
||||
|
||||
assert unify_lift(
|
||||
{:map, [{:optional, {:atom, :foo}, :boolean}]},
|
||||
{:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]},
|
||||
{:map, [{:required, {:atom, :foo}, :atom}]}
|
||||
) ==
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
|
||||
|
||||
assert unify_lift(
|
||||
{:map, [{:required, {:atom, :foo}, :boolean}]},
|
||||
{:map, [{:required, {:atom, :foo}, {:atom, :bar}}]},
|
||||
{:map, [{:optional, {:atom, :foo}, :atom}]}
|
||||
) ==
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, :boolean}]}}
|
||||
{:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}}
|
||||
|
||||
assert unify_lift(
|
||||
{:map, [{:optional, {:atom, :foo}, :boolean}]},
|
||||
{:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]},
|
||||
{:map, [{:optional, {:atom, :foo}, :atom}]}
|
||||
) ==
|
||||
{:ok, {:map, [{:optional, {:atom, :foo}, :boolean}]}}
|
||||
{:ok, {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}}
|
||||
end
|
||||
|
||||
test "map with subtyped keys" do
|
||||
@@ -277,8 +267,8 @@ defmodule Module.Types.InferTest do
|
||||
assert unify_lift({:union, [:integer, :atom]}, {:union, [:atom, :integer]}) ==
|
||||
{:ok, {:union, [:integer, :atom]}}
|
||||
|
||||
assert unify_lift({:union, [:atom]}, {:union, [:boolean]}) == {:ok, {:union, [:boolean]}}
|
||||
assert unify_lift({:union, [:boolean]}, {:union, [:atom]}) == {:ok, {:union, [:boolean]}}
|
||||
assert unify_lift({:union, [:atom]}, {:union, [{:atom, :bar}]}) ==
|
||||
{:ok, {:union, [{:atom, :bar}]}}
|
||||
|
||||
assert {:error, {:unable_unify, {{:union, [:integer]}, {:union, [:atom]}, _}}} =
|
||||
unify_lift({:union, [:integer]}, {:union, [:atom]})
|
||||
@@ -360,11 +350,22 @@ defmodule Module.Types.InferTest do
|
||||
assert {{:var, 2}, var_context} = new_var({:baz, [version: 2], nil}, var_context)
|
||||
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context)
|
||||
assert {:ok, {:var, _}, _context} = unify({:var, 1}, {:var, 0}, context)
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context)
|
||||
assert context.types[0] == {:var, 1}
|
||||
assert context.types[1] == {:var, 0}
|
||||
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 0}, :tuple, var_context)
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context)
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, context)
|
||||
assert context.types[0] == {:var, 1}
|
||||
assert context.types[1] == :tuple
|
||||
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context)
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 2}, context)
|
||||
assert {:ok, {:var, _}, _context} = unify({:var, 2}, {:var, 0}, context)
|
||||
assert context.types[0] == :unbound
|
||||
assert context.types[1] == {:var, 0}
|
||||
assert context.types[2] == {:var, 1}
|
||||
|
||||
assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context)
|
||||
|
||||
@@ -425,15 +426,11 @@ defmodule Module.Types.InferTest do
|
||||
describe "subtype?/3" do
|
||||
test "with simple types" do
|
||||
assert subtype?({:atom, :foo}, :atom, new_context())
|
||||
assert subtype?({:atom, true}, :boolean, new_context())
|
||||
assert subtype?({:atom, true}, :atom, new_context())
|
||||
assert subtype?(:boolean, :atom, new_context())
|
||||
|
||||
refute subtype?(:integer, :binary, new_context())
|
||||
refute subtype?(:atom, {:atom, :foo}, new_context())
|
||||
refute subtype?(:boolean, {:atom, true}, new_context())
|
||||
refute subtype?(:atom, {:atom, true}, new_context())
|
||||
refute subtype?(:atom, :boolean, new_context())
|
||||
end
|
||||
|
||||
test "with composite types" do
|
||||
@@ -503,8 +500,7 @@ defmodule Module.Types.InferTest do
|
||||
test "to_union/2" do
|
||||
assert to_union([:atom], new_context()) == :atom
|
||||
assert to_union([:integer, :integer], new_context()) == :integer
|
||||
assert to_union([:boolean, :atom], new_context()) == :atom
|
||||
assert to_union([{:atom, :foo}, :boolean, :atom], new_context()) == :atom
|
||||
assert to_union([{:atom, :foo}, {:atom, :bar}, :atom], new_context()) == :atom
|
||||
|
||||
assert to_union([:binary, :atom], new_context()) == {:union, [:binary, :atom]}
|
||||
assert to_union([:atom, :binary, :atom], new_context()) == {:union, [:atom, :binary]}
|
||||
@@ -95,17 +95,16 @@ defmodule ModuleTest do
|
||||
end
|
||||
|
||||
test "supports read access to module from __after_compile__/2" do
|
||||
contents =
|
||||
quote do
|
||||
@after_compile __MODULE__
|
||||
@foo 42
|
||||
defmodule ModuleTest.NoRaise do
|
||||
@after_compile __MODULE__
|
||||
@foo 42
|
||||
|
||||
def __after_compile__(%Macro.Env{module: module}, bin) when is_binary(bin) do
|
||||
Module.get_attribute(module, :foo)
|
||||
end
|
||||
def __after_compile__(%Macro.Env{module: module}, bin) when is_binary(bin) do
|
||||
send(self(), Module.get_attribute(module, :foo))
|
||||
end
|
||||
end
|
||||
|
||||
Module.create(ModuleTest.NoRaise, contents, __ENV__)
|
||||
assert_received 42
|
||||
end
|
||||
|
||||
test "in memory modules are tagged as so" do
|
||||
|
||||
@@ -156,7 +156,20 @@ defmodule RecordTest do
|
||||
)
|
||||
|
||||
assert user == {RecordTest, :name, {RecordTest, :inner_name, :inner_age}}
|
||||
assert Process.info(self(), :messages) == {:messages, [:inner_age, :inner_name, :name]}
|
||||
assert for(_ <- 1..3, do: assert_receive(_)) == [:inner_age, :inner_name, :name]
|
||||
|
||||
user =
|
||||
user(
|
||||
name: send(self(), :name),
|
||||
age:
|
||||
user(
|
||||
age: send(self(), :inner_age),
|
||||
name: send(self(), :inner_name)
|
||||
)
|
||||
)
|
||||
|
||||
assert user == {RecordTest, :name, {RecordTest, :inner_name, :inner_age}}
|
||||
assert for(_ <- 1..3, do: assert_receive(_)) == [:name, :inner_age, :inner_name]
|
||||
end
|
||||
|
||||
Record.defrecord(
|
||||
|
||||
@@ -259,41 +259,10 @@ defmodule ExUnit.Case do
|
||||
end
|
||||
|
||||
quote do
|
||||
unless Module.has_attribute?(__MODULE__, :ex_unit_tests) do
|
||||
tag_check =
|
||||
[:moduletag, :describetag, :tag]
|
||||
|> Enum.any?(&Module.has_attribute?(__MODULE__, &1))
|
||||
|
||||
if tag_check do
|
||||
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
|
||||
end
|
||||
|
||||
attributes = [
|
||||
:ex_unit_tests,
|
||||
:tag,
|
||||
:describetag,
|
||||
:moduletag,
|
||||
:ex_unit_registered_test_attributes,
|
||||
:ex_unit_registered_describe_attributes,
|
||||
:ex_unit_registered_module_attributes,
|
||||
:ex_unit_used_describes
|
||||
]
|
||||
|
||||
Enum.each(attributes, &Module.register_attribute(__MODULE__, &1, accumulate: true))
|
||||
|
||||
@before_compile ExUnit.Case
|
||||
@after_compile ExUnit.Case
|
||||
@ex_unit_async false
|
||||
@ex_unit_describe nil
|
||||
unless ExUnit.Case.__register__(__MODULE__, unquote(opts)) do
|
||||
use ExUnit.Callbacks
|
||||
end
|
||||
|
||||
async = unquote(opts)[:async]
|
||||
|
||||
if is_boolean(async) do
|
||||
@ex_unit_async async
|
||||
end
|
||||
|
||||
import ExUnit.Callbacks
|
||||
import ExUnit.Assertions
|
||||
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
|
||||
@@ -301,6 +270,49 @@ defmodule ExUnit.Case do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __register__(module, opts) do
|
||||
registered? = Module.has_attribute?(module, :ex_unit_tests)
|
||||
|
||||
unless registered? do
|
||||
tag_check = Enum.any?([:moduletag, :describetag, :tag], &Module.has_attribute?(module, &1))
|
||||
|
||||
if tag_check do
|
||||
raise "you must set @tag, @describetag, and @moduletag after the call to \"use ExUnit.Case\""
|
||||
end
|
||||
|
||||
attributes = [
|
||||
:ex_unit_tests,
|
||||
:tag,
|
||||
:describetag,
|
||||
:moduletag,
|
||||
:ex_unit_registered_test_attributes,
|
||||
:ex_unit_registered_describe_attributes,
|
||||
:ex_unit_registered_module_attributes,
|
||||
:ex_unit_used_describes
|
||||
]
|
||||
|
||||
Enum.each(attributes, &Module.register_attribute(module, &1, accumulate: true))
|
||||
|
||||
attributes = [
|
||||
before_compile: ExUnit.Case,
|
||||
after_compile: ExUnit.Case,
|
||||
ex_unit_async: false,
|
||||
ex_unit_describe: nil
|
||||
]
|
||||
|
||||
Enum.each(attributes, fn {k, v} -> Module.put_attribute(module, k, v) end)
|
||||
end
|
||||
|
||||
async? = opts[:async]
|
||||
|
||||
if is_boolean(async?) do
|
||||
Module.put_attribute(module, :ex_unit_async, async?)
|
||||
end
|
||||
|
||||
registered?
|
||||
end
|
||||
|
||||
@doc """
|
||||
Defines a test with `message`.
|
||||
|
||||
@@ -333,9 +345,17 @@ defmodule ExUnit.Case do
|
||||
|
||||
var = Macro.escape(var)
|
||||
contents = Macro.escape(contents, unquote: true)
|
||||
%{module: mod, file: file, line: line} = __CALLER__
|
||||
|
||||
quote bind_quoted: [var: var, contents: contents, message: message] do
|
||||
name = ExUnit.Case.register_test(__ENV__, :test, message, [])
|
||||
quote bind_quoted: [
|
||||
var: var,
|
||||
contents: contents,
|
||||
message: message,
|
||||
mod: mod,
|
||||
file: file,
|
||||
line: line
|
||||
] do
|
||||
name = ExUnit.Case.register_test(mod, file, line, :test, message, [])
|
||||
def unquote(name)(unquote(var)), do: unquote(contents)
|
||||
end
|
||||
end
|
||||
@@ -354,8 +374,10 @@ defmodule ExUnit.Case do
|
||||
|
||||
"""
|
||||
defmacro test(message) do
|
||||
%{module: mod, file: file, line: line} = __CALLER__
|
||||
|
||||
quote bind_quoted: binding() do
|
||||
name = ExUnit.Case.register_test(__ENV__, :test, message, [:not_implemented])
|
||||
name = ExUnit.Case.register_test(mod, file, line, :test, message, [:not_implemented])
|
||||
def unquote(name)(_), do: flunk("Not implemented")
|
||||
end
|
||||
end
|
||||
@@ -423,23 +445,14 @@ defmodule ExUnit.Case do
|
||||
"""
|
||||
defmacro describe(message, do: block) do
|
||||
quote do
|
||||
ExUnit.Case.__describe__(__MODULE__, __ENV__.line, unquote(message))
|
||||
|
||||
try do
|
||||
ExUnit.Case.__describe__(__MODULE__, __ENV__.line, unquote(message), fn ->
|
||||
unquote(block)
|
||||
after
|
||||
@ex_unit_describe nil
|
||||
Module.delete_attribute(__MODULE__, :describetag)
|
||||
|
||||
for attribute <- Module.get_attribute(__MODULE__, :ex_unit_registered_describe_attributes) do
|
||||
Module.delete_attribute(__MODULE__, attribute)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __describe__(module, line, message) do
|
||||
def __describe__(module, line, message, fun) do
|
||||
if Module.get_attribute(module, :ex_unit_describe) do
|
||||
raise "cannot call \"describe\" inside another \"describe\". See the documentation " <>
|
||||
"for ExUnit.Case.describe/2 on named setups and how to handle hierarchies"
|
||||
@@ -463,7 +476,17 @@ defmodule ExUnit.Case do
|
||||
|
||||
Module.put_attribute(module, :ex_unit_describe, {line, message})
|
||||
Module.put_attribute(module, :ex_unit_used_describes, message)
|
||||
:ok
|
||||
|
||||
try do
|
||||
fun.()
|
||||
after
|
||||
Module.put_attribute(module, :ex_unit_describe, nil)
|
||||
Module.delete_attribute(module, :describetag)
|
||||
|
||||
for attribute <- Module.get_attribute(module, :ex_unit_registered_describe_attributes) do
|
||||
Module.delete_attribute(module, attribute)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -496,7 +519,7 @@ defmodule ExUnit.Case do
|
||||
display. You can use `ExUnit.plural_rule/2` to set a custom
|
||||
pluralization.
|
||||
"""
|
||||
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
|
||||
def register_test(mod, file, line, test_type, name, tags) do
|
||||
unless Module.has_attribute?(mod, :ex_unit_tests) do
|
||||
raise "cannot define #{test_type}. Please make sure you have invoked " <>
|
||||
"\"use ExUnit.Case\" in the current module"
|
||||
@@ -557,6 +580,17 @@ defmodule ExUnit.Case do
|
||||
name
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reigsters a test with the given environment.
|
||||
|
||||
This function is deprecated in favor of register_test/6 which performs
|
||||
better under tight loops by avoiding `__ENV__`.
|
||||
"""
|
||||
@doc deprecated: "Use register_test/6 instead"
|
||||
def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do
|
||||
register_test(mod, file, line, test_type, name, tags)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Registers a new attribute to be used during `ExUnit.Case` tests.
|
||||
|
||||
|
||||
@@ -70,8 +70,14 @@ defmodule ExUnit.Diff do
|
||||
diff_quoted(number, right, env)
|
||||
end
|
||||
|
||||
defp diff_quoted({:++, _, _} = left, right, env) when is_list(right) do
|
||||
diff_maybe_improper_list(left, right, env)
|
||||
defp diff_quoted({:++, meta, [prefix, suffix]}, right, env) when is_list(right) do
|
||||
case prefix do
|
||||
{_, [expanded: expanded] ++ _, _} ->
|
||||
diff_maybe_improper_list({:++, meta, [expanded, suffix]}, right, env)
|
||||
|
||||
_ ->
|
||||
diff_maybe_improper_list({:++, meta, [prefix, suffix]}, right, env)
|
||||
end
|
||||
end
|
||||
|
||||
defp diff_quoted({:{}, _, left}, right, env) when is_tuple(right) do
|
||||
|
||||
@@ -345,7 +345,6 @@ defmodule ExUnit.DiffTest do
|
||||
|
||||
refute_diff([:a, :b] ++ [:c] = [:a, :b], "[:a, :b] ++ [-:c-]", "[:a, :b]")
|
||||
refute_diff([:a, :c] ++ [:b] = [:a, :b], "[:a, -:c-] ++ [:b]", "[:a, :b]")
|
||||
|
||||
refute_diff([:a] ++ [:b] ++ [:c] = [:a, :b], "[:a] ++ [:b] ++ [-:c-]", "[:a, :b]")
|
||||
|
||||
assert_diff([:a] ++ :b = [:a | :b], [])
|
||||
@@ -353,10 +352,16 @@ defmodule ExUnit.DiffTest do
|
||||
|
||||
refute_diff([:a, :b] ++ :c = [:a, :b, :c], "[:a, :b] ++ -:c-", "[:a, :b, +:c+]")
|
||||
refute_diff([:a] ++ [:b] ++ :c = [:a, :b, :c], "[:a] ++ [:b] ++ -:c-", "[:a, :b, +:c+]")
|
||||
|
||||
refute_diff([:a] ++ [:b] = :a, "-[:a] ++ [:b]-", "+:a+")
|
||||
end
|
||||
|
||||
@a [:a]
|
||||
test "concat lists with module attributes" do
|
||||
assert_diff(@a ++ [:b] = [:a, :b], [])
|
||||
refute_diff(@a ++ [:b] = [:a], "[:a] ++ [-:b-]", "[:a]")
|
||||
refute_diff(@a ++ [:b] = [:b], "[-:a-] ++ [:b]", "[:b]")
|
||||
end
|
||||
|
||||
test "mixed lists" do
|
||||
refute_diff([:a | :b] = [:a, :b], "[:a | -:b-]", "[:a, +:b+]")
|
||||
refute_diff([:a, :b] = [:a | :b], "[:a, -:b-]", "[:a | +:b+]")
|
||||
|
||||
@@ -778,9 +778,10 @@ defmodule IEx.Introspection do
|
||||
doc && IO.ANSI.Docs.print(doc, format, opts)
|
||||
end
|
||||
|
||||
defp translate_doc(%{"en" => doc}), do: doc
|
||||
defp translate_doc(%{}), do: nil
|
||||
defp translate_doc(:none), do: nil
|
||||
defp translate_doc(:hidden), do: nil
|
||||
defp translate_doc(%{"en" => doc}), do: doc
|
||||
|
||||
defp no_beam(module) do
|
||||
case Code.ensure_loaded(module) do
|
||||
|
||||
@@ -242,8 +242,8 @@ defmodule IEx.Server do
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_take_over({:respawn, evaluator}, _state, evaluator, evaluator_ref, input, _callback) do
|
||||
rerun([], evaluator, evaluator_ref, input)
|
||||
defp handle_take_over({:respawn, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
|
||||
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
|
||||
end
|
||||
|
||||
defp handle_take_over({:continue, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
|
||||
@@ -254,18 +254,18 @@ defmodule IEx.Server do
|
||||
|
||||
defp handle_take_over(
|
||||
{:DOWN, evaluator_ref, :process, evaluator, :normal},
|
||||
_state,
|
||||
state,
|
||||
evaluator,
|
||||
evaluator_ref,
|
||||
input,
|
||||
_callback
|
||||
) do
|
||||
rerun([], evaluator, evaluator_ref, input)
|
||||
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
|
||||
end
|
||||
|
||||
defp handle_take_over(
|
||||
{:DOWN, evaluator_ref, :process, evaluator, reason},
|
||||
_state,
|
||||
state,
|
||||
evaluator,
|
||||
evaluator_ref,
|
||||
input,
|
||||
@@ -281,7 +281,7 @@ defmodule IEx.Server do
|
||||
io_error("** (IEx.Error) #{type} when printing EXIT message: #{inspect(detail)}")
|
||||
end
|
||||
|
||||
rerun([], evaluator, evaluator_ref, input)
|
||||
rerun(state.evaluator_options, evaluator, evaluator_ref, input)
|
||||
end
|
||||
|
||||
defp handle_take_over(_, state, _evaluator, _evaluator_ref, _input, callback) do
|
||||
|
||||
@@ -62,7 +62,9 @@ defmodule Logger.App do
|
||||
|
||||
@doc false
|
||||
def config_change(changed, _new, _removed) do
|
||||
Logger.configure(changed)
|
||||
# All other config has already been persisted, we only need to
|
||||
# update the level and reload the logger state.
|
||||
Logger.configure(Keyword.take(changed, [:level]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -147,8 +147,17 @@ defmodule Logger.Handler do
|
||||
end
|
||||
end
|
||||
|
||||
defp notify(:sync, msg), do: :gen_event.sync_notify(Logger, msg)
|
||||
defp notify(:async, msg), do: :gen_event.notify(Logger, msg)
|
||||
defp notify(:sync, msg) do
|
||||
pid = Process.whereis(Logger)
|
||||
|
||||
# If we are within the logger process itself,
|
||||
# we cannot use sync notify as that will deadlock.
|
||||
if pid == self(), do: :gen_event.notify(pid, msg), else: :gen_event.sync_notify(pid, msg)
|
||||
end
|
||||
|
||||
defp notify(:async, msg) do
|
||||
:gen_event.notify(Logger, msg)
|
||||
end
|
||||
|
||||
defp truncate(data, n) when is_list(data) do
|
||||
Logger.Utils.truncate(data, n)
|
||||
|
||||
@@ -357,15 +357,22 @@ defmodule Logger.Translator do
|
||||
[]
|
||||
end
|
||||
|
||||
defp child_debug(:debug, restart_type: restart, shutdown: shutdown, child_type: type) do
|
||||
["\nRestart: ", inspect(restart), "\nShutdown: ", inspect(shutdown)] ++
|
||||
["\nType: ", inspect(type)]
|
||||
defp child_debug(:debug, opts) do
|
||||
for {key, value} <- opts do
|
||||
child_debug_key(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
defp child_debug(_min_level, _child) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp child_debug_key(:restart_type, value), do: ["\nRestart: " | inspect(value)]
|
||||
defp child_debug_key(:shutdown, value), do: ["\nShutdown: " | inspect(value)]
|
||||
defp child_debug_key(:child_type, value), do: ["\nType: " | inspect(value)]
|
||||
defp child_debug_key(:significant, value), do: if(value, do: "\nSignificant: true", else: [])
|
||||
defp child_debug_key(_, _), do: []
|
||||
|
||||
defp report_crash(min_level, [[{:initial_call, initial_call} | crashed], linked]) do
|
||||
mfa = initial_call_to_mfa(initial_call)
|
||||
report_crash(min_level, crashed, [{:initial_call, mfa}], linked)
|
||||
|
||||
@@ -62,7 +62,7 @@ defmodule LoggerTest do
|
||||
Logger.add_backend({UnknownBackend, :hello})
|
||||
end
|
||||
|
||||
test "logs or writes to stderr on failed backends" do
|
||||
test "logs or writes to stderr on failed call on async mode" do
|
||||
assert {:ok, _} = Logger.add_backend({MyBackend, :hello})
|
||||
|
||||
assert capture_log(fn ->
|
||||
@@ -86,6 +86,32 @@ defmodule LoggerTest do
|
||||
Logger.add_backend(:console)
|
||||
end
|
||||
|
||||
test "logs or writes to stderr on failed call on sync mode" do
|
||||
Logger.configure(sync_threshold: 0)
|
||||
assert {:ok, _} = Logger.add_backend({MyBackend, :hello})
|
||||
|
||||
assert capture_log(fn ->
|
||||
:gen_event.call(Logger, {MyBackend, :hello}, :error)
|
||||
wait_for_handler(Logger, {MyBackend, :hello})
|
||||
end) =~
|
||||
":gen_event handler {LoggerTest.MyBackend, :hello} installed in Logger terminating"
|
||||
|
||||
assert :ok = Logger.remove_backend(:console)
|
||||
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
:gen_event.call(Logger, {MyBackend, :hello}, :error)
|
||||
wait_for_handler(Logger, {MyBackend, :hello})
|
||||
end) =~
|
||||
":gen_event handler {LoggerTest.MyBackend, :hello} installed in Logger terminating"
|
||||
|
||||
# Flush logs before reattaching to avoid OTP reports
|
||||
Logger.flush()
|
||||
after
|
||||
Logger.configure(sync_threshold: 20)
|
||||
Logger.remove_backend({MyBackend, :hello})
|
||||
Logger.add_backend(:console)
|
||||
end
|
||||
|
||||
test "level/0" do
|
||||
assert Logger.level() == :debug
|
||||
|
||||
@@ -644,14 +670,12 @@ defmodule LoggerTest do
|
||||
end
|
||||
|
||||
test "updates config on config_change/3" do
|
||||
:ok = Logger.configure(level: :debug)
|
||||
|
||||
try do
|
||||
assert Logger.App.config_change([level: :error], [], []) === :ok
|
||||
assert Logger.level() === :error
|
||||
after
|
||||
Logger.configure(level: :debug)
|
||||
end
|
||||
assert Application.put_env(:logger, :level, :error) == :ok
|
||||
assert :application_controller.config_change(logger: [level: :debug]) == :ok
|
||||
assert Logger.level() == :error
|
||||
after
|
||||
Application.put_env(:logger, :level, :debug)
|
||||
Logger.configure(level: :debug)
|
||||
end
|
||||
|
||||
describe "OTP integration" do
|
||||
|
||||
@@ -78,7 +78,9 @@ defmodule Mix.CLI do
|
||||
defp run_task(name, args) do
|
||||
try do
|
||||
ensure_no_slashes(name)
|
||||
Mix.Tasks.Loadconfig.load_default()
|
||||
# We must go through the task instead of invoking the module directly
|
||||
# because projects like Nerves use this to invoke it early.
|
||||
Mix.Task.run("loadconfig")
|
||||
Mix.Task.run(name, args)
|
||||
rescue
|
||||
# We only rescue exceptions in the Mix namespace, all
|
||||
@@ -144,7 +146,7 @@ defmodule Mix.CLI do
|
||||
path = Path.join(Mix.Utils.mix_config(), "config.exs")
|
||||
|
||||
if File.regular?(path) do
|
||||
Mix.Tasks.Loadconfig.load_imports(path)
|
||||
Mix.Tasks.Loadconfig.load_compile(path)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ defmodule Mix.Compilers.ApplicationTracer do
|
||||
:ok
|
||||
end
|
||||
|
||||
# Also skip __impl__ calls inside protocols as they are meant
|
||||
# to invert dependencies.
|
||||
def trace({_, _, _, :__impl__, _}, _env) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def trace({type, meta, module, function, arity}, env)
|
||||
when type in [:remote_function, :remote_macro, :imported_function, :imported_macro] do
|
||||
# Unknown modules need to be looked up and filtered later
|
||||
@@ -110,8 +116,8 @@ defmodule Mix.Compilers.ApplicationTracer do
|
||||
def format_warning({:undefined_app, app, module, function, arity}) do
|
||||
"""
|
||||
#{Exception.format_mfa(module, function, arity)} defined in application :#{app} \
|
||||
is used by the current application but the current application does not directly \
|
||||
depend on :#{app}. To fix this, you must do one of:
|
||||
is used by the current application but the current application does not depend \
|
||||
on :#{app}. To fix this, you must do one of:
|
||||
|
||||
1. If :#{app} is part of Erlang/Elixir, you must include it under \
|
||||
:extra_applications inside "def application" in your mix.exs
|
||||
@@ -120,7 +126,7 @@ defmodule Mix.Compilers.ApplicationTracer do
|
||||
in your mix.exs
|
||||
|
||||
3. In case you don't want to add a requirement to :#{app}, you may \
|
||||
optionally skip this warning by adding [xref: [exclude: #{inspect(module)}] \
|
||||
optionally skip this warning by adding [xref: [exclude: [#{inspect(module)}]]] \
|
||||
to your "def project" in mix.exs
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -271,7 +271,7 @@ defmodule Mix.Compilers.Erlang do
|
||||
defp to_diagnostics(warnings_or_errors, severity) do
|
||||
for {file, issues} <- warnings_or_errors,
|
||||
{line, module, data} <- issues do
|
||||
position = if is_integer(line) and line >= 1, do: line
|
||||
position = line(line)
|
||||
|
||||
%Mix.Task.Compiler.Diagnostic{
|
||||
file: Path.absname(file),
|
||||
@@ -288,7 +288,12 @@ defmodule Mix.Compilers.Erlang do
|
||||
for {_, warnings} <- entries,
|
||||
{file, issues} <- warnings,
|
||||
{line, module, message} <- issues do
|
||||
IO.puts("#{file}:#{line}: Warning: #{module.format_error(message)}")
|
||||
IO.puts("#{file}:#{line(line)}: Warning: #{module.format_error(message)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp line({line, _column}) when is_integer(line) and line >= 1, do: line
|
||||
# TODO: remove when we require OTP 24
|
||||
defp line(line) when is_integer(line) and line >= 1, do: line
|
||||
defp line(_), do: nil
|
||||
end
|
||||
|
||||
@@ -176,7 +176,11 @@ defmodule Mix.Config do
|
||||
"""
|
||||
@doc deprecated: "Use Config.Reader.read_imports!/2 instead"
|
||||
def eval!(file, imported_paths \\ []) do
|
||||
Config.Reader.read_imports!(file, imports: imported_paths)
|
||||
Config.Reader.read_imports!(file,
|
||||
imports: imported_paths,
|
||||
env: Mix.env(),
|
||||
target: Mix.target()
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -195,7 +199,7 @@ defmodule Mix.Config do
|
||||
@doc deprecated: "Use Config.Reader.read!/2 instead"
|
||||
@spec read!(Path.t(), [Path.t()]) :: keyword
|
||||
def read!(file, imported_paths \\ []) do
|
||||
Config.Reader.read!(file, imports: imported_paths)
|
||||
Config.Reader.read!(file, imports: imported_paths, env: Mix.env(), target: Mix.target())
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -75,19 +75,6 @@ defmodule Mix.Dep do
|
||||
system_env: keyword
|
||||
}
|
||||
|
||||
@doc """
|
||||
Receives the project configuration and returns
|
||||
a map saying if dependencies are runtime or compile time.
|
||||
"""
|
||||
def deps_opts(config) do
|
||||
for config_dep <- Keyword.get(config, :deps, []),
|
||||
do: {elem(config_dep, 0), dep_opts(config_dep)}
|
||||
end
|
||||
|
||||
defp dep_opts({_app, opts}) when is_list(opts), do: opts
|
||||
defp dep_opts({_app, _req, opts}) when is_list(opts), do: opts
|
||||
defp dep_opts(_), do: []
|
||||
|
||||
@doc """
|
||||
Returns loaded dependencies from the cache for the current environment.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule Mix.Local do
|
||||
def path_for(:escript), do: Mix.path_for(:escripts)
|
||||
|
||||
@doc """
|
||||
Appends archives paths into Erlang code path.
|
||||
Appends archive paths to the Erlang code path.
|
||||
"""
|
||||
def append_archives do
|
||||
for archive <- archives_ebins() do
|
||||
@@ -48,7 +48,18 @@ defmodule Mix.Local do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Appends Mix paths into Erlang code path.
|
||||
Removes archive paths from Erlang code path.
|
||||
"""
|
||||
def remove_archives do
|
||||
for archive <- archives_ebins() do
|
||||
Code.delete_path(archive)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Appends Mix paths to the Erlang code path.
|
||||
"""
|
||||
def append_paths do
|
||||
Enum.each(mix_paths(), &Code.append_path(&1))
|
||||
|
||||
@@ -808,11 +808,7 @@ defmodule Mix.Release do
|
||||
{:ok, {_, chunks}} ->
|
||||
chunks = for {name, chunk} <- chunks, is_binary(chunk), do: {name, chunk}
|
||||
{:ok, binary} = :beam_lib.build_module(chunks)
|
||||
{:ok, fd} = :ram_file.open(binary, [:write, :binary])
|
||||
{:ok, _} = :ram_file.compress(fd)
|
||||
{:ok, binary} = :ram_file.get_file(fd)
|
||||
:ok = :ram_file.close(fd)
|
||||
{:ok, binary}
|
||||
{:ok, :zlib.gzip(binary)}
|
||||
|
||||
{:error, _, _} = error ->
|
||||
error
|
||||
|
||||
+26
-16
@@ -355,12 +355,12 @@ defmodule Mix.Task do
|
||||
alias = Mix.Project.config()[:aliases][String.to_atom(task)]
|
||||
|
||||
cond do
|
||||
is_nil(alias) ->
|
||||
run_task(proj, task, args)
|
||||
|
||||
Mix.TasksServer.run({:alias, task, proj}) ->
|
||||
alias && Mix.TasksServer.run({:alias, task, proj}) ->
|
||||
run_alias(List.wrap(alias), args, proj, task, :ok)
|
||||
|
||||
!Mix.TasksServer.get({:task, task, proj}) ->
|
||||
run_task(proj, task, args)
|
||||
|
||||
true ->
|
||||
:noop
|
||||
end
|
||||
@@ -371,8 +371,8 @@ defmodule Mix.Task do
|
||||
# 2. Otherwise we compile and load dependencies
|
||||
# 3. Finally, we compile the current project in hope it is available.
|
||||
module =
|
||||
get_task_or_run(proj, task, fn -> Mix.Task.run("deps.loadpaths") end) ||
|
||||
get_task_or_run(proj, task, fn -> Mix.Task.run("compile", []) end) ||
|
||||
get_task_or_run(proj, task, fn -> run("deps.loadpaths") end) ||
|
||||
get_task_or_run(proj, task, fn -> run("compile", []) end) ||
|
||||
get!(task)
|
||||
|
||||
recursive = recursive(module)
|
||||
@@ -387,15 +387,16 @@ defmodule Mix.Task do
|
||||
Mix.ProjectStack.on_recursing_root(fn -> run(task, args) end)
|
||||
|
||||
Mix.TasksServer.run({:task, task, proj}) ->
|
||||
if Mix.debug?(), do: output_task_debug_info(task, args, proj)
|
||||
run_requirements(module)
|
||||
|
||||
try do
|
||||
module.run(args)
|
||||
rescue
|
||||
e in OptionParser.ParseError ->
|
||||
Mix.raise("Could not invoke task #{inspect(task)}: " <> Exception.message(e))
|
||||
end
|
||||
with_debug(task, args, proj, fn ->
|
||||
try do
|
||||
module.run(args)
|
||||
rescue
|
||||
e in OptionParser.ParseError ->
|
||||
Mix.raise("Could not invoke task #{inspect(task)}: " <> Exception.message(e))
|
||||
end
|
||||
end)
|
||||
|
||||
true ->
|
||||
:noop
|
||||
@@ -409,8 +410,16 @@ defmodule Mix.Task do
|
||||
end)
|
||||
end
|
||||
|
||||
defp output_task_debug_info(task, args, proj) do
|
||||
Mix.shell().info("** Running mix " <> task_to_string(task, args) <> project_to_string(proj))
|
||||
defp with_debug(task, args, proj, fun) do
|
||||
if Mix.debug?() do
|
||||
shell = Mix.shell()
|
||||
shell.info(["-> Running mix ", task_to_string(task, args), project_to_string(proj)])
|
||||
{time, res} = :timer.tc(fun)
|
||||
shell.info(["<- Ran mix ", task, " in ", Integer.to_string(div(time, 1000)), "ms"])
|
||||
res
|
||||
else
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
defp project_to_string(nil), do: ""
|
||||
@@ -450,7 +459,8 @@ defmodule Mix.Task do
|
||||
run_alias(t, alias_args, proj, original_task, res)
|
||||
end
|
||||
|
||||
defp run_alias([], _alias_task, _proj, _original_task, res) do
|
||||
defp run_alias([], _alias_args, proj, original_task, res) do
|
||||
Mix.TasksServer.put({:task, original_task, proj})
|
||||
res
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ defmodule Mix.Tasks.App.Config do
|
||||
runtime = config[:config_path] |> Path.dirname() |> Path.join("runtime.exs")
|
||||
|
||||
if File.exists?(runtime) do
|
||||
Mix.Tasks.Loadconfig.load_file(runtime)
|
||||
Mix.Tasks.Loadconfig.load_runtime(runtime)
|
||||
end
|
||||
|
||||
if opts[:preload_modules] do
|
||||
|
||||
@@ -354,14 +354,15 @@ defmodule Mix.Tasks.Compile.App do
|
||||
defp apps_from_runtime_prod_deps(properties, config) do
|
||||
included_applications = Keyword.get(properties, :included_applications, [])
|
||||
|
||||
for {app, opts} <- Mix.Dep.deps_opts(config),
|
||||
for {app, opts} <- deps_opts(config),
|
||||
runtime_app?(opts),
|
||||
app not in included_applications,
|
||||
do: {app, if(Keyword.get(opts, :optional, false), do: :optional, else: :required)}
|
||||
end
|
||||
|
||||
defp runtime_app?(opts) do
|
||||
Keyword.get(opts, :runtime, true) and Keyword.get(opts, :app, true) and matching_only?(opts)
|
||||
Keyword.get(opts, :runtime, true) and Keyword.get(opts, :app, true) and matching_only?(opts) and
|
||||
matching_target?(opts)
|
||||
end
|
||||
|
||||
defp matching_only?(opts) do
|
||||
@@ -371,6 +372,22 @@ defmodule Mix.Tasks.Compile.App do
|
||||
end
|
||||
end
|
||||
|
||||
defp matching_target?(opts) do
|
||||
case Keyword.fetch(opts, :targets) do
|
||||
{:ok, value} -> Mix.target() in List.wrap(value)
|
||||
:error -> true
|
||||
end
|
||||
end
|
||||
|
||||
defp deps_opts(config) do
|
||||
for config_dep <- Keyword.get(config, :deps, []),
|
||||
do: {elem(config_dep, 0), dep_opts(config_dep)}
|
||||
end
|
||||
|
||||
defp dep_opts({_app, opts}) when is_list(opts), do: opts
|
||||
defp dep_opts({_app, _req, opts}) when is_list(opts), do: opts
|
||||
defp dep_opts(_), do: []
|
||||
|
||||
## Helpers for loading and manipulating apps
|
||||
|
||||
@doc false
|
||||
@@ -386,7 +403,7 @@ defmodule Mix.Tasks.Compile.App do
|
||||
Keyword.get(properties, :extra_applications, [])
|
||||
|
||||
project_apps(properties, config, extra, fn ->
|
||||
config |> Mix.Dep.deps_opts() |> Keyword.keys()
|
||||
config |> deps_opts() |> Keyword.keys()
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -103,12 +103,13 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
manifest = manifest()
|
||||
configs = [Mix.Project.config_mtime() | Mix.Tasks.Compile.Erlang.manifests()]
|
||||
force = opts[:force] || Mix.Utils.stale?(configs, [manifest])
|
||||
{tracers, opts} = pop_tracers(opts)
|
||||
|
||||
opts =
|
||||
(project[:elixirc_options] || [])
|
||||
|> Keyword.merge(opts)
|
||||
|> xref_exclude_opts(project)
|
||||
|> tracers_opts()
|
||||
|> tracers_opts(tracers)
|
||||
|> profile_opts()
|
||||
|
||||
Mix.Compilers.Elixir.compile(manifest, srcs, dest, [:ex], force, opts)
|
||||
@@ -134,17 +135,21 @@ defmodule Mix.Tasks.Compile.Elixir do
|
||||
end
|
||||
end
|
||||
|
||||
defp tracers_opts(opts) do
|
||||
defp pop_tracers(opts) do
|
||||
case Keyword.pop_values(opts, :tracer) do
|
||||
{[], opts} ->
|
||||
opts
|
||||
{[], opts}
|
||||
|
||||
{tracers, opts} ->
|
||||
tracers = Enum.map(tracers, &Module.concat([&1]))
|
||||
Keyword.update(opts, :tracers, tracers, &(tracers ++ &1))
|
||||
{Enum.map(tracers, &Module.concat([&1])), opts}
|
||||
end
|
||||
end
|
||||
|
||||
defp tracers_opts(opts, tracers) do
|
||||
tracers = tracers ++ Code.get_compiler_option(:tracers)
|
||||
Keyword.update(opts, :tracers, tracers, &(tracers ++ &1))
|
||||
end
|
||||
|
||||
defp profile_opts(opts) do
|
||||
case Keyword.fetch(opts, :profile) do
|
||||
{:ok, "time"} -> Keyword.put(opts, :profile, :time)
|
||||
|
||||
@@ -100,9 +100,10 @@ defmodule Mix.Tasks.Compile.Erlang do
|
||||
file = Erlang.to_erl_file(Path.rootname(input, ".erl"))
|
||||
|
||||
case :compile.file(file, erlc_options) do
|
||||
{:error, :badarg} ->
|
||||
# TODO: Don't handle {:error, :badarg} when we require OTP 24
|
||||
error when error == :error or error == {:error, :badarg} ->
|
||||
message =
|
||||
"Compiling Erlang #{inspect(file)} failed with ArgumentError, probably because of invalid :erlc_options"
|
||||
"Compiling Erlang file #{inspect(file)} failed, probably because of invalid :erlc_options"
|
||||
|
||||
Mix.raise(message)
|
||||
|
||||
|
||||
@@ -197,14 +197,8 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
config_path = Path.join(dep_path, "mix.rebar.config")
|
||||
lib_path = Path.join(config[:env_path], "lib/*/ebin")
|
||||
|
||||
# REBAR_BARE_COMPILER_OUTPUT_DIR is only honored by rebar3 >= 3.14
|
||||
env = [
|
||||
{"REBAR_CONFIG", config_path},
|
||||
{"TERM", "dumb"},
|
||||
{"REBAR_BARE_COMPILER_OUTPUT_DIR", dep_path}
|
||||
]
|
||||
|
||||
cmd = "#{rebar_cmd(dep)} bare compile --paths #{lib_path}"
|
||||
env = [{"REBAR_CONFIG", config_path}, {"TERM", "dumb"}]
|
||||
cmd = "#{rebar_cmd(dep)} bare compile --paths=#{inspect(lib_path)}"
|
||||
|
||||
File.mkdir_p!(dep_path)
|
||||
File.write!(config_path, rebar_config(dep))
|
||||
|
||||
@@ -29,35 +29,42 @@ defmodule Mix.Tasks.Loadconfig do
|
||||
|
||||
case args do
|
||||
[] -> load_default()
|
||||
[file] -> load_imports(file)
|
||||
[file] -> load_compile(file)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def load_default do
|
||||
defp load_default do
|
||||
config = Mix.Project.config()
|
||||
|
||||
if File.regular?(config[:config_path]) or config[:config_path] != "config/config.exs" do
|
||||
load_imports(config[:config_path])
|
||||
load_compile(config[:config_path])
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def load_imports(file) do
|
||||
# Loads compile-time configuration, they support imports, and are not deep merged.
|
||||
def load_compile(file) do
|
||||
{config, files} = Config.Reader.read_imports!(file, env: Mix.env(), target: Mix.target())
|
||||
Mix.ProjectStack.loaded_config(persist_apps(config, file), files)
|
||||
config
|
||||
end
|
||||
|
||||
@doc false
|
||||
def load_file(file) do
|
||||
# Loads runtime configuration, they do not support imports, and are deep merged.
|
||||
def load_runtime(file) do
|
||||
config = Config.Reader.read!(file, env: Mix.env(), target: Mix.target(), imports: :disabled)
|
||||
Mix.ProjectStack.loaded_config(persist_apps(config, file), [])
|
||||
Mix.ProjectStack.loaded_config(persist_apps(hydrate_apps(config), file), [])
|
||||
config
|
||||
end
|
||||
|
||||
defp hydrate_apps(config) do
|
||||
for {app, pairs} <- config do
|
||||
hd(Config.Reader.merge([{app, Application.get_all_env(app)}], [{app, pairs}]))
|
||||
end
|
||||
end
|
||||
|
||||
defp persist_apps(config, file) do
|
||||
Application.put_all_env(config, persistent: true)
|
||||
apps = Keyword.keys(config)
|
||||
|
||||
@@ -154,7 +154,13 @@ defmodule Mix.Tasks.Run do
|
||||
|
||||
defp process_config(opts) do
|
||||
for {:config, value} <- opts do
|
||||
Mix.Tasks.Loadconfig.load_imports(value)
|
||||
# TODO: Remove on v2.0.
|
||||
IO.warn(
|
||||
"the --config flag is deprecated. If you need to handle multiple configurations, " <>
|
||||
"it is preferable to dynamically import them in your config files"
|
||||
)
|
||||
|
||||
Mix.Tasks.Loadconfig.load_compile(value)
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
@@ -252,7 +252,8 @@ defmodule Mix.Tasks.Xref do
|
||||
{:ok, {_, [debug_info: debug_info]}} <- :beam_lib.chunks(path, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- debug_info,
|
||||
{:ok, %{definitions: defs}} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
do: walk_definitions(module, source, defs)
|
||||
do: walk_definitions(module, source, defs),
|
||||
else: (_ -> [])
|
||||
end
|
||||
|
||||
defp walk_definitions(module, file, definitions) do
|
||||
|
||||
@@ -21,6 +21,10 @@ defmodule Mix.TasksServer do
|
||||
Agent.update(@name, &Map.put(&1, tuple, true), @timeout)
|
||||
end
|
||||
|
||||
def get(tuple) do
|
||||
Agent.get(@name, &Map.get(&1, tuple), @timeout)
|
||||
end
|
||||
|
||||
def delete_many(many) do
|
||||
Agent.update(@name, &Map.drop(&1, many), @timeout)
|
||||
end
|
||||
|
||||
@@ -588,7 +588,7 @@ defmodule Mix.Utils do
|
||||
|
||||
# Starting an HTTP client profile allows us to scope
|
||||
# the effects of using an HTTP proxy to this function
|
||||
{:ok, _pid} = :inets.start(:httpc, [{:profile, :mix}])
|
||||
{:ok, _pid} = :inets.start(:httpc, profile: :mix)
|
||||
|
||||
headers = [{'user-agent', 'Mix/#{System.version()}'}]
|
||||
request = {:binary.bin_to_list(path), headers}
|
||||
@@ -601,7 +601,28 @@ defmodule Mix.Utils do
|
||||
# If a proxy environment variable was supplied add a proxy to httpc.
|
||||
http_options = [relaxed: true] ++ proxy_config(path)
|
||||
|
||||
case :httpc.request(:get, request, http_options, [body_format: :binary], :mix) do
|
||||
case httpc_request(request, http_options) do
|
||||
{:error, {:failed_connect, [{:to_address, _}, {inet, _, reason}]}}
|
||||
when inet in [:inet, :inet6] and reason in [:ehostunreach, :enetunreach] ->
|
||||
:httpc.set_options([ipfamily: fallback(inet)], :mix)
|
||||
request |> httpc_request(http_options) |> httpc_response()
|
||||
|
||||
response ->
|
||||
httpc_response(response)
|
||||
end
|
||||
after
|
||||
:inets.stop(:httpc, :mix)
|
||||
end
|
||||
|
||||
defp fallback(:inet), do: :inet6
|
||||
defp fallback(:inet6), do: :inet
|
||||
|
||||
defp httpc_request(request, http_options) do
|
||||
:httpc.request(:get, request, http_options, [body_format: :binary], :mix)
|
||||
end
|
||||
|
||||
defp httpc_response(response) do
|
||||
case response do
|
||||
{:ok, {{_, status, _}, _, body}} when status in 200..299 ->
|
||||
{:ok, body}
|
||||
|
||||
@@ -611,8 +632,6 @@ defmodule Mix.Utils do
|
||||
{:error, reason} ->
|
||||
{:remote, "httpc request failed with: #{inspect(reason)}"}
|
||||
end
|
||||
after
|
||||
:inets.stop(:httpc, :mix)
|
||||
end
|
||||
|
||||
defp file?(path) do
|
||||
|
||||
@@ -10,11 +10,14 @@ defmodule Mix.AliasesTest do
|
||||
h: "hello",
|
||||
p: &inspect/1,
|
||||
compile: "hello",
|
||||
cmd: &call_cmd/1,
|
||||
help: ["help", "hello"],
|
||||
"nested.h": [&Mix.shell().info(inspect(&1)), "h foo bar"]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp call_cmd(args), do: Mix.Task.run("cmd", args)
|
||||
end
|
||||
|
||||
setup do
|
||||
@@ -50,13 +53,18 @@ defmodule Mix.AliasesTest do
|
||||
assert Mix.Task.run("compile", []) == :noop
|
||||
end
|
||||
|
||||
test "run alias override with recursion" do
|
||||
test "run alias override with name-recursion" do
|
||||
assert Mix.Task.rerun("help", []) == "Hello, World!"
|
||||
assert_received {:mix_shell, :info, ["mix test" <> _]}
|
||||
|
||||
# Arguments are passed to the recursive task and not the last one.
|
||||
assert ExUnit.CaptureIO.capture_io(fn ->
|
||||
Mix.Task.rerun("help", ["test"]) == "Hello, World!"
|
||||
Mix.Task.rerun("help", ["test"])
|
||||
end) =~ "mix test"
|
||||
end
|
||||
|
||||
test "run alias override with code-recursion" do
|
||||
assert Mix.Task.rerun("cmd", ["echo", "hello"]) == :ok
|
||||
assert_received {:mix_shell, :run, ["hello" <> _]}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,11 +60,11 @@ defmodule Mix.CLITest do
|
||||
assert contents =~ "This won't appear"
|
||||
|
||||
contents = mix(~w[my_hello], [{"MIX_DEBUG", "1"}])
|
||||
assert contents =~ "** Running mix my_hello (inside MyProject)"
|
||||
assert contents =~ "-> Running mix my_hello (inside MyProject)"
|
||||
assert contents =~ "** (Mix.Error) oops"
|
||||
|
||||
contents = mix(~w[my_hello], [{"MIX_DEBUG", "0"}])
|
||||
refute contents =~ "** Running mix my_hello (inside MyProject)"
|
||||
refute contents =~ "-> Running mix my_hello (inside MyProject)"
|
||||
refute contents =~ "** (Mix.Error) oops"
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -59,7 +59,8 @@ defmodule Mix.TaskTest do
|
||||
Mix.shell(Mix.Shell.IO)
|
||||
Mix.debug(true)
|
||||
|
||||
assert ExUnit.CaptureIO.capture_io(fn -> Mix.Task.run("hello") end) =~ "** Running mix hello"
|
||||
assert ExUnit.CaptureIO.capture_io(fn -> Mix.Task.run("hello") end) =~
|
||||
~r"-> Running mix hello\n<- Ran mix hello in \d+ms"
|
||||
after
|
||||
Mix.shell(Mix.Shell.Process)
|
||||
Mix.debug(false)
|
||||
|
||||
@@ -17,11 +17,13 @@ defmodule Mix.Tasks.App.ConfigTest do
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :sample, from_compile: :compile, from_runtime: :compile
|
||||
config :sample, :nested, from_compile: :compile, from_runtime: :compile
|
||||
""")
|
||||
|
||||
File.write!("config/runtime.exs", """
|
||||
import Config
|
||||
config :sample, from_runtime: :runtime
|
||||
config :sample, :nested, from_compile: :compile, from_runtime: :compile
|
||||
""")
|
||||
|
||||
Mix.Task.run("loadconfig")
|
||||
@@ -30,10 +32,12 @@ defmodule Mix.Tasks.App.ConfigTest do
|
||||
assert Application.get_all_env(:sample) |> Enum.sort() == [
|
||||
from_compile: :compile,
|
||||
from_env: :env,
|
||||
from_runtime: :runtime
|
||||
from_runtime: :runtime,
|
||||
nested: [from_compile: :compile, from_runtime: :compile]
|
||||
]
|
||||
end)
|
||||
after
|
||||
Application.delete_env(:sample, :nested, persistent: true)
|
||||
Application.delete_env(:sample, :from_env, persistent: true)
|
||||
Application.delete_env(:sample, :from_compile, persistent: true)
|
||||
Application.delete_env(:sample, :from_runtime, persistent: true)
|
||||
|
||||
@@ -19,7 +19,11 @@ defmodule Mix.Tasks.ArchiveTest do
|
||||
File.rm_rf!(tmp_path("userhome"))
|
||||
System.put_env("MIX_ARCHIVES", tmp_path("userhome/.mix/archives/"))
|
||||
Mix.Project.push(ArchiveProject)
|
||||
:ok
|
||||
|
||||
on_exit(fn ->
|
||||
Mix.Local.remove_archives()
|
||||
System.delete_env("MIX_ARCHIVES")
|
||||
end)
|
||||
end
|
||||
|
||||
test "archive build" do
|
||||
|
||||
@@ -40,7 +40,9 @@ defmodule Mix.Tasks.Compile.AppTest do
|
||||
{:ok6, path: "../ok", optional: true},
|
||||
{:ok7, path: "../ok", optional: false},
|
||||
{:ok8, path: "../ok", app: false},
|
||||
{:ok9, path: "../ok"}
|
||||
{:ok9, path: "../ok"},
|
||||
{:ok10, path: "../ok", targets: [:will_never_be_listed]},
|
||||
{:ok11, path: "../ok", targets: [Mix.target()]}
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -130,7 +132,7 @@ defmodule Mix.Tasks.Compile.AppTest do
|
||||
properties = parse_resource_file(:custom_deps)
|
||||
|
||||
assert properties[:applications] ==
|
||||
[:kernel, :stdlib, :elixir, :logger, :ok1, :ok3, :ok4, :ok7]
|
||||
[:kernel, :stdlib, :elixir, :logger, :ok1, :ok3, :ok4, :ok7, :ok11]
|
||||
end)
|
||||
end
|
||||
|
||||
|
||||
@@ -57,6 +57,19 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
Code.put_compiler_option(:tracers, [])
|
||||
end
|
||||
|
||||
test "compiles a project with a previously set custom tracer" do
|
||||
Process.register(self(), __MODULE__)
|
||||
Code.put_compiler_option(:tracers, [__MODULE__])
|
||||
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Tasks.Compile.Elixir.run([])
|
||||
assert_received {:alias_reference, _meta, A}
|
||||
assert_received {:alias_reference, _meta, B}
|
||||
end)
|
||||
after
|
||||
Code.put_compiler_option(:tracers, [])
|
||||
end
|
||||
|
||||
test "warns when Logger is used but not depended on" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
File.write!("lib/a.ex", """
|
||||
@@ -67,7 +80,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
""")
|
||||
|
||||
message =
|
||||
"Logger.info/1 defined in application :logger is used by the current application but the current application does not directly depend on :logger"
|
||||
"Logger.info/1 defined in application :logger is used by the current application but the current application does not depend on :logger"
|
||||
|
||||
assert capture_io(:stderr, fn ->
|
||||
Mix.Task.run("compile", [])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user